Compare commits
No commits in common. "main" and "ci/pr-intake-minimum-20" have entirely different histories.
main
...
ci/pr-inta
|
|
@ -1,81 +0,0 @@
|
|||
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.
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
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=<start>:<count>` (presents; `APS5_TRACE_SECONDS=<start>:<length>` also works), as described under [Performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace). It writes `aps5-trace-<commit>-<pid>.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-<commit>-<pid>.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 <trace>` (with `--stacks <file>.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 (<file stem>.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
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
### What
|
||||
|
||||
### Tested
|
||||
<!-- OS, title or homebrew, what was checked. "Not tested" is a valid answer. A performance change: the summary lines of main and of this PR from the same machine and scene, with the traces attached, captured as the Performance report issue form describes. -->
|
||||
<!-- OS, title or homebrew, what was checked. "Not tested" is a valid answer. -->
|
||||
|
||||
### Checklist
|
||||
<!-- Rules behind each item: https://github.com/boykopovar/AnyPS5/blob/main/CONTRIBUTING.md -->
|
||||
- [ ] 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 under Silent stubs in their library's [technical debt](https://github.com/boykopovar/AnyPS5/tree/main/docs/dev/debt) file
|
||||
- [ ] 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)
|
||||
- [ ] 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: <!-- #PR, or none -->
|
||||
- [ ] Unblocks: <!-- title ID and Game bring-up discussion, e.g. PPSA01325 #3232, or none -->
|
||||
- [ ] GPUs tested: <!-- for AGC, shader recompiler or VideoOut changes: vendor, model, driver and OS of each GPU the GPU tests ran on; otherwise n/a -->
|
||||
- [ ] AI-assisted: <!-- yes / no -->
|
||||
|
|
|
|||
|
|
@ -14,34 +14,16 @@ jobs:
|
|||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/area-state.json
|
||||
key: area-labels-${{ github.run_id }}
|
||||
restore-keys: area-labels-
|
||||
- id: prs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
state=$RUNNER_TEMP/area-state.json
|
||||
[ -s "$state" ] || echo '{}' > "$state"
|
||||
config=$(gh api "repos/$GITHUB_REPOSITORY/contents/.github/labeler.yml" --jq .sha)
|
||||
gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 200 --json number,headRefOid \
|
||||
--jq "map({key: (.number | tostring), value: (.headRefOid + \"-$config\")}) | from_entries" > "$RUNNER_TEMP/area-open.json"
|
||||
jq -r --slurpfile old "$state" 'to_entries[] | select($old[0][.key] != .value) | .key' "$RUNNER_TEMP/area-open.json" | head -n 40 > "$RUNNER_TEMP/area-todo"
|
||||
jq -c --slurpfile open "$RUNNER_TEMP/area-open.json" --rawfile todo "$RUNNER_TEMP/area-todo" \
|
||||
'with_entries(select(.key | in($open[0]))) + ($open[0] | with_entries(select(.key | IN($todo | split("\n")[]))))' "$state" > "$RUNNER_TEMP/area-next.json"
|
||||
{
|
||||
echo 'numbers<<EOF'
|
||||
cat "$RUNNER_TEMP/area-todo"
|
||||
gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 200 --json number --jq '.[].number'
|
||||
echo EOF
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- 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 }}
|
||||
|
|
|
|||
|
|
@ -12,18 +12,13 @@ on:
|
|||
description: Pull request number to build
|
||||
required: true
|
||||
type: string
|
||||
linux:
|
||||
description: Also build and test on Linux
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'push' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CONFIGURE: >-
|
||||
|
|
@ -41,11 +36,7 @@ jobs:
|
|||
pull-requests: read
|
||||
outputs:
|
||||
sha: ${{ steps.gate.outputs.sha }}
|
||||
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 }}
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
|
|
@ -59,11 +50,7 @@ jobs:
|
|||
throw new Error('Automatic full builds only run on main');
|
||||
}
|
||||
core.setOutput('sha', context.sha);
|
||||
core.setOutput('pr', '');
|
||||
core.setOutput('head', '');
|
||||
core.setOutput('relinker', true);
|
||||
core.setOutput('register-context', true);
|
||||
core.setOutput('write-watch', true);
|
||||
core.setOutput('full', true);
|
||||
await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write();
|
||||
return;
|
||||
|
|
@ -96,7 +83,7 @@ jobs:
|
|||
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) {
|
||||
if (pr.mergeable !== true || !pr.merge_commit_sha) {
|
||||
throw new Error('The pull request does not merge cleanly into main; rebase it first');
|
||||
}
|
||||
let relinker = manual;
|
||||
|
|
@ -106,32 +93,12 @@ jobs:
|
|||
});
|
||||
relinker = commits.length > 0;
|
||||
}
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 });
|
||||
const paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
|
||||
const touches = (...prefixes) => files.length >= 3000 ||
|
||||
paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix));
|
||||
const workflow = '.github/workflows/build.yml';
|
||||
const libc = 'core/libs/prx/libc/';
|
||||
relinker = relinker && touches(
|
||||
'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp',
|
||||
'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp',
|
||||
'tools/', 'cmake/', 'CMakeLists.txt', workflow
|
||||
);
|
||||
const registerContext = touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow);
|
||||
const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow);
|
||||
const { 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('sha', pr.merge_commit_sha);
|
||||
core.setOutput('relinker', relinker);
|
||||
core.setOutput('register-context', registerContext);
|
||||
core.setOutput('write-watch', writeWatch);
|
||||
core.setOutput('full', manual);
|
||||
core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`);
|
||||
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: no relinker file changed or the author has no commit in main'}, ` +
|
||||
`register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}`
|
||||
`PR #${pull_number} at ${pr.head.sha} merged into main as ${pr.merge_commit_sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` +
|
||||
`relinker ${relinker ? 'enabled' : 'skipped because the author has no commit in main'}`
|
||||
).write();
|
||||
|
||||
relinker:
|
||||
|
|
@ -160,16 +127,6 @@ jobs:
|
|||
with:
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
shell: bash
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
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
|
||||
- if: runner.os == 'Windows'
|
||||
id: toolchain
|
||||
uses: actions/cache@v6
|
||||
|
|
@ -186,40 +143,9 @@ jobs:
|
|||
- run: cmake --build build-relinker --parallel 3
|
||||
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120
|
||||
|
||||
register-context:
|
||||
needs: eligibility
|
||||
if: ${{ needs.eligibility.outputs.register-context == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-15-intel]
|
||||
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
|
||||
|
||||
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
|
||||
- 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' }}
|
||||
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
|
|
@ -228,49 +154,22 @@ jobs:
|
|||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
- 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: |
|
||||
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
|
||||
with:
|
||||
key: linux
|
||||
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
save: false
|
||||
evict-old-files: job
|
||||
- name: Install lavapipe and the SDL audio and XRandR development files
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev
|
||||
- id: mesa
|
||||
run: |
|
||||
echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT"
|
||||
echo "cpu=$(grep -m1 '^flags' /proc/cpuinfo | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: .mesa-shader-cache
|
||||
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
|
||||
restore-keys: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-
|
||||
- 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
|
||||
- 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
|
||||
- run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log
|
||||
env:
|
||||
ANYPS5_REQUIRE_VULKAN: 1
|
||||
GALLIUM_OVERRIDE_CPU_CAPS: avx
|
||||
MESA_SHADER_CACHE_DIR: ${{ github.workspace }}/.mesa-shader-cache
|
||||
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !cancelled() }}
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: .mesa-shader-cache
|
||||
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
|
||||
- run: python3 tools/check_skips.py build/ctest.xml
|
||||
- name: Report tests slower than 30 s
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
|
|
@ -297,17 +196,6 @@ jobs:
|
|||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
- 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: |
|
||||
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
|
||||
- id: toolchain
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
|
|
@ -321,10 +209,10 @@ jobs:
|
|||
- uses: hendrikmuhs/ccache-action@v1.2.24
|
||||
with:
|
||||
key: windows
|
||||
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
save: false
|
||||
evict-old-files: job
|
||||
- shell: cmd
|
||||
run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
|
||||
run: cmake -S . -B build %CONFIGURE%
|
||||
- name: Check that every test executable is registered with ctest
|
||||
shell: cmd
|
||||
run: python tools/check_test_registration.py build
|
||||
|
|
@ -333,8 +221,7 @@ jobs:
|
|||
- 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/check_skips.py build/ctest.xml --job windows
|
||||
run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log
|
||||
- name: Report tests slower than 30 s
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ on:
|
|||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: conflict-label
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
|
|
|
|||
|
|
@ -19,12 +19,11 @@ jobs:
|
|||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- run: |
|
||||
existing=$(gh label list --limit 1000 --json name --jq '.[].name')
|
||||
label() { grep -qixF -- "$1" <<< "$existing" || gh label create "$1" --color "$2" --description "$3"; }
|
||||
label() { gh label create "$1" --color "$2" --description "$3"; }
|
||||
force_label() { gh label create "$1" --color "$2" --description "$3" --force; }
|
||||
|
||||
force_label 'needs rebase' b60205 'Conflicts with main, rebase needed'
|
||||
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 'intake paused' d93f0b 'Closed while PR intake is limited to authors with at least 20 PRs merged into main'
|
||||
force_label ci 5319e7 '.github'
|
||||
force_label agc 1d76db 'libSceAgc*'
|
||||
force_label shader 1d76db 'core/shader'
|
||||
|
|
@ -35,4 +34,3 @@ jobs:
|
|||
label prx c5def5 'Other PRX libraries'
|
||||
label build bfd4f2 'CMake, submodules, 3rdparty'
|
||||
label docs 0075ca 'Documentation'
|
||||
label stale ededed 'Conflicting with main and no activity for 7 days'
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
name: Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_run:
|
||||
workflows: [Release]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
pages:
|
||||
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
- name: Build web converter
|
||||
shell: bash
|
||||
run: |
|
||||
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk"
|
||||
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.3
|
||||
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3
|
||||
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
|
||||
emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build-wasm --target relinker_web --parallel 2
|
||||
- name: Test web converter
|
||||
run: python3 tools/test_web.py --build build-wasm
|
||||
- name: Download latest release libraries
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: python3 tools/build_web.py --build build-wasm --libraries
|
||||
- run: python3 tools/progress.py build-wasm/web
|
||||
- run: python3 tools/progress_history.py build-wasm/web
|
||||
- uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: build-wasm/web
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
|
|
@ -3,7 +3,7 @@ name: PR intake
|
|||
on:
|
||||
pull_request_target:
|
||||
branches: [main]
|
||||
types: [opened, reopened]
|
||||
types: [opened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ jobs:
|
|||
if (pr.state !== 'open' || pr.base.ref !== 'main') {
|
||||
return;
|
||||
}
|
||||
const minimumMergedPRs = 20;
|
||||
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
|
||||
|
|
@ -34,20 +35,10 @@ jobs:
|
|||
if (mergedPRs.incomplete_results || !Number.isSafeInteger(mergedPRs.total_count) || mergedPRs.total_count < 0) {
|
||||
throw new Error('Could not reliably count merged pull requests');
|
||||
}
|
||||
const merged = mergedPRs.total_count;
|
||||
if (merged >= 20) {
|
||||
if (mergedPRs.total_count >= minimumMergedPRs) {
|
||||
return;
|
||||
}
|
||||
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 open = new Set(openPRs.filter((p) => p.user.login === pr.user.login).map((p) => p.number));
|
||||
open.add(pull_number);
|
||||
if (open.size <= allowedOpen) {
|
||||
return;
|
||||
}
|
||||
const name = 'open PR limit';
|
||||
const name = 'intake paused';
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name });
|
||||
} catch (error) {
|
||||
|
|
@ -57,7 +48,7 @@ jobs:
|
|||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner, repo, name, color: 'd93f0b',
|
||||
description: 'Closed over the open PR limit: 1 without merged PRs, 20 with at least 1, none from 20'
|
||||
description: 'Closed while PR intake is limited to authors with at least 20 PRs merged into main'
|
||||
});
|
||||
} catch (createError) {
|
||||
if (createError.status !== 422) {
|
||||
|
|
@ -67,7 +58,7 @@ jobs:
|
|||
}
|
||||
}
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: pull_number, labels: [name] });
|
||||
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 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 comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: pull_number, per_page: 100
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,30 +22,15 @@ 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='<!-- pr-overlap -->'
|
||||
prev=
|
||||
for file in "$out"/*.md; do
|
||||
[ -z "$prev" ] || record "$prev" "$hash"
|
||||
pr=$(basename "$file" .md)
|
||||
hash=$(sha256sum "$file" | cut -d' ' -f1)
|
||||
prev=
|
||||
[ "$(jq -r --arg pr "$pr" '.[$pr] // empty' "$state")" = "$hash" ] && continue
|
||||
prev=$pr
|
||||
comment=$(gh api "repos/$GH_REPO/issues/$pr/comments" --paginate \
|
||||
--jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | [.id, .body] | @json" | head -n1)
|
||||
id=$(jq -r '.[0] // empty' <<< "${comment:-null}")
|
||||
|
|
@ -60,11 +45,3 @@ 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 }}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
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
|
||||
|
|
@ -49,8 +49,6 @@ jobs:
|
|||
path: C:\winlibs
|
||||
key: ${{ env.WINLIBS_ZIP }}
|
||||
- if: matrix.platform == 'windows' && steps.toolchain.outputs.cache-hit != 'true'
|
||||
env:
|
||||
SSL_CERT_FILE: 'C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt'
|
||||
run: |
|
||||
curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP"
|
||||
7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs
|
||||
|
|
@ -60,17 +58,12 @@ 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
|
||||
- name: Configure Linux
|
||||
if: matrix.platform == 'linux'
|
||||
run: cmake -S . -B build $CONFIGURE
|
||||
- name: Configure Windows
|
||||
if: matrix.platform == 'windows'
|
||||
shell: cmd
|
||||
env:
|
||||
SSL_CERT_FILE: 'C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt'
|
||||
run: cmake -S . -B build %CONFIGURE%
|
||||
- name: Build project and tests (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
|
|
@ -82,9 +75,6 @@ jobs:
|
|||
- name: Run tests (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error
|
||||
env:
|
||||
ANYPS5_REQUIRE_VULKAN: 1
|
||||
GALLIUM_OVERRIDE_CPU_CAPS: avx
|
||||
- name: Run tests (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
shell: cmd
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
name: Stale conflicts
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
warn:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
only-pr-labels: needs rebase
|
||||
days-before-issue-stale: -1
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 7
|
||||
days-before-pr-close: -1
|
||||
stale-pr-label: stale
|
||||
stale-pr-message: 'This pull request has conflicted with `main` and had no activity for 7 days. Rebase it on current `main` so it can be reviewed and merged. It will not be closed automatically.'
|
||||
remove-pr-stale-when-updated: true
|
||||
exempt-draft-pr: true
|
||||
operations-per-run: 200
|
||||
|
|
@ -28,12 +28,6 @@
|
|||
[submodule "3rdparty/stb"]
|
||||
path = 3rdparty/stb
|
||||
url = https://github.com/nothings/stb.git
|
||||
[submodule "3rdparty/tracy"]
|
||||
path = 3rdparty/tracy
|
||||
url = https://github.com/wolfpld/tracy.git
|
||||
[submodule "3rdparty/libjpeg-turbo"]
|
||||
path = 3rdparty/libjpeg-turbo
|
||||
url = https://github.com/libjpeg-turbo/libjpeg-turbo.git
|
||||
[submodule "3rdparty/libusb-cmake"]
|
||||
path = 3rdparty/libusb-cmake
|
||||
url = https://github.com/libusb/libusb-cmake.git
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit bea6567b63796ab27c1c33257d230284fb2d8316
|
||||
|
|
@ -1 +0,0 @@
|
|||
Subproject commit 30997d5ca6bb632cc10807a1da8a6d3de0aeeb3c
|
||||
141
CMakeLists.txt
141
CMakeLists.txt
|
|
@ -10,19 +10,6 @@ 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)
|
||||
|
|
@ -37,104 +24,27 @@ 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=$<BOOL:${APS5_ENABLE_TIMING_LOG}>)
|
||||
include(cmake/SystemDeps.cmake)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
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)
|
||||
if(NOT APPLE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++")
|
||||
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)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
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 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 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 test_registration COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py)
|
||||
add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py)
|
||||
add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
|
||||
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_debt_split.py)
|
||||
add_test(NAME stubgen_stdout_newlines COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py)
|
||||
add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py)
|
||||
add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py)
|
||||
add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py)
|
||||
add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py)
|
||||
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
|
||||
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py)
|
||||
add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py)
|
||||
set_tests_properties(pad_input PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
|
||||
add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py)
|
||||
add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
|
@ -158,28 +68,10 @@ 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)
|
||||
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(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}")
|
||||
set(ARCHITECTURE x86_64)
|
||||
add_subdirectory(3rdparty/ffmpeg-core)
|
||||
if(MINGW)
|
||||
|
|
@ -188,33 +80,14 @@ 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)
|
||||
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)
|
||||
add_subdirectory(3rdparty/freetype EXCLUDE_FROM_ALL)
|
||||
set_target_properties(freetype PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
add_subdirectory(core/shader/recompiler)
|
||||
add_subdirectory(core/Decoder/Jpeg)
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@
|
|||
- 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 under Silent stubs in its library's [technical debt](docs/dev/debt/) file.
|
||||
- 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).
|
||||
- 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).
|
||||
- Avoid non-standard extensions (`__attribute__`, etc.) where standard C++ is enough. Helper symbols that must not become NIDs use the `_nid_no_patch` or `_nid_no_patch_cut` suffix.
|
||||
- The relinker uses only the C++20 standard library.
|
||||
- Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system. With `ANYPS5_USE_SYSTEM_DEPS=ON` the packages packaged by the distributions (SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools) come from the system instead; that resolution lives in [cmake/SystemDeps.cmake](cmake/SystemDeps.cmake) and the default build never reads the system.
|
||||
- Third-party code is added as a submodule under `3rdparty/` and built from source, not found on 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.
|
||||
|
||||
|
|
@ -31,15 +31,9 @@ Python 3 is optional; without it some relinker tests are not registered.
|
|||
|
||||
Every push to `main`, including a merged pull request, automatically runs full Linux and Windows builds and tests and the relinker matrix on Linux, Windows and macOS, using the pushed commit without contributor checks.
|
||||
|
||||
For pull requests, full Linux and Windows builds and tests run only when a collaborator with write access selects Actions > Build > Run workflow on `main` and enters an open pull request number targeting `main`. A collaborator can start the same manual run from the command line, replacing `1234` with the pull request number:
|
||||
For pull requests, full Linux and Windows builds and tests run only when a collaborator with write access selects Actions > Build > Run workflow on `main` and enters an open pull request number targeting `main`. 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.
|
||||
|
||||
```
|
||||
gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234
|
||||
```
|
||||
|
||||
This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
```
|
||||
python3 tools/check_conventions.py --base origin/main
|
||||
|
|
@ -50,20 +44,13 @@ 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.
|
||||
- A pull request that conflicts with `main` and has no activity for 7 days is labeled `stale` with a reminder. It is not closed.
|
||||
- If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise).
|
||||
- If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first.
|
||||
- Fixes a title needs go to `main` as pull requests. A Game bring-up discussion lists the open pull requests and local changes the title still needs; update the list when one is merged, closed or replaced.
|
||||
- The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`.
|
||||
- If a pull request needs another one first, say so in the description (`Depends on #N`).
|
||||
- Run the tests before opening the pull request and describe what was tested (OS, title or homebrew).
|
||||
- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form.
|
||||
- Changes to the AGC driver, the shader recompiler or VideoOut name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging.
|
||||
- Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments.
|
||||
- Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it.
|
||||
|
||||
## Documentation
|
||||
|
||||
Text in the repository is dry, strict and concise. Record known gaps, unknown NIDs and signatures in [technical debt](docs/dev/TechnicalDebt.md) instead of new files. An entry goes in `docs/dev/debt/<name>.md`, named after the first `core/` path it links (`<name>` for `core/libs/prx/<name>/...`, otherwise for `core/<name>/...`), under the same `###` sections as the index; entries that link neither stay in the index. A pull request whose `TechnicalDebt.md` change conflicts after the split rebases and runs `python3 tools/debt_split.py resolve` during the stopped rebase, which moves its added, edited and removed entries to their files; `python3 tools/debt_split.py split` moves entries left in the index.
|
||||
Text in the repository is dry, strict and concise. Record known gaps, unknown NIDs and signatures in [technical debt](docs/dev/TechnicalDebt.md) instead of new files.
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -2,33 +2,29 @@
|
|||
|
||||
Tool for automatic executables porting to Linux and Windows.
|
||||
|
||||
[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
|
||||
|
||||

|
||||
|
||||
[Discord](https://discord.gg/BHFztBPUe)
|
||||
|
||||
Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process.
|
||||
|
||||
[Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Architecture](docs/dev/ARCHITECTURE.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md)
|
||||
|
||||
## Status
|
||||
|
||||
[](https://boykopovar.github.io/AnyPS5/progress.html) [](https://boykopovar.github.io/AnyPS5/progress.html)
|
||||
[](https://boykopovar.github.io/AnyPS5/) [](https://boykopovar.github.io/AnyPS5/)
|
||||
|
||||
[](https://boykopovar.github.io/AnyPS5/progress.html)
|
||||
|
||||
[Progress history](https://boykopovar.github.io/AnyPS5/history.html)
|
||||
[](https://boykopovar.github.io/AnyPS5/)
|
||||
|
||||
<sub>* 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.</sub>
|
||||
|
||||
[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
|
||||
|
||||
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.
|
||||
See the [game compatibility list](docs/user/COMPATIBILITY.md) for tested games and known issues.
|
||||
|
||||
## Input mapping
|
||||
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
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 <vulkan/vulkan.h>\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()
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
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()
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
extern "C" int __cxa_thread_atexit(void (*)(void*), void*, void*) {
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -138,9 +138,9 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> 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;
|
||||
|
|
|
|||
|
|
@ -153,12 +153,6 @@ 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<const std::uint8_t>(PALETTE_TRNS, size));
|
||||
Require(truncated.has_value() && !truncated->hasTransparency);
|
||||
}
|
||||
const auto completeTransparency = Decoder::Png::ParseHeader(std::span<const std::uint8_t>(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<std::uint8_t>({255, 0, 0, 255, 0, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 128}));
|
||||
|
|
|
|||
|
|
@ -3,12 +3,6 @@ if(NOT WIN32)
|
|||
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()
|
||||
|
||||
add_executable(nid_patcher
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/Sha1.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/NidCompute.cpp
|
||||
|
|
@ -16,7 +10,6 @@ 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
|
||||
)
|
||||
|
|
@ -25,14 +18,7 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i
|
|||
target_compile_features(nid_patcher PRIVATE cxx_std_20)
|
||||
|
||||
if(MINGW)
|
||||
target_link_options(nid_patcher PRIVATE -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()
|
||||
target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
add_compile_options(-fno-asynchronous-unwind-tables)
|
||||
endif()
|
||||
|
||||
|
|
@ -60,10 +46,6 @@ 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})
|
||||
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)
|
||||
|
|
@ -109,12 +91,9 @@ foreach(LIB_DIR ${LIBS_DIRS})
|
|||
endif()
|
||||
|
||||
if(LIB_NAME STREQUAL "libkernel")
|
||||
target_link_libraries(${LIB_NAME} PUBLIC libc)
|
||||
target_link_libraries(${LIB_NAME} PRIVATE libc)
|
||||
else()
|
||||
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)
|
||||
target_link_libraries(${LIB_NAME} PRIVATE libkernel libc)
|
||||
endif()
|
||||
if(WIN32 AND LIB_NAME STREQUAL "libSceNet")
|
||||
target_link_libraries(${LIB_NAME} PRIVATE ws2_32)
|
||||
|
|
@ -149,195 +128,25 @@ 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 $<TARGET_FILE:TracyClient> ${PATCHED_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${TRACY_CLIENT_COPIED}
|
||||
DEPENDS TracyClient
|
||||
COMMENT "Copying the Tracy client next to the patched libraries"
|
||||
)
|
||||
list(APPEND PATCHED_OUTPUTS ${TRACY_CLIENT_COPIED})
|
||||
endif()
|
||||
|
||||
add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS})
|
||||
|
||||
if(BUILD_TESTING)
|
||||
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:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp)
|
||||
target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
|
||||
target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20)
|
||||
if(MINGW)
|
||||
target_compile_options(nid_buffer_validation_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests)
|
||||
set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10)
|
||||
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $<TARGET_FILE:nid_patcher>)
|
||||
add_test(NAME nid_patcher_macho COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_macho_nids.py $<TARGET_FILE:nid_patcher>)
|
||||
add_test(NAME nid_patcher_native_paths COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $<TARGET_FILE:nid_patcher>)
|
||||
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 $<TARGET_FILE:nid_patcher>)
|
||||
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(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $<TARGET_FILE:nid_patcher>)
|
||||
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 $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
if(APPLE)
|
||||
add_subdirectory(tests/write_watch)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp)
|
||||
target_include_directories(windows_write_watch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00)
|
||||
if(MINGW)
|
||||
target_compile_options(windows_write_watch_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME windows_write_watch COMMAND windows_write_watch_tests)
|
||||
set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20)
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp)
|
||||
target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
|
||||
configure_windows_unwind(guest_offline_imports_tests)
|
||||
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_shared_strings COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_shared_strings.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
|
||||
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:$<TARGET_FILE_DIR:libSceSigninDialog>")
|
||||
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:$<TARGET_FILE_DIR:libSceNetCtlApDialog>")
|
||||
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:$<TARGET_FILE_DIR:libSceNpAuth>")
|
||||
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:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
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:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
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:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
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:$<TARGET_FILE_DIR:libSceSsl>")
|
||||
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:$<TARGET_FILE_DIR:libSceSystemGesture>")
|
||||
endif()
|
||||
|
||||
if(UNIX AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$" OR CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64"))
|
||||
add_subdirectory(tests/register_context)
|
||||
endif()
|
||||
|
||||
add_test_executable(savedata_memory_metadata_tests tests/SaveDataMemoryMetadata.cpp)
|
||||
target_include_directories(savedata_memory_metadata_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(savedata_memory_metadata_tests PRIVATE libSceSaveData.native)
|
||||
|
|
@ -375,11 +184,6 @@ 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=$<TARGET_FILE_DIR:libc> -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ThreadLocalDestructors.cmake)
|
||||
set_tests_properties(prx_thread_local_destructors PROPERTIES TIMEOUT 300)
|
||||
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})
|
||||
|
|
@ -399,16 +203,6 @@ 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:$<TARGET_FILE_DIR:libSceAmpr>")
|
||||
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)
|
||||
|
|
@ -420,44 +214,4 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(savedata_native_write_replacement PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSaveData.native>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_ppsa01530_exports_tests tests/GuestPPSA01530Exports.cpp)
|
||||
target_include_directories(guest_ppsa01530_exports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_ppsa01530_exports_tests PRIVATE libkernel libSceJson2 libSceNet libScePad libc)
|
||||
configure_windows_unwind(guest_ppsa01530_exports_tests)
|
||||
add_test(NAME guest_ppsa01530_exports COMMAND guest_ppsa01530_exports_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_ppsa01530_exports PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
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:$<TARGET_FILE_DIR:libScePosixForWebKit>")
|
||||
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:$<TARGET_FILE_DIR:libSceAcm>")
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
|||
|
|
@ -16,23 +16,6 @@ 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;
|
||||
|
|
@ -95,14 +78,6 @@ 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;
|
||||
|
|
@ -230,19 +205,6 @@ struct ModuleSegmentInfo {
|
|||
std::int32_t prot;
|
||||
};
|
||||
|
||||
struct ModuleInfo {
|
||||
std::uint64_t st_size;
|
||||
char name[256];
|
||||
ModuleSegmentInfo segments[4];
|
||||
std::uint32_t segment_count;
|
||||
std::uint8_t fingerprint[20];
|
||||
};
|
||||
static_assert(offsetof(ModuleInfo, name) == 0x8);
|
||||
static_assert(offsetof(ModuleInfo, segments) == 0x108);
|
||||
static_assert(offsetof(ModuleInfo, segment_count) == 0x148);
|
||||
static_assert(offsetof(ModuleInfo, fingerprint) == 0x14C);
|
||||
static_assert(sizeof(ModuleInfo) == 0x160);
|
||||
|
||||
struct ModuleInfoEx {
|
||||
std::uint64_t st_size;
|
||||
char name[256];
|
||||
|
|
@ -280,12 +242,6 @@ 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;
|
||||
|
|
@ -578,13 +534,6 @@ struct Audio3dOpenParameters {
|
|||
std::uint32_t num_beds;
|
||||
};
|
||||
|
||||
struct Audio3dAttribute {
|
||||
std::uint32_t attribute_id;
|
||||
std::uint32_t pad;
|
||||
const void* value;
|
||||
std::uint64_t value_size;
|
||||
};
|
||||
|
||||
using AudioPropagationHandle = std::uint64_t;
|
||||
|
||||
struct AudioPropagationStructDescriptor {
|
||||
|
|
@ -1265,16 +1214,9 @@ struct NetCtlNatInfo { std::uint8_t opaque[128]; };
|
|||
|
||||
union NetCtlInfo { std::uint8_t opaque[256]; };
|
||||
|
||||
struct NetCtlIfStat {
|
||||
std::uint32_t device;
|
||||
std::uint64_t tx_bytes;
|
||||
std::uint64_t rx_bytes;
|
||||
std::uint32_t reserved[8];
|
||||
};
|
||||
|
||||
using NetCtlCallback = void (*)(int, void*);
|
||||
|
||||
struct HttpEpoll;
|
||||
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*);
|
||||
|
|
@ -1316,18 +1258,6 @@ 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]; };
|
||||
|
|
@ -1461,12 +1391,7 @@ struct SaveDataMountInfo {
|
|||
|
||||
struct SceSaveDataTitleId { char data[10]; char pad[2]; };
|
||||
struct SceSaveDataDirName { char data[32]; };
|
||||
struct SaveDataSearchInfo {
|
||||
std::uint64_t blocks;
|
||||
std::uint64_t free_blocks;
|
||||
std::uint8_t reserved[32];
|
||||
};
|
||||
static_assert(sizeof(SaveDataSearchInfo) == 48);
|
||||
struct SaveDataSearchInfo { std::uint8_t opaque[128]; };
|
||||
struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; };
|
||||
|
||||
struct SaveDataMount3 {
|
||||
|
|
@ -1589,13 +1514,6 @@ 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 {
|
||||
|
|
@ -1615,50 +1533,8 @@ 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;
|
||||
|
|
@ -1951,19 +1827,6 @@ struct VideoOutOutputStatus {
|
|||
std::uint64_t reserved[3] = {};
|
||||
};
|
||||
|
||||
struct VideoOutResolutionStatus {
|
||||
std::uint32_t fullWidth = 0;
|
||||
std::uint32_t fullHeight = 0;
|
||||
std::uint32_t paneWidth = 0;
|
||||
std::uint32_t paneHeight = 0;
|
||||
std::uint64_t refreshRate = 0;
|
||||
float screenSizeInInch = 0.0f;
|
||||
std::uint16_t flags = 0;
|
||||
std::uint16_t reserved0 = 0;
|
||||
std::uint32_t reserved1[3] = {};
|
||||
};
|
||||
static_assert(sizeof(VideoOutResolutionStatus) == 48 && offsetof(VideoOutResolutionStatus, refreshRate) == 16 && offsetof(VideoOutResolutionStatus, screenSizeInInch) == 24);
|
||||
|
||||
struct VideoOutOutputOptions { std::uint32_t internalData[16] = {}; };
|
||||
|
||||
struct VideoOutColorSettings {
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
#ifndef CORE_LIBS_NID_MACHONIDPATCHER_HPP
|
||||
#define CORE_LIBS_NID_MACHONIDPATCHER_HPP
|
||||
|
||||
#include <nid/IBinaryPatcher.hpp>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<MachOExport> ReadMachOExports(const std::vector<std::uint8_t>& binary);
|
||||
|
||||
class MachONidPatcher final : public IBinaryPatcher {
|
||||
public:
|
||||
void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
#ifndef NID_NATIVEPATH_HPP
|
||||
#define NID_NATIVEPATH_HPP
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace Nid::Internal {
|
||||
|
||||
inline std::filesystem::path NativePath(const std::string& path) {
|
||||
return std::filesystem::path(std::u8string(path.begin(), path.end()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -61,7 +61,7 @@ inline std::string StripNidPostfix(const std::string& name) {
|
|||
|
||||
template<typename T>
|
||||
T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
|
||||
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("read out of bounds");
|
||||
if (offset + sizeof(T) > buf.size()) 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<std::uint8_t>& buf, std::size_t offset) {
|
|||
|
||||
template<typename T>
|
||||
void Write(std::vector<std::uint8_t>& buf, std::size_t offset, const T& v) {
|
||||
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("write out of bounds");
|
||||
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("write out of bounds");
|
||||
std::memcpy(buf.data() + offset, &v, sizeof(T));
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +78,6 @@ inline std::string ReadCStr(const std::vector<std::uint8_t>& buf, std::size_t of
|
|||
std::string s;
|
||||
while (offset < buf.size() && buf[offset] != 0u)
|
||||
s.push_back(static_cast<char>(buf[offset++]));
|
||||
if (offset == buf.size()) throw std::runtime_error("unterminated cstr");
|
||||
return s;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
#include <nid/BinaryPatcherFactory.hpp>
|
||||
#include <nid/ElfPatcher.hpp>
|
||||
#include <nid/MachONidPatcher.hpp>
|
||||
#include <nid/PeNidPatcher.hpp>
|
||||
#include <stdexcept>
|
||||
|
||||
|
|
@ -11,8 +10,6 @@ std::unique_ptr<IBinaryPatcher> MakePatcher(const std::vector<std::uint8_t>& bin
|
|||
return std::make_unique<ElfNidPatcher>();
|
||||
if (binary.size() >= 2 && binary[0] == 'M' && binary[1] == 'Z')
|
||||
return std::make_unique<PeNidPatcher>();
|
||||
if (binary.size() >= 4 && binary[0] == 0xcf && binary[1] == 0xfa && binary[2] == 0xed && binary[3] == 0xfe)
|
||||
return std::make_unique<MachONidPatcher>();
|
||||
throw std::runtime_error("unrecognized binary format");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,11 +15,6 @@ namespace Nid {
|
|||
namespace {
|
||||
|
||||
|
||||
void RequireElfRange(const std::vector<std::uint8_t>& 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)
|
||||
|
|
@ -41,18 +36,6 @@ GnuHashLayout ReadGnuHashLayout(const std::vector<std::uint8_t>& elf, std::size_
|
|||
return layout;
|
||||
}
|
||||
|
||||
void ValidateGnuHash(const std::vector<std::uint8_t>& 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<std::uint8_t>& elf,
|
||||
std::size_t dynSymOffset,
|
||||
|
|
@ -221,65 +204,49 @@ void RemapDynamicNeededOffsets(
|
|||
}
|
||||
}
|
||||
|
||||
struct VersionNameField {
|
||||
std::size_t Offset;
|
||||
std::uint32_t Name;
|
||||
};
|
||||
|
||||
std::vector<VersionNameField> ReadVersionNameFields(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
|
||||
using namespace Internal;
|
||||
std::vector<VersionNameField> fields;
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const auto section = Read<Elf64_Shdr>(elf, static_cast<std::size_t>(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<std::size_t>(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<Elf64_Verneed>(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<Elf64_Vernaux>(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<std::uint8_t>& elf,
|
||||
const Elf64_Ehdr& ehdr,
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>>& oldToNewOffset
|
||||
) {
|
||||
using namespace Internal;
|
||||
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);
|
||||
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type != kShtGnuVerneed) continue;
|
||||
|
||||
const std::size_t verneedOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
std::size_t entryOffset = verneedOffset;
|
||||
|
||||
while (true) {
|
||||
const auto verneed = Read<Elf64_Verneed>(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<Elf64_Vernaux>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -308,8 +275,35 @@ std::vector<std::uint32_t> CollectDynamicNameOffsets(const std::vector<std::uint
|
|||
}
|
||||
|
||||
std::vector<std::uint32_t> CollectVerneedNameOffsets(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
|
||||
using namespace Internal;
|
||||
|
||||
std::vector<std::uint32_t> offsets;
|
||||
for (const auto& field : ReadVersionNameFields(elf, ehdr)) offsets.push_back(field.Name);
|
||||
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type != kShtGnuVerneed) continue;
|
||||
|
||||
std::size_t entryOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
|
||||
while (true) {
|
||||
const auto verneed = Read<Elf64_Verneed>(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<Elf64_Vernaux>(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;
|
||||
}
|
||||
}
|
||||
|
||||
return offsets;
|
||||
}
|
||||
|
||||
|
|
@ -322,9 +316,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
if (elf[4] != 2) throw std::runtime_error("not ELF64");
|
||||
|
||||
const auto ehdr = Read<Elf64_Ehdr>(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;
|
||||
|
|
@ -335,9 +326,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(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<std::size_t>(shdr.sh_offset);
|
||||
dynSymSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
dynSymSectionIndex = i;
|
||||
|
|
@ -345,6 +333,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
if (shdr.sh_type == kShtGnuHash) {
|
||||
gnuHashOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
gnuHashSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
|
||||
}
|
||||
if (shdr.sh_type == kShtGnuVersym)
|
||||
versymOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
|
|
@ -356,10 +345,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
|
||||
const std::size_t symCount = dynSymSize / sizeof(Elf64_Sym);
|
||||
if (symCount == 0u) throw std::runtime_error(".dynsym is empty");
|
||||
if (gnuHashOffset != 0u) {
|
||||
ValidateGnuHash(elf, gnuHashOffset, gnuHashSize, symCount);
|
||||
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
|
||||
}
|
||||
|
||||
const std::size_t dynStrSectionLink = [&]() -> std::size_t {
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
|
|
@ -374,8 +359,6 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
{
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + dynStrSectionLink * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(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<std::size_t>(shdr.sh_offset);
|
||||
dynStrSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
}
|
||||
|
|
@ -444,7 +427,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& 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 && entry.second == value; }
|
||||
[&](const auto& entry) { return entry.first == oldOffset; }
|
||||
);
|
||||
if (existing == names.end()) names.emplace_back(oldOffset, std::move(value));
|
||||
};
|
||||
|
|
@ -480,9 +463,8 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
newDynStr.push_back(0u);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> preservedToNewOffset;
|
||||
for (const auto offset : preservedOffsets)
|
||||
preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset)));
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> oldToNewOffset;
|
||||
for (const auto& name : names) oldToNewOffset.emplace_back(name.first, valueOffsets.at(name.second));
|
||||
|
||||
if (newDynStr.size() > dynStrSize) {
|
||||
throw std::runtime_error(
|
||||
|
|
@ -493,15 +475,25 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
|||
newDynStr.resize(dynStrSize, 0u);
|
||||
|
||||
std::vector<std::string> newSymbolNames(symCount);
|
||||
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));
|
||||
for (std::size_t i = 1u; i < symCount; ++i) {
|
||||
const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym);
|
||||
const auto sym = Read<Elf64_Sym>(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);
|
||||
}
|
||||
|
||||
std::memcpy(elf.data() + dynStrOffset, newDynStr.data(), newDynStr.size());
|
||||
|
||||
RemapDynamicNeededOffsets(elf, ehdr, preservedToNewOffset);
|
||||
RemapVerneedOffsets(elf, ehdr, preservedToNewOffset);
|
||||
RemapDynamicNeededOffsets(elf, ehdr, oldToNewOffset);
|
||||
RemapVerneedOffsets(elf, ehdr, oldToNewOffset);
|
||||
|
||||
if (gnuHashOffset != 0u) {
|
||||
const auto bucketCount = Read<std::uint32_t>(elf, gnuHashOffset);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
#include <nid/ExportExclusions.hpp>
|
||||
#include <nid/ElfPatcher.hpp>
|
||||
#include <nid/PeNidPatcher.hpp>
|
||||
#include <nid/MachONidPatcher.hpp>
|
||||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <nid/NativePath.hpp>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
|
|
@ -50,12 +48,7 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& 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<std::uint32_t>(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<PeSectionHeader>(binary, sections + std::uint64_t{index} * sizeof(PeSectionHeader));
|
||||
if (rva < section.VirtualAddress) continue;
|
||||
|
|
@ -70,20 +63,14 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
|
|||
const auto table = ReadValue<PeExportDirectory>(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<std::string> result;
|
||||
for (std::uint32_t index = 0; index < table.NumberOfNames; ++index) {
|
||||
const auto ordinal = ReadValue<std::uint16_t>(binary, ordinals + std::uint64_t{index} * 2);
|
||||
if (ordinal >= table.NumberOfFunctions) throw std::runtime_error("invalid reference PE export ordinal");
|
||||
if (!ReadValue<std::uint32_t>(binary, functions + std::uint64_t{ordinal} * 4)) continue;
|
||||
const auto rva = ReadValue<std::uint32_t>(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;
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +109,7 @@ std::string NormalizeExportName(const std::string& name) {
|
|||
|
||||
std::unordered_set<std::string> 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(Internal::NativePath(path), std::ios::binary | std::ios::ate);
|
||||
std::ifstream input(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);
|
||||
|
|
@ -134,12 +121,6 @@ std::unordered_set<std::string> 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,493 +0,0 @@
|
|||
#include <nid/MachONidPatcher.hpp>
|
||||
#include <nid/NidResolver.hpp>
|
||||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
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<std::uint8_t>& 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<std::uint8_t>& 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<std::uint8_t>& 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<std::uint8_t>& 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<const char*>(data.data() + start), cursor - start);
|
||||
++cursor;
|
||||
return text;
|
||||
}
|
||||
|
||||
struct Image {
|
||||
MachHeader64 Header {};
|
||||
std::vector<std::size_t> Commands;
|
||||
};
|
||||
|
||||
Image ParseImage(const std::vector<std::uint8_t>& binary) {
|
||||
Image image;
|
||||
image.Header = Read<MachHeader64>(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<LoadCommand>(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<TrieLocation> FindTrie(const std::vector<std::uint8_t>& binary, const Image& image) {
|
||||
std::optional<TrieLocation> found;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(binary, offset).Cmd;
|
||||
std::optional<TrieLocation> here;
|
||||
if (cmd == kLcDyldExportsTrie) here = TrieLocation{offset + offsetof(LinkeditDataCommand, DataOffset), offset + offsetof(LinkeditDataCommand, DataSize)};
|
||||
else if (cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly) {
|
||||
if (Read<DyldInfoCommand>(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<std::uint8_t>& data, std::size_t start, std::size_t end, std::size_t node, std::string& prefix, std::set<std::size_t>& visited, std::vector<MachOExport>& 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<std::size_t>(child), prefix, visited, out);
|
||||
prefix.resize(length);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<MachOExport> ParseTrie(const std::vector<std::uint8_t>& binary, std::uint32_t offset, std::uint32_t size) {
|
||||
RequireRange(binary, offset, size);
|
||||
std::vector<MachOExport> exports;
|
||||
if (size == 0) return exports;
|
||||
std::string prefix;
|
||||
std::set<std::size_t> visited;
|
||||
ParseTrieNode(binary, offset, std::size_t{offset} + size, 0, prefix, visited, exports);
|
||||
return exports;
|
||||
}
|
||||
|
||||
class TrieBuilder {
|
||||
public:
|
||||
explicit TrieBuilder(const std::vector<MachOExport>& exports) {
|
||||
for (const auto& entry : exports) Insert(entry);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Build() {
|
||||
std::vector<Node*> 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<std::uint8_t> 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<std::uint8_t>(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<std::pair<std::string, std::unique_ptr<Node>>> Children;
|
||||
std::optional<MachOExport> 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<Node>();
|
||||
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 = 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<Node*>& order) {
|
||||
order.push_back(node);
|
||||
for (auto& [label, child] : node->Children) Collect(child.get(), order);
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> TerminalBytes(const Node& node) {
|
||||
std::vector<std::uint8_t> 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<unsigned char>(name[0])) == 's' &&
|
||||
std::tolower(static_cast<unsigned char>(name[1])) == 'c' &&
|
||||
std::tolower(static_cast<unsigned char>(name[2])) == 'e';
|
||||
return hasNidPostfix || hasScePrefix;
|
||||
}
|
||||
|
||||
std::vector<bool> DylibOrdinalsArePrx(const std::vector<std::uint8_t>& binary, const Image& image) {
|
||||
std::vector<bool> result;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto command = Read<LoadCommand>(binary, offset);
|
||||
if (command.Cmd != kLcLoadDylib && command.Cmd != kLcLoadWeakDylib && command.Cmd != kLcReexportDylib && command.Cmd != kLcLazyLoadDylib && command.Cmd != kLcLoadUpwardDylib) continue;
|
||||
const auto nameOffset = Read<std::uint32_t>(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<std::uint8_t>& binary, const Image& image) {
|
||||
std::optional<LinkeditDataCommand> fixups;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(binary, offset).Cmd;
|
||||
if (cmd == kLcDyldChainedFixups) fixups = Read<LinkeditDataCommand>(binary, offset);
|
||||
if ((cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly)) {
|
||||
const auto info = Read<DyldInfoCommand>(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<ChainedFixupsHeader>(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<Import> 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<std::uint64_t>(binary, at);
|
||||
imports.push_back({static_cast<std::uint32_t>(raw >> 32), static_cast<std::uint32_t>(raw & 0xffff)});
|
||||
} else {
|
||||
const auto raw = Read<std::uint32_t>(binary, at);
|
||||
imports.push_back({raw >> 9, raw & 0xff});
|
||||
}
|
||||
}
|
||||
std::set<std::uint32_t> nameOffsets;
|
||||
for (const auto& item : imports) nameOffsets.insert(item.NameOffset);
|
||||
std::set<std::uint32_t> 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<std::ptrdiff_t>(at + patched.size()), binary.begin() + static_cast<std::ptrdiff_t>(at + name.size() + 1), 0);
|
||||
rewritten.insert(item.NameOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteTrie(std::vector<std::uint8_t>& binary, const Image& image, const TrieLocation& location, const std::vector<std::uint8_t>& trie) {
|
||||
const auto oldOffset = Read<std::uint32_t>(binary, location.OffsetField);
|
||||
const auto oldSize = Read<std::uint32_t>(binary, location.SizeField);
|
||||
if (trie.size() <= oldSize) {
|
||||
std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset);
|
||||
std::fill(binary.begin() + oldOffset + static_cast<std::ptrdiff_t>(trie.size()), binary.begin() + oldOffset + oldSize, 0);
|
||||
Write(binary, location.SizeField, static_cast<std::uint32_t>(trie.size()));
|
||||
return;
|
||||
}
|
||||
std::optional<std::size_t> linkedit;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(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<SegmentCommand64>(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<SegmentCommand64>(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<std::uint32_t>(newOffset));
|
||||
Write(binary, location.SizeField, static_cast<std::uint32_t>(trie.size()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::vector<MachOExport> ReadMachOExports(const std::vector<std::uint8_t>& binary) {
|
||||
const auto image = ParseImage(binary);
|
||||
const auto location = FindTrie(binary, image);
|
||||
if (!location) return {};
|
||||
return ParseTrie(binary, Read<std::uint32_t>(binary, location->OffsetField), Read<std::uint32_t>(binary, location->SizeField));
|
||||
}
|
||||
|
||||
void MachONidPatcher::PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& 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<std::uint32_t>(binary, location->OffsetField), Read<std::uint32_t>(binary, location->SizeField));
|
||||
if (exports.empty()) throw std::runtime_error("no exported names");
|
||||
|
||||
std::vector<std::string> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
#include <nid/BinaryPatcherFactory.hpp>
|
||||
#include <nid/ExportExclusions.hpp>
|
||||
#include <nid/NativePath.hpp>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
|
@ -10,20 +9,20 @@
|
|||
namespace {
|
||||
|
||||
std::vector<std::uint8_t> _readFile(const std::string& path) {
|
||||
std::ifstream f(Nid::Internal::NativePath(path), std::ios::binary);
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
if (!f) throw std::runtime_error("cannot open: " + path);
|
||||
return {std::istreambuf_iterator<char>(f), {}};
|
||||
}
|
||||
|
||||
void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data) {
|
||||
std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary);
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
if (!f) throw std::runtime_error("cannot write: " + path);
|
||||
f.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int Run(int argc, char* argv[]) {
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library>] <file> [file2 ...]\n";
|
||||
return 1;
|
||||
|
|
@ -66,27 +65,3 @@ int Run(int argc, char* argv[]) {
|
|||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t* argv[]) {
|
||||
try {
|
||||
std::vector<std::string> values;
|
||||
values.reserve(static_cast<std::size_t>(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<char*> 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
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition) {
|
||||
if (!condition) throw std::runtime_error("NID buffer validation failed");
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
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<std::uint8_t> unterminated{'n', 'a', 'm', 'e'};
|
||||
RequireRejected([&] { ReadCStr(unterminated, 0); });
|
||||
RequireRejected([&] { ReadCStr(unterminated, 3); });
|
||||
const std::vector<std::uint8_t> 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<std::uint8_t> bytes(8, 0xa5);
|
||||
const std::uint64_t value = 0x1020304050607080;
|
||||
Write(bytes, 0, value);
|
||||
Require(Read<std::uint64_t>(bytes, 0) == value);
|
||||
Write<std::uint32_t>(bytes, 4, 0x12345678);
|
||||
Require(Read<std::uint32_t>(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<std::size_t>::max(), std::numeric_limits<std::size_t>::max() - 1,
|
||||
std::numeric_limits<std::size_t>::max() - 3}) {
|
||||
RequireRejected([&] { Read<std::uint32_t>(bytes, offset); });
|
||||
RequireRejected([&] { Write<std::uint32_t>(bytes, offset, 0); });
|
||||
Require(bytes == before);
|
||||
}
|
||||
RequireRejected([&] { Read<std::uint64_t>(bytes, 1); });
|
||||
std::vector<std::uint8_t> empty;
|
||||
RequireRejected([&] { Read<std::uint8_t>(empty, 0); });
|
||||
RequireRejected([&] { Write<std::uint8_t>(empty, 0, 0); });
|
||||
std::cout << "NID buffer validation passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
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()
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash
|
||||
|
||||
|
||||
class GnuHashTests(unittest.TestCase):
|
||||
def image(self, buckets=4, bloom=1, shift=6, symoffset=3):
|
||||
image = build_image(symbols(False), False)
|
||||
count = len(symbols(False))
|
||||
struct.pack_into("<IIII", image, HASH_OFFSET, buckets, symoffset, bloom, shift)
|
||||
size = 16 + 8 * bloom + 4 * buckets + 4 * max(0, count - symoffset)
|
||||
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
|
||||
return image
|
||||
|
||||
def test_non_power_of_two_bloom_is_rejected(self):
|
||||
self.reject(self.image(bloom=3))
|
||||
|
||||
def test_hash_shift_outside_word_is_rejected(self):
|
||||
self.reject(self.image(shift=32))
|
||||
|
||||
def test_unsafe_geometry_is_rejected(self):
|
||||
for fields in ({"bloom": 0}, {"buckets": 0}, {"symoffset": 100}):
|
||||
with self.subTest(fields=fields):
|
||||
self.reject(self.image(**fields))
|
||||
|
||||
def test_declared_section_extent_is_checked(self):
|
||||
for size in (12, 16, 0x10000):
|
||||
with self.subTest(size=size):
|
||||
image = self.image()
|
||||
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
|
||||
self.reject(image)
|
||||
|
||||
def reject(self, image):
|
||||
result, after = patch(PATCHER, bytes(image), "geometry.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertIn(b"gnu.hash", result.stderr)
|
||||
self.assertEqual(after, image)
|
||||
|
||||
def test_rebuilt_hash_finds_every_export(self):
|
||||
image = self.image(bloom=2)
|
||||
result, after = patch(PATCHER, image, "valid.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
entries = read_dynsym(after)
|
||||
buckets, first, bloom, shift = struct.unpack_from("<4I", after, HASH_OFFSET)
|
||||
bucket_offset = HASH_OFFSET + 16 + bloom * 8
|
||||
chain_offset = bucket_offset + buckets * 4
|
||||
for index in range(first, len(entries)):
|
||||
name = entries[index][0]
|
||||
hashed = gnu_hash(name)
|
||||
word = struct.unpack_from("<Q", after, HASH_OFFSET + 16 + ((hashed // 64) % bloom) * 8)[0]
|
||||
self.assertTrue(word & (1 << (hashed % 64)))
|
||||
self.assertTrue(word & (1 << ((hashed >> shift) % 64)))
|
||||
start = struct.unpack_from("<I", after, bucket_offset + (hashed % buckets) * 4)[0]
|
||||
found = False
|
||||
for candidate in range(start, len(entries)):
|
||||
value = struct.unpack_from("<I", after, chain_offset + (candidate - first) * 4)[0]
|
||||
if (value | 1) == (hashed | 1) and entries[candidate][0] == name:
|
||||
found = True
|
||||
if value & 1:
|
||||
break
|
||||
self.assertTrue(found, name)
|
||||
|
||||
def test_empty_hash_with_no_buckets_is_supported(self):
|
||||
image = self.image(buckets=0, symoffset=len(symbols(False)))
|
||||
result, after = patch(PATCHER, image, "empty.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(struct.unpack_from("<I", after, HASH_OFFSET)[0], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -1,235 +0,0 @@
|
|||
"""Check nid_patcher on small byte-built Mach-O dylibs: the export trie, the chained imports and what it refuses."""
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1, 0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30])
|
||||
BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-"
|
||||
|
||||
LC_SEGMENT_64 = 0x19
|
||||
LC_LOAD_DYLIB = 0x0C
|
||||
LC_CODE_SIGNATURE = 0x1D
|
||||
LC_DYLD_INFO_ONLY = 0x80000022
|
||||
LC_DYLD_EXPORTS_TRIE = 0x80000033
|
||||
LC_DYLD_CHAINED_FIXUPS = 0x80000034
|
||||
EXPORT_WEAK = 0x04
|
||||
PAGE = 0x1000
|
||||
|
||||
|
||||
def nid(name):
|
||||
digest = hashlib.sha1(name.encode() + SUFFIX).digest()
|
||||
reversed_bytes = digest[:8][::-1]
|
||||
text = ""
|
||||
for index in (0, 3):
|
||||
triple = (reversed_bytes[index] << 16) | (reversed_bytes[index + 1] << 8) | reversed_bytes[index + 2]
|
||||
text += BASE64[(triple >> 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("<I", 0) + b"".join(struct.pack("<I", entry) for entry in entries) + bytes(symbols)
|
||||
return blob + bytes(-len(blob) % 8)
|
||||
|
||||
|
||||
def dylib_command(path):
|
||||
name = path.encode() + b"\0"
|
||||
size = (24 + len(name) + 7) & ~7
|
||||
return struct.pack("<IIIIII", LC_LOAD_DYLIB, size, 24, 2, 0x10000, 0x10000) + name.ljust(size - 24, b"\0")
|
||||
|
||||
|
||||
def build_dylib(exports, imports, signed=False, bind_opcodes=False):
|
||||
trie = build_trie(exports)
|
||||
fixups = build_fixups(imports)
|
||||
linkedit = bytearray(trie + fixups)
|
||||
trie_command = (struct.pack("<IIII", LC_DYLD_EXPORTS_TRIE, 16, PAGE, len(trie)))
|
||||
fixups_command = struct.pack("<IIII", LC_DYLD_CHAINED_FIXUPS, 16, PAGE + len(trie), len(fixups))
|
||||
if bind_opcodes:
|
||||
trie_command = struct.pack("<IIIIIIIIIIII", LC_DYLD_INFO_ONLY, 48, 0, 0, PAGE, 8, 0, 0, 0, 0, PAGE, len(trie))
|
||||
extra = [trie_command, fixups_command, dylib_command("@rpath/libkernel.prx"), dylib_command("/usr/lib/libSystem.B.dylib")]
|
||||
if signed:
|
||||
extra.append(struct.pack("<IIII", LC_CODE_SIGNATURE, 16, PAGE + len(linkedit), 0))
|
||||
text = struct.pack("<II16sQQQQiiII", LC_SEGMENT_64, 72, b"__TEXT", 0, PAGE, 0, PAGE, 5, 5, 0, 0)
|
||||
link = struct.pack("<II16sQQQQiiII", LC_SEGMENT_64, 72, b"__LINKEDIT", PAGE, PAGE, PAGE, len(linkedit), 1, 1, 0, 0)
|
||||
body = text + link + b"".join(extra)
|
||||
header = struct.pack("<IIIIIIII", 0xFEEDFACF, 0x01000007, 3, 6, 2 + len(extra), len(body), 0, 0)
|
||||
image = bytearray(header + body)
|
||||
image += bytes(PAGE - len(image))
|
||||
image += linkedit
|
||||
return bytes(image)
|
||||
|
||||
|
||||
def command_fields(data):
|
||||
offset, found = 32, {}
|
||||
for _ in range(struct.unpack_from("<I", data, 16)[0]):
|
||||
cmd, size = struct.unpack_from("<II", data, offset)
|
||||
found.setdefault(cmd, []).append(offset)
|
||||
offset += size
|
||||
return found
|
||||
|
||||
|
||||
def segment(data, name):
|
||||
for offset in command_fields(data)[LC_SEGMENT_64]:
|
||||
fields = struct.unpack_from("<16sQQQQ", data, offset + 8)
|
||||
if fields[0].rstrip(b"\0").decode() == name:
|
||||
return offset, fields[1:]
|
||||
raise AssertionError(name)
|
||||
|
||||
|
||||
def chained_imports(data):
|
||||
offset = command_fields(data)[LC_DYLD_CHAINED_FIXUPS][0]
|
||||
base, size = struct.unpack_from("<II", data, offset + 8)
|
||||
_, _, imports_offset, symbols_offset, count, _, _ = struct.unpack_from("<7I", data, base)
|
||||
result = []
|
||||
for index in range(count):
|
||||
raw, = struct.unpack_from("<I", data, base + imports_offset + 4 * index)
|
||||
start = base + symbols_offset + (raw >> 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("<II", data, offset + 8)
|
||||
return parse_trie(data, start, size)
|
||||
|
||||
|
||||
def check_shrinking(patcher, directory):
|
||||
exports = [("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108), ("_weak_inline", EXPORT_WEAK, 0x110)]
|
||||
imports = [("_malloc", 2), ("_puts_nid_postfix", 1), ("_sceKernelFoo", 1), ("_helper", 1), ("_exit_nid_postfix", 2)]
|
||||
result, path = patch(patcher, directory, "shrink.dylib", build_dylib(exports, imports))
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
data = path.read_bytes()
|
||||
assert exports_of(data) == {"_" + nid("puts"): (0, 0x100), "_" + nid("exit"): (0, 0x108), "_weak_inline": (EXPORT_WEAK, 0x110)}, exports_of(data)
|
||||
assert chained_imports(data) == [("_malloc", 2), ("_" + nid("puts"), 1), ("_" + nid("sceKernelFoo"), 1), ("_helper", 1), ("_exit_nid_postfix", 2)], chained_imports(data)
|
||||
assert len(data) == len(build_dylib(exports, imports)), "the image changed size"
|
||||
|
||||
|
||||
def check_growing(patcher, directory):
|
||||
original = build_dylib([("_ab", 0, 0x100), ("_cd", 0, 0x108)], [])
|
||||
result, path = patch(patcher, directory, "grow.dylib", original)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
data = path.read_bytes()
|
||||
assert exports_of(data) == {"_" + nid("ab"): (0, 0x100), "_" + nid("cd"): (0, 0x108)}, exports_of(data)
|
||||
offset, (vmaddr, vmsize, fileoff, filesize) = segment(data, "__LINKEDIT")
|
||||
assert fileoff + filesize == len(data) and vmsize >= filesize and len(data) > len(original), (fileoff, filesize, len(data))
|
||||
trie_offset, trie_size = struct.unpack_from("<II", data, command_fields(data)[LC_DYLD_EXPORTS_TRIE][0] + 8)
|
||||
assert trie_offset >= 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 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_refusals(patcher, directory)
|
||||
print("NID patcher Mach-O tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_macho_nids import build_dylib, exports_of, nid
|
||||
|
||||
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")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
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("<I", image, 0x3c, 0x40)
|
||||
struct.pack_into("<IHHIIIHH", image, 0x40, 0x4550, 0x8664, 2, 0, 0, 0, 240, 0x2022)
|
||||
optional = 0x58
|
||||
struct.pack_into("<H", image, optional, 0x20b)
|
||||
struct.pack_into("<I", image, optional + 60, 512)
|
||||
struct.pack_into("<I", image, optional + 108, 16)
|
||||
start, rva = (416, 416) if headers else (512, 0x1000)
|
||||
struct.pack_into("<II", image, optional + 112, rva, 80)
|
||||
for index, (name, va, offset, size) in enumerate(((b".edata", 0x1000, 512, 256), (b".text", 0x2000, 768, 512))):
|
||||
struct.pack_into("<8sIIIIIIHHI", image, optional + 240 + index * 40,
|
||||
name, size, va, size, offset, 0, 0, 0, 0, 0x40000040)
|
||||
struct.pack_into("<IIHHIIIIIII", image, start, 0, 0, 0, 0, rva + 50, 1, 1, 1, rva + 40, rva + 44, rva + 48)
|
||||
struct.pack_into("<IIH", image, start + 40, address, rva + 50, ordinal)
|
||||
name = b"puts_nid_postfix\0"
|
||||
image[start + 50:start + 50 + len(name)] = name
|
||||
return image
|
||||
|
||||
|
||||
class ReferenceTests(unittest.TestCase):
|
||||
def patch_with_reference(self, data):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
source, ref = root / "target.dylib", root / "reference.bin"
|
||||
original = build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])
|
||||
source.write_bytes(original)
|
||||
ref.write_bytes(data)
|
||||
result = subprocess.run([str(PATCHER), "libc", str(source), "--preserve-exports", str(ref)], capture_output=True, text=True)
|
||||
self.assertEqual(ref.read_bytes(), data)
|
||||
return result, source.read_bytes(), original
|
||||
|
||||
def test_header_resident_export_tables(self):
|
||||
result, image, _ = self.patch_with_reference(reference(headers=True))
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(exports_of(image), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
|
||||
|
||||
def test_section_resident_export_tables(self):
|
||||
result, image, _ = self.patch_with_reference(reference())
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("_puts_nid_postfix", exports_of(image))
|
||||
|
||||
def test_null_function_slot_does_not_preserve_a_name(self):
|
||||
result, image, original = self.patch_with_reference(reference(address=0))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("no named exports", result.stdout + result.stderr)
|
||||
self.assertEqual(image, original)
|
||||
|
||||
def test_invalid_name_ordinal_is_rejected_without_changing_target(self):
|
||||
result, image, original = self.patch_with_reference(reference(ordinal=1))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("ordinal", result.stdout + result.stderr)
|
||||
self.assertEqual(image, original)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSYM_OFFSET, read_dynsym, sections, DEFINITIONS
|
||||
|
||||
|
||||
class SharedStringTests(unittest.TestCase):
|
||||
def test_dynamic_names_keep_original_string_when_export_shares_offset(self):
|
||||
for tag in (1, 14, 15, 29):
|
||||
with self.subTest(tag=tag):
|
||||
data = build_image(symbols(False), False)
|
||||
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 6, 2, 0, 0x580, 32, 2, 0, 8, 16)
|
||||
struct.pack_into("<qQqQ", data, 0x580, tag, name_offset, 0, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "shared-dynamic.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
new_offset = struct.unpack_from("<Q", after, 0x588)[0]
|
||||
start = strings[4] + new_offset
|
||||
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
|
||||
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
|
||||
self.assertNotEqual(renamed, DEFINITIONS[0][0])
|
||||
|
||||
def test_version_requirement_strings_keep_original_names(self):
|
||||
data = build_image(symbols(False), False)
|
||||
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, 0x580, 32, 2, 1, 8, 0)
|
||||
struct.pack_into("<HHIII", data, 0x580, 1, 1, name_offset, 16, 0)
|
||||
struct.pack_into("<IHHII", data, 0x590, 0, 0, 2, name_offset, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "shared-version.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for field in (0x584, 0x598):
|
||||
offset = struct.unpack_from("<I", after, field)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
|
||||
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
|
||||
self.assertNotEqual(renamed, DEFINITIONS[0][0])
|
||||
|
||||
def test_local_symbol_keeps_name_shared_with_renamed_global(self):
|
||||
entries = symbols(False)
|
||||
entries[1] = (DEFINITIONS[0][0], 1, 9, 2, 0x1500)
|
||||
data = build_image(entries, False)
|
||||
data[DYNSYM_OFFSET + 24 + 4] = 2
|
||||
result, after = patch(PATCHER, bytes(data), "shared-local.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
names = {value: name for name, _, value in read_dynsym(after)}
|
||||
self.assertEqual(names[0x1500], DEFINITIONS[0][0])
|
||||
self.assertNotEqual(names[DEFINITIONS[0][2]], DEFINITIONS[0][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections
|
||||
|
||||
VERNEED = 0x580
|
||||
|
||||
|
||||
def image(*, auxiliary=16, count=1, size=32, version=1, auxnext=0, nextrecord=0):
|
||||
data = build_image(symbols(False), False)
|
||||
strings = sections(data)[2]
|
||||
filename = strings[5]
|
||||
names = b"libfixture.so\0FIXTURE_1\0"
|
||||
data[DYNSTR_OFFSET + filename:DYNSTR_OFFSET + filename + len(names)] = names
|
||||
struct.pack_into("<Q", data, SHOFF + 2 * 64 + 32, filename + len(names))
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, VERNEED, size, 2, 1, 8, 0)
|
||||
struct.pack_into("<HHIII", data, VERNEED, version, count, filename, auxiliary, nextrecord)
|
||||
struct.pack_into("<IHHII", data, VERNEED + auxiliary, 0, 0, 2, filename + len(b"libfixture.so\0"), auxnext)
|
||||
return data
|
||||
|
||||
|
||||
class VersionRequirementsTests(unittest.TestCase):
|
||||
def test_auxiliary_record_cannot_leave_declared_section(self):
|
||||
self.reject(image(auxiliary=32))
|
||||
|
||||
def test_auxiliary_count_must_match_chain(self):
|
||||
self.reject(image(count=2))
|
||||
|
||||
def test_unsupported_structure_version_is_rejected(self):
|
||||
self.reject(image(version=2))
|
||||
|
||||
def test_invalid_forward_links_and_section_ranges_are_rejected(self):
|
||||
for fields in ({"auxnext": 16}, {"nextrecord": 64}, {"size": 15},
|
||||
{"size": 0x10000}, {"auxiliary": 8}, {"count": 2, "auxnext": 8}):
|
||||
with self.subTest(fields=fields):
|
||||
self.reject(image(**fields))
|
||||
|
||||
def test_valid_multiple_auxiliaries_and_requirements(self):
|
||||
data = image(count=2, size=80, auxnext=16, nextrecord=48)
|
||||
original_name = struct.unpack_from("<I", data, VERNEED + 24)[0]
|
||||
original_file = struct.unpack_from("<I", data, VERNEED + 4)[0]
|
||||
struct.pack_into("<IHHII", data, VERNEED + 32, 0, 0, 3, original_name, 0)
|
||||
struct.pack_into("<HHIII", data, VERNEED + 48, 1, 1, original_file, 16, 0)
|
||||
struct.pack_into("<IHHII", data, VERNEED + 64, 0, 0, 4, original_name, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "multiple-versions.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for position, expected in ((4, b"libfixture.so"), (24, b"FIXTURE_1"),
|
||||
(40, b"FIXTURE_1"), (52, b"libfixture.so"), (72, b"FIXTURE_1")):
|
||||
offset = struct.unpack_from("<I", after, VERNEED + position)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)], expected)
|
||||
|
||||
def reject(self, data):
|
||||
result, after = patch(PATCHER, bytes(data), "versions.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertIn(b"version_r", result.stderr)
|
||||
self.assertEqual(after, data)
|
||||
|
||||
def test_valid_names_are_preserved_when_strings_are_rebuilt(self):
|
||||
data = image()
|
||||
result, after = patch(PATCHER, bytes(data), "valid-versions.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for position, expected in ((VERNEED + 4, b"libfixture.so"), (VERNEED + 16 + 8, b"FIXTURE_1")):
|
||||
offset = struct.unpack_from("<I", after, position)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)], expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -53,15 +53,12 @@ int APS5_VABI sceAcm_ConvReverb_SharedInput(void) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcm_FFT(void) {
|
||||
int APS5_VABI sceAcm_FFT() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcm_Panner(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcmBatchJobPriority(void) {
|
||||
int APS5_VABI sceAcm_Panner() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,34 +4,17 @@
|
|||
#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 <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) {
|
||||
if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
|
||||
NotImplemented_nid_no_patch(function);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) {
|
||||
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<std::uint32_t>(compareData),
|
||||
static_cast<std::uint32_t>(compareData >> 32u), static_cast<std::uint32_t>(extraData),
|
||||
static_cast<std::uint32_t>(extraData >> 32u)}, __func__);
|
||||
// unknown signature
|
||||
void* APS5_VABI sceAgcAcbAtomicGds_0900(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbJump(CommandBuffer* buf, std::uint8_t cachePolicy, const std::uint32_t* target, std::uint32_t sizeInDwords) {
|
||||
|
|
@ -63,12 +46,11 @@ std::uint32_t APS5_VABI sceAgcAcbRewindGetSize() {
|
|||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t videoOutHandle, std::uint32_t displayBufferIndex) {
|
||||
auto* packet = Agc::Command::Allocate(buf, AgcDriver::RenderingWaitPacketWords, __func__);
|
||||
packet[0] = AgcDriver::RenderingWaitPacketHeader;
|
||||
packet[1] = videoOutHandle;
|
||||
packet[2] = displayBufferIndex;
|
||||
packet[3] = 0;
|
||||
return packet;
|
||||
(void)buf;
|
||||
(void)videoOutHandle;
|
||||
(void)displayBufferIndex;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t videoOutHandle, std::int32_t displayBufferIndex, std::uint32_t flipMode, std::int64_t flipArg) {
|
||||
|
|
@ -81,21 +63,6 @@ std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t vide
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) {
|
||||
(void)workloadIds;
|
||||
(void)workloadCount;
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) {
|
||||
(void)workloadId;
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) {
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) {
|
||||
(void)color;
|
||||
return Agc::Marker::Push(buf, str, __func__);
|
||||
|
|
|
|||
|
|
@ -29,11 +29,9 @@ 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
|
||||
|
|
@ -48,7 +46,6 @@ 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
|
||||
|
|
@ -70,7 +67,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 libSceAgcDriver libc)
|
||||
target_link_libraries(agc_command_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_command_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_command_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
|
|
@ -96,19 +93,6 @@ 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:$<TARGET_FILE_DIR:libc>")
|
||||
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)
|
||||
|
|
@ -225,7 +209,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 ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR})
|
||||
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_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)
|
||||
|
|
@ -306,22 +290,6 @@ 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:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_dma_data_tests EXCLUDE_FROM_ALL
|
||||
tests/DmaData.cpp
|
||||
Command/src/Packet.cpp
|
||||
|
|
|
|||
|
|
@ -85,7 +85,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 = count == 1 ? 0xffff1000u : Header(0x10u, count);
|
||||
const auto header = Header(0x10u, count);
|
||||
auto* packet = Allocate(buffer, count, function);
|
||||
packet[0] = header;
|
||||
std::fill_n(packet + 1, count - 1u, 0u);
|
||||
|
|
|
|||
|
|
@ -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<std::uintptr_t>(indirect_base_addr);
|
||||
if (address != 0) Agc::Command::CheckAddress(address, 8, __func__);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@
|
|||
extern "C" {
|
||||
|
||||
// unknown signature
|
||||
void* APS5_VABI sceAgcDcbAtomicGds_0900(void) {
|
||||
APS5_EXPORT("zARR5aCmkoY", sceAgcDcbA_zARR5aCmkoY);
|
||||
void* APS5_VABI sceAgcDcbA_zARR5aCmkoY(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
}
|
||||
|
|
@ -34,7 +35,8 @@ std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t o
|
|||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command) {
|
||||
return Agc::Command::Emit(buf, 0x12u, {command & 0xfu}, __func__);
|
||||
Agc::Command::CheckBits(command, 0xfu, __func__);
|
||||
return Agc::Command::Emit(buf, 0x12u, {command}, __func__);
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbRewind(CommandBuffer* buf, uint32_t initial_state) {
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ 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<std::uint32_t>(guestAddress), static_cast<std::uint32_t>(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__);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,6 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
static constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80890001);
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) {
|
||||
|
|
@ -18,7 +16,6 @@ int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) {
|
|||
}
|
||||
|
||||
int APS5_VABI sceAgcInit(std::uint32_t version) {
|
||||
if (version >= 14) return SCE_AGC_ERROR_INVALID_ARGUMENT;
|
||||
Agc::Command::GetRegisterDefaults(version, false, __func__);
|
||||
Agc::Command::GetRegisterDefaults(version, true, __func__);
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetCxRegistersIndirect(CommandBuffer* buf, con
|
|||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcDcbSetCxRegistersIndirectGetSize(std::uint32_t numRegs) {
|
||||
static_cast<void>(numRegs);
|
||||
Agc::Command::CheckBits(numRegs, 0x3fffu, __func__);
|
||||
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) {
|
||||
static_cast<void>(numRegs);
|
||||
Agc::Command::CheckBits(numRegs, 0x3fffu, __func__);
|
||||
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) {
|
||||
static_cast<void>(numRegs);
|
||||
Agc::Command::CheckBits(numRegs, 0x3fffu, __func__);
|
||||
return 20;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,62 +1,26 @@
|
|||
#include "prx/libSceAgc/DcbState/include/Workload.hpp"
|
||||
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
#include "prx/libSceAgcDriver/Resource/include/Registration.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#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) {
|
||||
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<std::uint32_t>(clearMask);
|
||||
packet[4] = static_cast<std::uint32_t>(clearMask >> 32);
|
||||
return packet;
|
||||
(void)buf;
|
||||
(void)stream_id;
|
||||
(void)workload_id;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) {
|
||||
if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount || !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<std::uint32_t>(mask);
|
||||
packet[3] = static_cast<std::uint32_t>(mask >> 32);
|
||||
return packet;
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) {
|
||||
if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
|
||||
(void)buf;
|
||||
(void)stream_id;
|
||||
(void)workload_ids;
|
||||
(void)workload_count;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_DEBUG_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_DEBUG_HPP
|
||||
|
||||
#endif
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_SEMAPHORE_HPP
|
||||
|
||||
#endif
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#include "prx/libSceAgc/Misc/include/Debug.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#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<unsigned>(exceptionId));
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
#include "prx/libSceAgc/Misc/include/Semaphore.hpp"
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#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<std::uintptr_t>(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<std::uintptr_t>::max() - address, __func__, "AMM semaphore memory wraps the address space");
|
||||
auto* labels = static_cast<std::uint8_t*>(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<std::uint64_t>(index) * semaphoreLabelBytes;
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -19,12 +19,6 @@ 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<std::uint32_t>(cachePolicy) << 28u);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
|
@ -48,14 +42,4 @@ 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,11 @@ 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;
|
||||
|
|
@ -80,7 +78,7 @@ enum class PrimitiveType : std::uint32_t {
|
|||
};
|
||||
|
||||
constexpr std::uint32_t VGT_SHADER_STAGES_GS_BIT = 0x20u;
|
||||
constexpr std::uint32_t VGT_SHADER_STAGES_HS_BIT = 0x04u;
|
||||
constexpr std::uint32_t VGT_SHADER_STAGES_NGG_BIT = 0x04u;
|
||||
|
||||
enum class AgcDirectResourceType : std::uint32_t {
|
||||
GdsCounterRange = 0,
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <prx/libc/include/General.hpp>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -53,21 +53,10 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS
|
|||
: CreateInterpolantValue(psWord, gsSemantic);
|
||||
}
|
||||
|
||||
std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) {
|
||||
const std::uint32_t f16 = (psWord >> 20u) & 0x3u;
|
||||
if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown");
|
||||
}
|
||||
return CreateInterpolantValueSplitF16(psWord, gsSemantic);
|
||||
}
|
||||
|
||||
int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) {
|
||||
int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue) {
|
||||
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");
|
||||
}
|
||||
|
|
@ -77,14 +66,13 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
|
|||
auto* output = regs;
|
||||
regs = values.data();
|
||||
if (ps == nullptr || ps->num_input_semantics == 0) {
|
||||
const std::uint32_t count = fillIdentityTail ? InterpolantRegisterCount : 0;
|
||||
if (fillIdentityTail) FillIdentityInterpolants(regs, 0);
|
||||
FillIdentityInterpolants(regs, 0);
|
||||
if (ps != nullptr) {
|
||||
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, count}, {});
|
||||
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {});
|
||||
if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0);
|
||||
}
|
||||
transaction.Commit();
|
||||
std::copy_n(values.begin(), count, output);
|
||||
std::copy(values.begin(), values.end(), output);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -110,12 +98,11 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
|
|||
SetInterpolantRegister(regs, i, createValue(ShaderSemanticWord(psSemantic), gsSemantic));
|
||||
}
|
||||
|
||||
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}, {});
|
||||
FillIdentityInterpolants(regs, ps->num_input_semantics);
|
||||
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {});
|
||||
AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0);
|
||||
transaction.Commit();
|
||||
std::copy_n(values.begin(), count, output);
|
||||
std::copy(values.begin(), values.end(), output);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
|
@ -123,18 +110,14 @@ 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, true);
|
||||
APS5_EXPORT("HV4j+E0MBHE", sceAgcCreateInterpolantMapping);
|
||||
int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
|
||||
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue);
|
||||
}
|
||||
|
||||
APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping);
|
||||
int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
|
||||
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true);
|
||||
}
|
||||
|
||||
APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw);
|
||||
int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
|
||||
return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false);
|
||||
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
#include <algorithm>
|
||||
|
||||
extern "C" int APS5_VABI sceAgcCreatePrimState(ShaderRegister*, ShaderRegister*, const Shader*, const Shader*, std::uint32_t);
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister*, const Shader*, const Shader*);
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister*, const Shader*, const Shader*);
|
||||
|
||||
extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegister* primitive,
|
||||
const void* reserved, const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
|
||||
|
|
@ -26,11 +26,7 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist
|
|||
std::array<ShaderRegister, 34> contextValues{};
|
||||
std::array<ShaderRegister, 3> primitiveValues{};
|
||||
sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType);
|
||||
sceAgcCreateInterpolantMapping_0100(contextValues.data() + 2, vertex, pixel);
|
||||
if (pixel != nullptr) {
|
||||
const std::array<const Shader*, 2> stages{vertex, pixel};
|
||||
AgcDriverResolveGraphicsStagesAbi_nid_postfix(stages, contextValues, primitiveValues);
|
||||
}
|
||||
sceAgcCreateInterpolantMapping(contextValues.data() + 2, vertex, pixel);
|
||||
AgcDriverResolveGraphicsAbi_nid_postfix(vertex, pixel, primitiveType);
|
||||
transaction.Commit();
|
||||
std::copy(contextValues.begin(), contextValues.end(), context);
|
||||
|
|
|
|||
|
|
@ -11,18 +11,6 @@
|
|||
#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<unsigned>(reg.offset), static_cast<unsigned>(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) {
|
||||
|
|
@ -37,34 +25,31 @@ 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);
|
||||
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");
|
||||
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");
|
||||
}
|
||||
std::array<ShaderRegister, 2> 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) {
|
||||
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)};
|
||||
if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) contextValues[1] = hs->specials->vgt_gs_out_prim_type;
|
||||
}
|
||||
std::array<ShaderRegister, 3> 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<const Shader*, 2> stages{hs, gs};
|
||||
const bool patch = prim_type == static_cast<std::uint32_t>(ShaderRegs::PrimitiveType::Patch);
|
||||
if (patch == ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_HS_BIT) != 0)) {
|
||||
AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues);
|
||||
}
|
||||
AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues);
|
||||
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_HS_BIT)) == 0) {
|
||||
if (cx_regs != nullptr && (cx_regs[0].value & (ShaderRegs::VGT_SHADER_STAGES_GS_BIT | ShaderRegs::VGT_SHADER_STAGES_NGG_BIT)) == 0) {
|
||||
cx_regs[1].value &= ~0x7u;
|
||||
cx_regs[1].value |= GraphicsPrimTypeToGsOut(prim_type);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,21 @@ 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;
|
||||
|
|
@ -30,11 +45,31 @@ 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;
|
||||
|
|
@ -45,6 +80,16 @@ 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;
|
||||
|
|
@ -55,4 +100,15 @@ 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ 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 {
|
||||
|
|
@ -109,31 +107,6 @@ void testTarget(TSetter setter, std::size_t field, std::uint32_t cachePolicyBits
|
|||
check(storage.words == expected, name);
|
||||
}
|
||||
|
||||
template <typename TSetter>
|
||||
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;
|
||||
|
|
@ -155,8 +128,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -10,12 +10,9 @@
|
|||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
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" int APS5_VABI sceAgcSuspendPoint();
|
||||
extern "C" int APS5_VABI sceAgcInit(std::uint32_t version);
|
||||
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults();
|
||||
|
|
@ -32,28 +29,14 @@ 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 {
|
||||
|
||||
constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80890001);
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
throw std::runtime_error(message);
|
||||
|
|
@ -126,21 +109,6 @@ 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<std::uint32_t, 7> 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);
|
||||
|
|
@ -148,14 +116,12 @@ 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");
|
||||
}
|
||||
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");
|
||||
check(storage.buffer.cursor_up == storage.words.data() + 32 && storage.words[32] == 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() + 36, "invalid CLEAR_STATE modified the buffer");
|
||||
check(storage.words == before && storage.buffer.cursor_up == storage.words.data() + 32, "invalid CLEAR_STATE modified the buffer");
|
||||
}
|
||||
|
||||
struct ContextGrowth {
|
||||
|
|
@ -220,91 +186,6 @@ 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<Vector, 3> vectors{{
|
||||
{0x11111111u, 0x22222222u, 0x33333333u, 0x22222222u},
|
||||
{1, 2, 3, 2},
|
||||
{0x22222222u, static_cast<std::uintptr_t>(0x3333333344444444ull), 0, 0x44444444u}}};
|
||||
for (const auto& vector : vectors) {
|
||||
Storage storage;
|
||||
storage.words.fill(0xdeadbeefu);
|
||||
const auto before = storage.words;
|
||||
const auto* ids = reinterpret_cast<const std::uint32_t*>(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]{};
|
||||
|
|
@ -324,19 +205,6 @@ 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<std::uintptr_t>(arguments);
|
||||
const auto* bound = sceAgcDcbSetBaseIndirectArgs(&storage.buffer, 0, arguments);
|
||||
check(bound[1] == 1 && bound[2] == static_cast<std::uint32_t>(address) && bound[3] == static_cast<std::uint32_t>(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<const volatile void*>(address + 4)); });
|
||||
check(storage.words == before, "misaligned indirect base modified packet memory");
|
||||
}
|
||||
|
||||
void testContextState() {
|
||||
const std::array<std::array<std::uint32_t, 6>, 4> sizes{{{5}, {5, 8, 9, 3, 2}, {3, 5, 8, 9, 2}, {5, 8, 9, 3, 2, 5}}};
|
||||
const std::array<std::array<std::uint32_t, 4>, 4> reservations{{{5}, {22, 3, 2}, {3, 22, 2}, {22, 3, 2, 5}}};
|
||||
|
|
@ -415,17 +283,6 @@ 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<std::uint32_t, 4> 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");
|
||||
check(sceAgcSuspendPoint() == 0, "empty suspend failed");
|
||||
}
|
||||
|
||||
|
|
@ -514,111 +371,6 @@ 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<AtomicGdsAllocation*>(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<std::uint32_t, 11> 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<std::array<std::uint32_t, 3>, 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<std::uint32_t, 11> 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<std::uint32_t, 11> 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");
|
||||
|
|
@ -626,10 +378,8 @@ void testDefaults() {
|
|||
expectFailure([] { sceAgcInit_0090(nullptr, 8); });
|
||||
expectFailure([&] { sceAgcInit_0090(&state, 14); });
|
||||
check(sceAgcInit(8) == 0, "AGC version initialization failed");
|
||||
check(sceAgcInit(13) == 0, "AGC version 13 initialization failed");
|
||||
check(sceAgcInit(14) == SCE_AGC_ERROR_INVALID_ARGUMENT, "unsupported AGC version did not return invalid argument");
|
||||
check(sceAgcInit(0xffffffffu) == SCE_AGC_ERROR_INVALID_ARGUMENT, "maximum AGC version did not return invalid argument");
|
||||
check(sceAgcInit(0xffffc278u) == SCE_AGC_ERROR_INVALID_ARGUMENT, "pointer-valued AGC argument did not return invalid argument");
|
||||
expectFailure([] { sceAgcInit(14); });
|
||||
expectFailure([] { sceAgcInit(0xffffffffu); });
|
||||
for (std::uint32_t version = 0; version < 14; ++version) {
|
||||
for (const bool internal : {false, true}) {
|
||||
auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__);
|
||||
|
|
@ -643,47 +393,6 @@ void testDefaults() {
|
|||
expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); });
|
||||
}
|
||||
|
||||
void testEventWrite() {
|
||||
Storage storage;
|
||||
const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(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<const void*>(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<const void*>(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<const void*>(0x2d2a10000ull));
|
||||
check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
|
@ -695,23 +404,16 @@ int main(int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
testPackets();
|
||||
testEventWrite();
|
||||
testNop();
|
||||
testClearState();
|
||||
testIndexedIndirectDraws();
|
||||
testMarkers();
|
||||
testWorkloads();
|
||||
testWorkloadStreams();
|
||||
testIndexBuffer();
|
||||
testIndirectBase();
|
||||
testContextState();
|
||||
testFlip();
|
||||
testRegisters();
|
||||
testRegisterRange();
|
||||
testPacketPayloadAddress();
|
||||
testMemory();
|
||||
testAtomicGds();
|
||||
testDebugRaiseException();
|
||||
testDefaults();
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC command tests passed");
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
#include "SceShaders.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
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 <typename TAction>
|
||||
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<ShaderRegister>& cx) {
|
||||
Shader gs{};
|
||||
gs.cx_registers = cx.empty() ? nullptr : cx.data();
|
||||
gs.num_cx_registers = static_cast<std::uint8_t>(cx.size());
|
||||
return gs;
|
||||
}
|
||||
|
||||
void expectPayload(const Shader& gs, std::uint32_t expected, const char* message) {
|
||||
std::array<std::uint32_t, 2> 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<ShaderRegister> cx;
|
||||
expectPayload(makeGs(cx), 0, "a shader without context registers reported a payload");
|
||||
}
|
||||
|
||||
void testTwoComponentIndexFormat() {
|
||||
std::vector<ShaderRegister> 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<ShaderRegister> 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<ShaderRegister> 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<ShaderRegister> 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<ShaderRegister> 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<ShaderRegister> 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<ShaderRegister> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,16 +11,14 @@
|
|||
#include <stdexcept>
|
||||
#include <span>
|
||||
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps);
|
||||
|
||||
namespace {
|
||||
|
||||
using Registers = std::array<ShaderRegister, 32>;
|
||||
const Shader* mappedPixel = nullptr;
|
||||
Registers preparedMapping{};
|
||||
std::size_t preparedCount = 0;
|
||||
unsigned mappings = 0;
|
||||
unsigned links = 0;
|
||||
|
||||
|
|
@ -67,11 +65,6 @@ void testIdentity() {
|
|||
regs = filled();
|
||||
check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed");
|
||||
checkIdentity(regs, 0, "mapping without inputs is not the identity");
|
||||
regs = filled();
|
||||
check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed");
|
||||
check(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() {
|
||||
|
|
@ -106,67 +99,13 @@ void testMapping() {
|
|||
check(split[4].value == 0x03180002u, "full f16 interpolant mapping changed");
|
||||
checkIdentity(split, static_cast<std::uint32_t>(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(sceAgcUnknown_vieBRwlh1Lw(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(sceAgcCreateInterpolantMapping(regular.data(), &gs, &ps) == 0, "mapping failed");
|
||||
check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers");
|
||||
check(regular[3].value == split[3].value && regular[4].value == split[4].value, "mappings differ outside the high f16 half mode");
|
||||
checkIdentity(regular, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
|
||||
}
|
||||
|
||||
void testUpdateMapping() {
|
||||
std::array<ShaderSemantic, 4> 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<ShaderSemantic, 3> 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<std::uint16_t>(outputs.size());
|
||||
Shader ps{};
|
||||
ps.input_semantics = inputs.data();
|
||||
ps.num_input_semantics = static_cast<std::uint32_t>(inputs.size());
|
||||
|
||||
auto update = filled();
|
||||
const auto untouched = filled();
|
||||
check(sceAgcUnknown_vieBRwlh1Lw(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(sceAgcUnknownCreateInterpolantMapping(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<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
|
||||
|
||||
for (const std::uint32_t index : {1u, 3u}) {
|
||||
inputs[index].default_value = 0;
|
||||
update = filled();
|
||||
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); });
|
||||
check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers");
|
||||
inputs[index].default_value = inputs[index].default_value_hi;
|
||||
}
|
||||
}
|
||||
|
||||
void testRejections() {
|
||||
std::array<ShaderSemantic, 33> inputs{};
|
||||
Shader gs{};
|
||||
|
|
@ -176,25 +115,20 @@ void testRejections() {
|
|||
std::array<ShaderRegister, 64> regs{};
|
||||
std::memset(regs.data(), 0xcc, sizeof(regs));
|
||||
const auto saved = regs;
|
||||
expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); });
|
||||
expectFailure([&] { sceAgcCreateInterpolantMapping(regs.data(), &gs, &ps); });
|
||||
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); });
|
||||
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); });
|
||||
check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers");
|
||||
ps.num_input_semantics = 1;
|
||||
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); });
|
||||
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); });
|
||||
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); });
|
||||
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); });
|
||||
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> 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;
|
||||
}
|
||||
|
||||
|
|
@ -207,9 +141,8 @@ int main() {
|
|||
try {
|
||||
testIdentity();
|
||||
testMapping();
|
||||
testUpdateMapping();
|
||||
testRejections();
|
||||
check(mappings == 7u && links == 5u, "mapping preparation call count changed");
|
||||
check(mappings == 3u && links == 2u, "mapping preparation call count changed");
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC interpolant mapping tests passed");
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -116,8 +116,8 @@ void testIndirect() {
|
|||
check(storage.words == expected, "count setter modified unrelated packet data");
|
||||
}
|
||||
const auto before = storage.words;
|
||||
for (const auto count : {0x4000u, 0xffffffffu, 0xf5f5039cu}) {
|
||||
check(sizes[i](count) == 20, "indirect size depends on the count");
|
||||
for (const auto count : {0x4000u, 0xffffffffu}) {
|
||||
expectFailure([&] { sizes[i](count); });
|
||||
expectFailure([&] { setters[i](packet, count); });
|
||||
}
|
||||
expectFailure([&] { setters[i](nullptr, 0); });
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
extern "C" int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes);
|
||||
extern "C" int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint64_t blockBytes = 0x4000u;
|
||||
constexpr std::uint64_t labelBytes = 32u;
|
||||
constexpr std::uint64_t counterBytes = 8u;
|
||||
constexpr std::uint64_t memoryBytes = 2u * blockBytes;
|
||||
constexpr std::uint64_t storageBytes = 4u * blockBytes;
|
||||
constexpr std::uint32_t labelCount = static_cast<std::uint32_t>(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<std::uint8_t, AlignedRelease> storage;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
template <typename TAction>
|
||||
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<std::uint8_t> snapshot() {
|
||||
return {storage.get(), storage.get() + storageBytes};
|
||||
}
|
||||
|
||||
void checkUnchanged(const std::vector<std::uint8_t>& 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<void*, 3> 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<std::uintptr_t>::max() & ~static_cast<std::uintptr_t>(blockBytes - 1u);
|
||||
sceAgcSetAmmSemaphoreMemory(reinterpret_cast<void*>(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<void*, 3> 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<std::uint32_t>::max()}) {
|
||||
void* label = storage.get();
|
||||
expectFailure([&] { sceAgcGetSemaphoreLabel(index, &label); });
|
||||
check(label == storage.get(), "a rejected semaphore label changed the output");
|
||||
}
|
||||
expectFailure([] { sceAgcGetSemaphoreLabel(0, nullptr); });
|
||||
}
|
||||
|
||||
void testRepeatedRegistration() {
|
||||
std::fill(memory(), memory() + memoryBytes, std::uint8_t{0x3c});
|
||||
const auto before = snapshot();
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(memory(), memoryBytes); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(storage.get(), blockBytes); });
|
||||
expectFailure([] { sceAgcSetAmmSemaphoreMemory(nullptr, 0); });
|
||||
checkUnchanged(before, "a repeated registration changed memory");
|
||||
void* label = nullptr;
|
||||
check(sceAgcGetSemaphoreLabel(labelCount - 1u, &label) == 0 && label == memory() + memoryBytes - labelBytes, "a repeated registration replaced the semaphore memory");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
storage.reset(static_cast<std::uint8_t*>(::operator new(storageBytes, std::align_val_t{blockBytes})));
|
||||
fillStorage();
|
||||
testUnregistered();
|
||||
testRegistrationRejections();
|
||||
testUnsettledClearing();
|
||||
testRegistration();
|
||||
testLabels();
|
||||
testLabelRejections();
|
||||
testRepeatedRegistration();
|
||||
storage.reset();
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC semaphore tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
try { LibcRunShutdown_nid_postfix(); }
|
||||
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -7,20 +7,16 @@
|
|||
#include <array>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
|
||||
extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t);
|
||||
extern "C" int APS5_VABI sceAgcCreatePrimState(ShaderRegister*, ShaderRegister*, const Shader*, const Shader*, std::uint32_t);
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister*, const Shader*, const Shader*);
|
||||
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister*, const Shader*, const Shader*);
|
||||
|
||||
namespace {
|
||||
std::array<ShaderRegister, 2> preparedContext;
|
||||
std::array<ShaderRegister, 3> preparedPrimitive;
|
||||
std::size_t stageCount = 0;
|
||||
std::array<const Shader*, 2> linkedStages{};
|
||||
std::size_t linkedStageCount = 0;
|
||||
std::size_t linkedContextCount = 0;
|
||||
unsigned preparations = 0;
|
||||
unsigned mappings = 0;
|
||||
unsigned links = 0;
|
||||
|
|
@ -30,63 +26,13 @@ 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<ShaderRegister, 2> 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<std::uint32_t>(GsOutputPrimitiveType::Rectangle2D));
|
||||
bool rejectedMissingHullOutput = false;
|
||||
try { static_cast<void>(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<void>(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<std::uint8_t>(ShaderBinaryType::Gs);
|
||||
preRaster.specials = &handBuilt;
|
||||
std::array<ShaderRegister, 2> context{};
|
||||
std::array<ShaderRegister, 3> 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<std::uint32_t>(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<void>(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<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
|
||||
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();
|
||||
}
|
||||
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();
|
||||
++preparations;
|
||||
}
|
||||
|
||||
|
|
@ -133,7 +79,7 @@ int main() {
|
|||
context.back().value = primitive.back().value = 0xdeadbeef;
|
||||
const auto prepare = [&] { Require(sceAgcCreatePrimState(outputs & 1 ? context.data() : nullptr, outputs & 2 ? primitive.data() : nullptr, hs, &vertex, 7) == 0); };
|
||||
if (primitiveFirst) prepare();
|
||||
Require(sceAgcCreateInterpolantMapping_0100(interpolants.data(), &vertex, &pixel) == 0);
|
||||
Require(sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel) == 0);
|
||||
if (!primitiveFirst) prepare();
|
||||
Require(preparations == 1 && mappings == 1 && links == 1 && stageCount == (hs ? 2u : 1u));
|
||||
Require(preparedContext[0].offset == VGT_SHADER_STAGES_EN && preparedContext[1].offset == VGT_GS_OUT_PRIM_TYPE);
|
||||
|
|
@ -145,19 +91,6 @@ int main() {
|
|||
}
|
||||
}
|
||||
}
|
||||
preparations = 0;
|
||||
std::array<ShaderRegister, 2> patchContext{};
|
||||
std::array<ShaderRegister, 3> 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<ShaderRegister, 34> linkedContext{};
|
||||
std::array<ShaderRegister, 3> linkedPrimitive{};
|
||||
for (auto& value : linkedContext) value.value = 0xdeadbeef;
|
||||
|
|
@ -171,29 +104,23 @@ int main() {
|
|||
for (const auto value : linkedContext) Require(value.value == 0xdeadbeef);
|
||||
for (const auto value : linkedPrimitive) Require(value.value == 0xdeadbeef);
|
||||
rejected = false;
|
||||
try { sceAgcCreateInterpolantMapping_0100(interpolants.data(), &vertex, &pixel); }
|
||||
try { sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel); }
|
||||
catch (const std::runtime_error&) { rejected = true; }
|
||||
Require(rejected);
|
||||
for (const auto value : interpolants) Require(value.value == 0xdeadbeef);
|
||||
rejectLink = false;
|
||||
pixel.num_input_semantics = 33;
|
||||
rejected = false;
|
||||
try { sceAgcCreateInterpolantMapping_0100(interpolants.data(), &vertex, &pixel); }
|
||||
try { sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel); }
|
||||
catch (const std::runtime_error&) { rejected = true; }
|
||||
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(sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, nullptr) == 0);
|
||||
Require(preparations == 0 && mappings == 0 && links == 0);
|
||||
OptionalGeometryOutputRegister(special, vertex, hull);
|
||||
HandBuiltUserVgprRegister();
|
||||
std::cout << "shader helper preparation tests passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,77 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_COMPUTEPIPELINECAPTURE_HPP
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<ComputeDescriptorBinding> 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<std::uint32_t> spirv;
|
||||
std::vector<ComputeDescriptorSetLayout> sets;
|
||||
std::vector<VkPushConstantRange> pushConstants;
|
||||
std::vector<ComputeSpecialization> 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<std::uint8_t, VK_UUID_SIZE> pipelineCacheUuid{};
|
||||
|
||||
void Validate() const;
|
||||
std::vector<std::byte> Encode() const;
|
||||
static ComputePipelineCapture Decode(std::span<const std::byte> bytes);
|
||||
static ComputePipelineCapture Read(const std::filesystem::path& path);
|
||||
std::filesystem::path Write(const std::filesystem::path& directory) const;
|
||||
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<std::uint8_t, VK_UUID_SIZE> pipelineCacheUuid{};
|
||||
std::size_t initialCacheBytes = 0;
|
||||
std::vector<std::byte> cache;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -25,7 +25,6 @@ struct DisplayBuffer {
|
|||
};
|
||||
|
||||
std::size_t DisplayBufferSize(const DisplayBuffer& buffer);
|
||||
std::size_t DisplayBufferOffset(const DisplayBuffer& buffer, std::uint32_t x, std::uint32_t y);
|
||||
std::vector<std::byte> DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span<const std::byte> source);
|
||||
std::vector<std::byte> ReadDisplayBuffer(const DisplayBuffer& buffer);
|
||||
std::array<std::byte, 4> DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys);
|
||||
|
|
|
|||
|
|
@ -10,8 +10,6 @@ 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);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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();
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ namespace AgcDriver::DriverDetail {
|
|||
|
||||
double TraceMs();
|
||||
|
||||
std::uint64_t SkippedShader();
|
||||
|
||||
void require(bool condition, const char* reason);
|
||||
|
||||
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset);
|
||||
|
|
|
|||
|
|
@ -82,8 +82,6 @@ struct DataMask {
|
|||
|
||||
enum class EntryOutcome { Equal, EqualData, Differing, Inaccessible, QueuedLabel, FlushingImage, PublishMoved, PendingMoved, ForgetMoved };
|
||||
|
||||
bool CacheableResult(const ShaderRecompiler::RecompileResult& compiled);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -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, IndirectWorkgroupMemory = 8, IndirectPaths = 9 };
|
||||
enum IndirectPath { IndirectGpu = 0, IndirectPendingImage = 1, IndirectCopiedWrite = 2, IndirectNotImported = 3, IndirectThreadDimensions = 4, IndirectFillKernel = 5, IndirectMisaligned = 6, IndirectDisabled = 7, IndirectPaths = 8 };
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,9 +37,6 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
|
|||
struct PreparedGraphicsStage {
|
||||
std::shared_ptr<const ShaderSnapshot> snapshot;
|
||||
PreparedShaders::Entry entry;
|
||||
ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex;
|
||||
std::uint32_t pushOffset = 0;
|
||||
std::shared_ptr<const ShaderRecompiler::RecompileResult> registration;
|
||||
};
|
||||
|
||||
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target);
|
||||
|
|
@ -75,19 +72,6 @@ struct DrawEntryCounters {
|
|||
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
|
||||
};
|
||||
|
||||
class DrawKeySightings {
|
||||
public:
|
||||
static constexpr std::size_t Ways = 4;
|
||||
explicit DrawKeySightings(std::size_t keys);
|
||||
bool SeenBefore(std::uint64_t key);
|
||||
|
||||
private:
|
||||
std::vector<std::uint64_t> keys;
|
||||
std::vector<std::uint8_t> filled;
|
||||
std::vector<std::uint8_t> next;
|
||||
std::size_t bucketMask = 0;
|
||||
};
|
||||
|
||||
enum class DrawVerdict { Drawn, Nothing, Rejected };
|
||||
|
||||
struct StageCapture {
|
||||
|
|
@ -97,8 +81,6 @@ struct StageCapture {
|
|||
std::uint32_t pushOffset = 0;
|
||||
};
|
||||
|
||||
bool DecodeProgramsCurrent(const DrawDecode& decode, const ShaderRegistry& registry);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_DRIVER_DRAW_DRAWREGISTERKEY_HPP
|
||||
|
||||
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords = false, bool incremental = true);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -44,7 +44,6 @@ namespace AgcDriver::DriverDetail {
|
|||
class Driver {
|
||||
public:
|
||||
static Driver& Get();
|
||||
static std::uint64_t GpuMapImportThreads();
|
||||
~Driver();
|
||||
void Shutdown();
|
||||
void WaitIdle();
|
||||
|
|
@ -61,10 +60,6 @@ public:
|
|||
void ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
|
||||
void ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
|
||||
void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType);
|
||||
static void FoldDrawOffsets(const ShaderRecompiler::RecompileResult& result, const DrawProgram& program, Pm4::DrawParameters& parameters);
|
||||
static std::optional<Graphics::IndirectDrawPath> ClassifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr<VulkanDevice>& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect);
|
||||
static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request);
|
||||
DrawEntryCounters DrawCacheCounters();
|
||||
|
||||
private:
|
||||
friend class SampledReadScope;
|
||||
|
|
@ -79,10 +74,6 @@ 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);
|
||||
|
|
@ -115,8 +106,6 @@ private:
|
|||
static std::size_t drawCacheEntries();
|
||||
void accountDrawVariant(const DispatchVariant& variant, bool added);
|
||||
void insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::shared_ptr<const DrawDecode> decode);
|
||||
bool admitDrawKey(std::uint64_t key);
|
||||
void eraseDrawEntry(std::uint64_t key, const std::shared_ptr<DrawEntry>& expected);
|
||||
std::shared_ptr<const DrawRecipe> findDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages);
|
||||
void attachDrawRecipe(std::uint64_t key, const std::vector<std::shared_ptr<DispatchVariant>>& stages, std::shared_ptr<const DrawRecipe> recipe);
|
||||
void reportDrawCache(DrawEntryCounters& counters);
|
||||
|
|
@ -131,20 +120,22 @@ private:
|
|||
static bool drawPrecheck();
|
||||
std::optional<DrawVerdict> precheckDraw(const QueueState& queue, const Submission& submission, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect);
|
||||
static std::uint32_t drawUserWord(const DrawProgram& program, std::int32_t sgpr);
|
||||
std::optional<Graphics::IndirectDrawPath> classifyIndirectDraw(const ShaderRecompiler::RecompileResult& result, const Graphics::State& graphics, const DrawProgram& frontProgram, const std::shared_ptr<VulkanDevice>& localDevice, Pm4::DrawParameters& drawParameters, bool traceIndirect);
|
||||
DrawVerdict draw(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::string& rejected);
|
||||
void addDriverPhases(DispatchClass which, const std::array<double, DriverPhaseCount>& ms, bool hit, bool validated);
|
||||
static PendingDispatchPhases& pendingDispatchPhases();
|
||||
static std::chrono::steady_clock::time_point& packetStartedAt();
|
||||
static PendingDrawPhases& pendingDrawPhases();
|
||||
void addDrawPhases(const std::array<double, DrawDriverPhaseCount>& ms, bool drawn, std::uint64_t captures);
|
||||
void noteLabelStore(std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp, std::uint32_t queue);
|
||||
bool storedSince(std::span<const std::uint32_t> packet, std::uint64_t address, std::size_t bytes, std::uint64_t received, std::uint32_t* writer = nullptr);
|
||||
void noteLabelStore(std::uint64_t address, std::span<const std::byte> bytes, std::uint64_t stamp);
|
||||
bool storedSince(std::span<const std::uint32_t> packet, std::uint64_t address, std::size_t bytes, std::uint64_t received);
|
||||
static void traceLabel(std::span<const std::uint32_t> packet, std::uint32_t queue);
|
||||
static std::array<WriteRecord, 16384>& writeHistory();
|
||||
static std::size_t& writeCursor();
|
||||
static void dumpPackets(std::span<const std::uint32_t> 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<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
static bool fillClearEnabled();
|
||||
static bool fillClearExactOnly();
|
||||
|
|
@ -186,7 +177,6 @@ private:
|
|||
template<typename TVisit>
|
||||
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<std::pair<std::uint64_t, std::uint64_t>>& ranges);
|
||||
void noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue);
|
||||
void noteDrawWriters(std::span<const Graphics::CompiledShader> stages, std::uint32_t queue);
|
||||
std::optional<WrittenBuffer> newestWriterLocked(std::uint64_t begin, std::uint64_t end) const;
|
||||
|
|
@ -205,41 +195,7 @@ private:
|
|||
static bool pollReapAll();
|
||||
static bool pollTryEach();
|
||||
static bool traceLateLabels();
|
||||
void waitMemory(std::span<const std::uint32_t> packet, std::uint32_t queue, const PacketHistory& context, std::uint64_t received, bool heldAtSubmit, bool requireMemory = false);
|
||||
struct PendingWait {
|
||||
std::array<std::uint32_t, 8> words;
|
||||
std::size_t count;
|
||||
std::uint64_t received;
|
||||
};
|
||||
static std::vector<PendingWait>& pendingWaits();
|
||||
static void notePendingWait(std::span<const std::uint32_t> 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<std::uint32_t, 5> packet{};
|
||||
std::shared_ptr<const ShaderSnapshot> registeredShader;
|
||||
std::shared_ptr<const ShaderRecompiler::RecompileResult> compiledResult;
|
||||
std::shared_ptr<DispatchVariant> keepVariant, attachVariant;
|
||||
std::shared_ptr<ShaderMemory> shaderMemory;
|
||||
std::vector<ShaderRecompiler::MemoryRegion> captured;
|
||||
std::vector<std::uint32_t> liveWords;
|
||||
std::shared_ptr<const ShaderRecompiler::ResourceCapture> 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;
|
||||
double resolveMs = 0, resolveMaxMs = 0, ageMs = 0, ageMaxMs = 0;
|
||||
std::chrono::steady_clock::time_point reported = std::chrono::steady_clock::now();
|
||||
};
|
||||
static std::map<std::size_t, ResolvedDispatch>& 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);
|
||||
void waitMemory(std::span<const std::uint32_t> packet, std::uint32_t queue, const PacketHistory& context, std::uint64_t received, bool heldAtSubmit);
|
||||
static PollStats& pollStats();
|
||||
static WaitOutcomes& waitOutcomes();
|
||||
static Graphics::Recorder::LateStatistics& lateCountsSeen();
|
||||
|
|
@ -311,7 +267,6 @@ private:
|
|||
std::list<std::uint64_t> drawOrder;
|
||||
std::mutex drawCacheMutex;
|
||||
std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0;
|
||||
std::optional<DrawKeySightings> drawKeySightings;
|
||||
|
||||
DrawEntryCounters drawEntryCounters;
|
||||
|
||||
|
|
@ -346,11 +301,7 @@ private:
|
|||
std::uint64_t queue0Executing = 0;
|
||||
std::atomic<std::uint32_t> orderHolders{0};
|
||||
std::atomic<std::uint32_t> runningWorkers{0};
|
||||
std::atomic<std::uint64_t> idleEpoch{0};
|
||||
std::atomic<std::uint64_t> queue0Awaited{0};
|
||||
std::atomic<std::uint32_t> flipsAhead{0};
|
||||
std::atomic<std::uint32_t> flipHolders{0};
|
||||
std::atomic<bool> queue0AwaitsTitle{false};
|
||||
|
||||
std::atomic<std::uint64_t> evidenceReads{0};
|
||||
std::atomic<std::uint64_t> evidenceValidations{0};
|
||||
|
|
|
|||
|
|
@ -1,54 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_BUFFERFILL_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_BUFFERFILL_HPP
|
||||
|
||||
#include "Recompiler.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
inline constexpr std::array<std::uint32_t, 18> PatternFillKernel{
|
||||
0xbfa00001u, 0xd7460000u, 0x04010c0cu, 0xf4201a84u, 0xfa000000u, 0xbf8cc07fu,
|
||||
0x7da8006au, 0xbf880009u, 0xf4201a84u, 0xfa000004u, 0xbf8cc07fu, 0x3602006au,
|
||||
0xe0002000u, 0x80000101u, 0xbf8c3f70u, 0xe0102000u, 0x80010100u, 0xbf810000u
|
||||
};
|
||||
|
||||
inline bool MatchesPatternFillKernel(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
|
||||
if (userData.size() != 12 || compute.numThreads != std::array<std::uint32_t, 3>{64, 1, 1} || !compute.groupIdEnable[0] || compute.threadIdComponentCount != 1) return false;
|
||||
if (code.size() < PatternFillKernel.size() || !std::equal(PatternFillKernel.begin(), PatternFillKernel.end(), code.begin())) return false;
|
||||
for (const auto first : {0u, 4u, 8u}) {
|
||||
if ((userData[first + 1] & 0xffff0000u) != (first == 8 ? 0x00100000u : 0x00040000u)) return false;
|
||||
if (userData[first + 3] != (first == 8 ? 0x0004dfacu : 0x00014004u)) return false;
|
||||
}
|
||||
return userData[2] >= 1 && userData[10] >= 1;
|
||||
}
|
||||
|
||||
struct PatternFillRange {
|
||||
std::uint64_t source, destination, control, invocations;
|
||||
std::uint32_t records;
|
||||
|
||||
[[nodiscard]] std::optional<std::size_t> UniformBytes(std::uint32_t count, std::uint32_t mask) const {
|
||||
if (mask != 0) return std::nullopt;
|
||||
const auto bytes = std::min({invocations, std::uint64_t{count}, std::uint64_t{records}}) * 4u;
|
||||
if (destination % 16 != 0 || bytes % 16 != 0) return std::nullopt;
|
||||
if (bytes != 0 && ((source < destination + bytes && destination < source + 4) || (control < destination + bytes && destination < control + 8))) return std::nullopt;
|
||||
return static_cast<std::size_t>(bytes);
|
||||
}
|
||||
};
|
||||
|
||||
inline std::optional<PatternFillRange> DecodePatternFillRange(std::span<const std::uint32_t> userData, std::span<const std::uint32_t> packet) {
|
||||
if (userData.size() != 12 || packet.size() < 5 || packet[2] != 1 || packet[3] != 1) return std::nullopt;
|
||||
const std::uint64_t invocations = (packet[4] & 0x20u) != 0 ? packet[1] : std::uint64_t{packet[1]} * 64u;
|
||||
if (invocations > (std::uint64_t{1} << 32u)) return std::nullopt;
|
||||
const auto base = [&](std::size_t word) { return userData[word] | (static_cast<std::uint64_t>(userData[word + 1] & 0xffffu) << 32u); };
|
||||
if (base(0) % 4 != 0 || base(8) % 4 != 0) return std::nullopt;
|
||||
return PatternFillRange{base(0), base(4), base(8), invocations, userData[6]};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
#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 <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
struct DwordPatternFill {
|
||||
std::uint64_t base = 0;
|
||||
std::size_t bytes = 0;
|
||||
std::array<std::uint32_t, 4> pattern{};
|
||||
};
|
||||
|
||||
std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
std::optional<DwordPatternFill> MatchZeroDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
|
||||
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern);
|
||||
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -29,7 +29,6 @@ struct Submission {
|
|||
std::map<std::size_t, std::vector<std::uint32_t>> registerLists;
|
||||
bool suspend = false;
|
||||
bool waitFree = false;
|
||||
bool holdsFlip = false;
|
||||
|
||||
std::uint64_t received = 0;
|
||||
std::vector<std::uint64_t> labelWrites;
|
||||
|
|
|
|||
|
|
@ -5,34 +5,19 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct Shader;
|
||||
|
||||
namespace ShaderRecompiler {
|
||||
class ShaderPreparationContext;
|
||||
}
|
||||
|
||||
namespace AgcDriver {
|
||||
class VulkanDevice;
|
||||
}
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
struct ShaderSnapshot;
|
||||
using ShaderRegistry = std::map<std::uint64_t, std::vector<std::shared_ptr<const ShaderSnapshot>>>;
|
||||
using ShaderRegistry = std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>>;
|
||||
|
||||
struct PreparedShaderState {
|
||||
struct Entry {
|
||||
|
|
@ -61,14 +46,9 @@ struct PreparedShaderState {
|
|||
std::vector<RectangleProgress> rectangleProgress;
|
||||
std::vector<std::weak_ptr<const ShaderSnapshot>> fragments;
|
||||
bool rectangleRequested = false;
|
||||
bool deferred = false;
|
||||
};
|
||||
struct PreparedShaders : PreparedShaderState {
|
||||
std::mutex mutex;
|
||||
std::condition_variable settled;
|
||||
std::exception_ptr failure;
|
||||
std::atomic<bool> demanded{false};
|
||||
bool pending = false;
|
||||
};
|
||||
|
||||
struct RegisteredShaderState {
|
||||
|
|
@ -85,38 +65,18 @@ struct ShaderSnapshot {
|
|||
std::vector<std::byte> header;
|
||||
std::shared_ptr<PreparedShaders> prepared = std::make_shared<PreparedShaders>();
|
||||
std::shared_ptr<const RegisteredShaderState> registeredState;
|
||||
std::shared_ptr<std::atomic<std::uint8_t>> workgroupUse = std::make_shared<std::atomic<std::uint8_t>>(0u);
|
||||
std::shared_ptr<std::atomic<std::uint8_t>> positionVectorUse = std::make_shared<std::atomic<std::uint8_t>>(0u);
|
||||
};
|
||||
|
||||
|
||||
std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address);
|
||||
std::shared_ptr<const ShaderSnapshot> ProgramSnapshot(const ShaderRegistry& shaders, std::uint64_t address, std::initializer_list<std::uint8_t> types);
|
||||
void NoteRawGraphicsDraw();
|
||||
bool RawGraphicsDrawn();
|
||||
bool ProgramUsesWorkgroup(const ShaderSnapshot& snapshot, std::size_t codeOffset);
|
||||
// The PA_CL_VS_OUT_CNTL position export bits a vertex program is compiled with: none for a program
|
||||
// without pos1..pos3 exports, so its artifact does not depend on them.
|
||||
std::uint32_t PositionExportLayout(const ShaderSnapshot* snapshot, std::size_t codeOffset, std::span<const std::uint32_t> code, std::uint32_t paClVsOutCntl);
|
||||
bool VertexWorkgroupRequired(const QueueState& queue, const ShaderRegistry& registry);
|
||||
|
||||
std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnostics(const ShaderRecompiler::RecompileRequest& request, ShaderRecompiler::ShaderPreparationContext* preparation = nullptr);
|
||||
std::shared_ptr<const ShaderSnapshot> ReadRawComputeShader(std::uint64_t address);
|
||||
|
||||
std::uint64_t NullPixelProgramAddress();
|
||||
ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device);
|
||||
std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const ShaderSnapshot& snapshot);
|
||||
std::shared_ptr<const ShaderSnapshot> RegisteredHeader(const ShaderRegistry& registry, const Shader* shader);
|
||||
std::shared_ptr<const ShaderSnapshot> RegisteredProgram(const ShaderRegistry& registry, std::uint64_t address, std::initializer_list<std::uint8_t> types);
|
||||
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);
|
||||
void SubmitRegistrationPreparation(std::function<void()> task, const PreparedShaders* prepared = nullptr);
|
||||
void PrioritizeRegistrationPreparation(PreparedShaders& prepared);
|
||||
void SubmitLibraryPrebuild(std::function<void()> task);
|
||||
void CloseRegistrationPreparation();
|
||||
|
||||
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target);
|
||||
|
||||
ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapshot, std::uint64_t vertexId, std::uint64_t fragmentId);
|
||||
ShaderRecompiler::RectListShaders DrawRectangle(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint64_t vertexId, std::uint64_t fragmentId, const ShaderRecompiler::SpirvTarget& target);
|
||||
|
||||
std::shared_ptr<const ShaderRecompiler::SourceHandle> 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);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ struct SubmissionCosts {
|
|||
std::atomic<std::uint64_t> 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<std::uint64_t> gpuLabels, completionLabels, noOpLabels, unlockedDrains;
|
||||
extern std::atomic<std::uint64_t> gpuLabels, completionLabels, notImportedLabels, noOpLabels, unlockedDrains;
|
||||
|
||||
extern std::atomic<std::uint64_t> labelFallbacks[5];
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ enum TrySite { TryLabel = 0, TryFlush, TryReap, TryPoll, TryIdle, TryCapture, Tr
|
|||
|
||||
extern std::atomic<std::uint64_t> triesFailed[TrySites];
|
||||
|
||||
extern std::atomic<std::uint64_t> storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, copiesToHostMemory;
|
||||
extern std::atomic<std::uint64_t> storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained;
|
||||
|
||||
extern std::map<std::uint32_t, std::uint64_t> drainCounts;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FRAGMENTBARYCENTRIC_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FRAGMENTBARYCENTRIC_HPP
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
inline bool FragmentShaderBarycentricUsable(VkDriverId driver, bool reported) {
|
||||
return reported && driver != VK_DRIVER_ID_MOLTENVK;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -30,7 +30,6 @@ void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t a
|
|||
// Variants for resources in heaps the guest commits on demand (partially resident textures, GPU
|
||||
// scratch heaps): uncommitted pages read as zeros, are not compared, and are never stored.
|
||||
void ReadCommitted(std::uint64_t address, std::span<std::byte> destination);
|
||||
void ReadCommittedUnsynced(std::uint64_t address, std::span<std::byte> destination);
|
||||
bool EqualsCommitted(std::uint64_t address, std::span<const std::byte> 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).
|
||||
|
|
@ -60,7 +59,7 @@ void WriteChanged(std::uint64_t address, std::span<const std::byte> 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<bool()>& import, bool staysWatched = true);
|
||||
bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function<bool()>& import);
|
||||
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);
|
||||
|
|
@ -72,13 +71,10 @@ struct UnchangedQuery {
|
|||
};
|
||||
bool WrittenSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
|
||||
bool UnchangedSinceAll(std::span<const UnchangedQuery> queries);
|
||||
std::uint64_t CollectCommittedWrites(std::uint64_t address, std::size_t bytes, std::vector<std::pair<std::uint64_t, std::uint64_t>>& ranges);
|
||||
bool UnchangedCommittedSince(std::span<const std::pair<std::uint64_t, std::uint64_t>> 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<void()>& store);
|
||||
std::uint64_t StoreOwnBytes(std::uint64_t address, std::span<const std::byte> source);
|
||||
bool StoredOver(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
|
||||
|
|
@ -104,7 +100,6 @@ std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes);
|
|||
// 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);
|
||||
// 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
|
||||
|
|
@ -118,6 +113,7 @@ 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<const std::uint64_t> generations, std::span<std::uint8_t> changed, std::span<std::uint8_t> cpu = {});
|
||||
|
||||
// Serializes device work: draws, dispatches, presentation and the deferred write-backs below. The
|
||||
|
|
@ -152,7 +148,6 @@ 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).
|
||||
|
|
@ -187,7 +182,6 @@ 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 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
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
#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 <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
|
|
@ -260,11 +258,11 @@ private:
|
|||
|
||||
class PerformanceTimer {
|
||||
public:
|
||||
explicit PerformanceTimer(const char* scope) : frame(PerformanceContext::Current()), trace(PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr), scope(scope) {
|
||||
explicit PerformanceTimer(const char* scope) : frame(PerformanceContext::Current()), scope(scope) {
|
||||
if (frame != nullptr) {
|
||||
total = frame->Get(scope, "total");
|
||||
}
|
||||
if (frame != nullptr || trace != nullptr) start = Clock::now();
|
||||
if (frame != nullptr) start = Clock::now();
|
||||
previous = start;
|
||||
}
|
||||
|
||||
|
|
@ -276,12 +274,8 @@ public:
|
|||
}
|
||||
|
||||
void Finish() {
|
||||
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;
|
||||
const auto end = Clock::now();
|
||||
if (tail != nullptr) frame->Add(tail, end - previous);
|
||||
frame->Add(total, end - start);
|
||||
frame = nullptr;
|
||||
|
|
@ -293,13 +287,10 @@ public:
|
|||
}
|
||||
|
||||
void Mark(const char* stage, std::uint64_t bytes = 0) {
|
||||
if (frame == nullptr && trace == nullptr) return;
|
||||
if (frame == nullptr) return;
|
||||
const auto now = Clock::now();
|
||||
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);
|
||||
}
|
||||
if (tail == nullptr) tail = frame->Get(scope, "tail");
|
||||
frame->Add(frame->Get(scope, stage), now - previous, bytes);
|
||||
previous = now;
|
||||
}
|
||||
|
||||
|
|
@ -307,9 +298,7 @@ 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{};
|
||||
|
|
|
|||
|
|
@ -1,437 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETRACE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PERFORMANCETRACE_HPP
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
class VulkanDevice;
|
||||
|
||||
class TraceWindow {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
enum class Edge { None, Open, Close };
|
||||
|
||||
static std::optional<TraceWindow> 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 <start>:<length> 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::uint64_t>(std::chrono::duration_cast<std::chrono::seconds>(elapsed).count());
|
||||
}
|
||||
|
||||
bool frames;
|
||||
std::uint64_t start;
|
||||
std::uint64_t length;
|
||||
State state = State::Waiting;
|
||||
std::uint64_t openedPresent = 0;
|
||||
Clock::duration openedAt{};
|
||||
};
|
||||
|
||||
class TraceThread {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static constexpr std::size_t DefaultCapacity = 262144;
|
||||
static constexpr Clock::duration MinimumSlice = std::chrono::microseconds(20);
|
||||
|
||||
struct Slice {
|
||||
const char* scope;
|
||||
const char* stage;
|
||||
Clock::time_point begin;
|
||||
Clock::duration length;
|
||||
};
|
||||
|
||||
struct Total {
|
||||
const char* scope;
|
||||
const char* stage;
|
||||
Clock::duration length;
|
||||
std::uint64_t count;
|
||||
};
|
||||
|
||||
struct Snapshot {
|
||||
std::string name;
|
||||
std::span<const Slice> slices;
|
||||
std::vector<Total> totals;
|
||||
bool dropped = false;
|
||||
};
|
||||
|
||||
TraceThread(std::string name, const std::atomic<std::uint64_t>& presents, std::size_t capacity = DefaultCapacity)
|
||||
: name(std::move(name)), presents(presents), slices(std::make_unique<Slice[]>(capacity)), capacity(capacity) {}
|
||||
|
||||
TraceThread(const TraceThread&) = delete;
|
||||
TraceThread& operator=(const TraceThread&) = delete;
|
||||
|
||||
void Add(const char* scope, const char* stage, Clock::time_point begin, Clock::time_point end) {
|
||||
const auto length = end - begin;
|
||||
account(scope, stage, length);
|
||||
const auto present = presents.load(std::memory_order_relaxed);
|
||||
if (!sampled || present != sampledPresent) {
|
||||
if (sampled) append({nullptr, nullptr, sampleBegin, folded});
|
||||
sampled = true;
|
||||
sampledPresent = present;
|
||||
sampleBegin = begin;
|
||||
folded = {};
|
||||
}
|
||||
if (length < MinimumSlice) folded += length;
|
||||
else append({scope, stage, begin, length});
|
||||
}
|
||||
|
||||
Snapshot Collect() const {
|
||||
Snapshot snapshot{name, {slices.get(), used.load(std::memory_order_acquire)}, {}, false};
|
||||
for (const auto& entry : entries) {
|
||||
const auto* scope = entry.scope.load(std::memory_order_acquire);
|
||||
if (scope == nullptr) continue;
|
||||
snapshot.totals.push_back({scope, entry.stage.load(std::memory_order_relaxed), Clock::duration(entry.length.load(std::memory_order_relaxed)), entry.count.load(std::memory_order_relaxed)});
|
||||
}
|
||||
snapshot.dropped = dropped.load(std::memory_order_relaxed);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
std::atomic<const char*> scope{nullptr};
|
||||
std::atomic<const char*> stage{nullptr};
|
||||
std::atomic<Clock::rep> length{0};
|
||||
std::atomic<std::uint64_t> count{0};
|
||||
};
|
||||
|
||||
void append(const Slice& slice) {
|
||||
const auto index = used.load(std::memory_order_relaxed);
|
||||
if (index == capacity) {
|
||||
dropped.store(true, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
slices[index] = slice;
|
||||
used.store(index + 1, std::memory_order_release);
|
||||
}
|
||||
|
||||
void account(const char* scope, const char* stage, Clock::duration length) {
|
||||
const auto key = reinterpret_cast<std::uintptr_t>(scope) ^ (reinterpret_cast<std::uintptr_t>(stage) >> 3);
|
||||
const auto hash = static_cast<std::size_t>((static_cast<std::uint64_t>(key) * 0x9e3779b97f4a7c15ull) >> 55);
|
||||
for (std::size_t probe = 0; probe < entries.size(); ++probe) {
|
||||
auto& entry = entries[(hash + probe) % entries.size()];
|
||||
const auto* current = entry.scope.load(std::memory_order_relaxed);
|
||||
if (current == nullptr) {
|
||||
entry.stage.store(stage, std::memory_order_relaxed);
|
||||
entry.scope.store(scope, std::memory_order_release);
|
||||
} else if (current != scope || entry.stage.load(std::memory_order_relaxed) != stage) {
|
||||
continue;
|
||||
}
|
||||
entry.length.store(entry.length.load(std::memory_order_relaxed) + length.count(), std::memory_order_relaxed);
|
||||
entry.count.store(entry.count.load(std::memory_order_relaxed) + 1, std::memory_order_relaxed);
|
||||
return;
|
||||
}
|
||||
dropped.store(true, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
const std::string name;
|
||||
const std::atomic<std::uint64_t>& presents;
|
||||
const std::unique_ptr<Slice[]> slices;
|
||||
const std::size_t capacity;
|
||||
std::atomic<std::size_t> used{0};
|
||||
std::atomic<bool> dropped{false};
|
||||
std::array<Entry, 512> entries;
|
||||
bool sampled = false;
|
||||
std::uint64_t sampledPresent = 0;
|
||||
Clock::time_point sampleBegin{};
|
||||
Clock::duration folded{};
|
||||
};
|
||||
|
||||
struct TraceSummary {
|
||||
std::string commit;
|
||||
std::optional<bool> dirty;
|
||||
std::vector<std::pair<std::string, std::string>> environment;
|
||||
std::string gpu;
|
||||
std::string driver;
|
||||
std::string os;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::string window;
|
||||
std::uint64_t firstPresent = 0;
|
||||
double gpuBusyMs = 0;
|
||||
std::string trace;
|
||||
};
|
||||
|
||||
struct TraceGpuRange {
|
||||
const char* name;
|
||||
double beginNs;
|
||||
double endNs;
|
||||
};
|
||||
|
||||
struct TraceClockSample {
|
||||
double gpuNs = 0;
|
||||
std::chrono::steady_clock::time_point cpu{};
|
||||
};
|
||||
|
||||
class TraceFormat {
|
||||
public:
|
||||
using Clock = std::chrono::steady_clock;
|
||||
static constexpr std::array<std::string_view, 5> 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<unsigned char>(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<unsigned>(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<const TraceThread::Snapshot> threads, std::size_t presenter, std::span<const Clock::time_point> 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<unsigned long long>(process), static_cast<unsigned long long>(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<TraceThread::Slice> slices(thread.slices.begin(), thread.slices.end());
|
||||
std::stable_sort(slices.begin(), slices.end(), [](const auto& a, const auto& b) { return a.begin != b.begin ? a.begin < b.begin : a.length > b.length; });
|
||||
const auto folded = thread.name + " folded";
|
||||
for (const auto& slice : slices) {
|
||||
char text[64];
|
||||
if (slice.scope == nullptr) {
|
||||
event(folded, "C", slice.begin, tid);
|
||||
std::snprintf(text, sizeof text, ",\"args\":{\"ms\":%.3f}}", milliseconds(slice.length));
|
||||
} else {
|
||||
event(Name(slice.scope, slice.stage), "X", slice.begin, tid);
|
||||
std::snprintf(text, sizeof text, ",\"dur\":%.3f}", microseconds(slice.length));
|
||||
}
|
||||
output += text;
|
||||
}
|
||||
}
|
||||
for (std::size_t index = 0; index + 1 < presents.size(); ++index) {
|
||||
event("present", "i", presents[index], presenter + 1);
|
||||
output += ",\"s\":\"g\",\"args\":{\"present\":" + std::to_string(firstPresent + index) + "}}";
|
||||
}
|
||||
output += "\n]}\n";
|
||||
return output;
|
||||
}
|
||||
|
||||
static std::vector<TraceThread::Slice> GpuSlices(std::span<const TraceGpuRange> 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<double, std::nano>(last.cpu - first.cpu).count() / gpuSpan : 1.0;
|
||||
const auto map = [&](double gpuNs) { return first.cpu + std::chrono::duration_cast<Clock::duration>(std::chrono::duration<double, std::nano>((gpuNs - first.gpuNs) * scale)); };
|
||||
std::vector<TraceThread::Slice> slices;
|
||||
for (const auto& range : ranges) {
|
||||
const auto begin = map(range.beginNs);
|
||||
const auto end = map(range.endNs);
|
||||
if (end <= begin || begin < origin) continue;
|
||||
slices.push_back({range.name, nullptr, begin, end - begin});
|
||||
}
|
||||
return slices;
|
||||
}
|
||||
|
||||
static std::string Summary(const TraceSummary& summary, std::span<const TraceThread::Snapshot> threads, std::span<const Clock::time_point> 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<double> 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<std::size_t>(fraction * static_cast<double>(frames.size())))]; };
|
||||
const double perPresent = count != 0 ? 1.0 / static_cast<double>(count) : 0.0;
|
||||
|
||||
std::string output = "{\"commit\":";
|
||||
AppendString(output, summary.commit);
|
||||
output += ",\"dirty\":";
|
||||
output += summary.dirty.has_value() ? (*summary.dirty ? "true" : "false") : "null";
|
||||
output += ",\"env\":{";
|
||||
for (std::size_t index = 0; index < summary.environment.size(); ++index) {
|
||||
if (index != 0) output += ',';
|
||||
AppendString(output, summary.environment[index].first);
|
||||
output += ':';
|
||||
AppendString(output, summary.environment[index].second);
|
||||
}
|
||||
output += "},\"gpu\":";
|
||||
AppendString(output, summary.gpu);
|
||||
output += ",\"driver\":";
|
||||
AppendString(output, summary.driver);
|
||||
output += ",\"os\":";
|
||||
AppendString(output, summary.os);
|
||||
output += ",\"resolution\":";
|
||||
AppendString(output, std::to_string(summary.width) + "x" + std::to_string(summary.height));
|
||||
output += ",\"window\":{\"spec\":";
|
||||
AppendString(output, summary.window);
|
||||
output += ",\"first_present\":" + std::to_string(summary.firstPresent);
|
||||
output += ",\"ms\":" + number(milliseconds(span)) + "}";
|
||||
output += ",\"presents\":" + std::to_string(count);
|
||||
output += ",\"frame_ms\":{\"p50\":" + number(percentile(0.5)) + ",\"p90\":" + number(percentile(0.9)) + ",\"p99\":" + number(percentile(0.99)) + ",\"max\":" + number(frames.empty() ? 0.0 : frames.back()) + "}";
|
||||
output += ",\"gpu_busy_ms_per_present\":" + number(summary.gpuBusyMs * perPresent);
|
||||
|
||||
output += ",\"worker_busy_pct\":{";
|
||||
bool firstWorker = true;
|
||||
std::map<std::string, Clock::duration> phases;
|
||||
bool dropped = false;
|
||||
for (const auto& thread : threads) {
|
||||
dropped = dropped || thread.dropped;
|
||||
Clock::duration busy{};
|
||||
for (const auto& total : thread.totals) {
|
||||
const std::string_view scope = total.scope;
|
||||
if (total.stage == nullptr && std::find(WorkerBusyScopes.begin(), WorkerBusyScopes.end(), scope) != WorkerBusyScopes.end()) busy += total.length;
|
||||
if (scope != WorkerWaitScope) phases[Name(total.scope, total.stage)] += total.length;
|
||||
}
|
||||
if (!thread.name.starts_with(WorkerPrefix)) continue;
|
||||
if (!firstWorker) output += ',';
|
||||
firstWorker = false;
|
||||
AppendString(output, thread.name);
|
||||
output += ':' + number(span != Clock::duration{} ? 100.0 * static_cast<double>(busy.count()) / static_cast<double>(span.count()) : 0.0);
|
||||
}
|
||||
output += "},\"top_phases_ms_per_frame\":{";
|
||||
std::vector<std::pair<std::string, Clock::duration>> top(phases.begin(), phases.end());
|
||||
std::stable_sort(top.begin(), top.end(), [](const auto& a, const auto& b) { return a.second > b.second; });
|
||||
for (std::size_t index = 0; index < top.size() && index < TopPhases; ++index) {
|
||||
if (index != 0) output += ',';
|
||||
AppendString(output, top[index].first);
|
||||
output += ':' + number(milliseconds(top[index].second) * perPresent);
|
||||
}
|
||||
output += "},\"trace_dropped\":";
|
||||
output += dropped ? "true" : "false";
|
||||
output += ",\"trace\":";
|
||||
AppendString(output, summary.trace);
|
||||
output += "}\n";
|
||||
return output;
|
||||
}
|
||||
|
||||
private:
|
||||
static double microseconds(Clock::duration elapsed) {
|
||||
return std::chrono::duration<double, std::micro>(elapsed).count();
|
||||
}
|
||||
|
||||
static double milliseconds(Clock::duration elapsed) {
|
||||
return std::chrono::duration<double, std::milli>(elapsed).count();
|
||||
}
|
||||
|
||||
static std::string number(double value) {
|
||||
char text[32];
|
||||
std::snprintf(text, sizeof text, "%.3f", value);
|
||||
return text;
|
||||
}
|
||||
};
|
||||
|
||||
class PerformanceTrace {
|
||||
public:
|
||||
static bool Configured() {
|
||||
static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr;
|
||||
return configured;
|
||||
}
|
||||
|
||||
static void BindThread(const char* name, std::size_t capacity = TraceThread::DefaultCapacity);
|
||||
static void Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device);
|
||||
|
||||
static void GpuRange(const char* name, double beginNs, double endNs) {
|
||||
if (!Recording()) return;
|
||||
std::lock_guard lock(gpuMutex);
|
||||
if (gpuRanges.size() == gpuRanges.capacity()) {
|
||||
gpuDropped = true;
|
||||
return;
|
||||
}
|
||||
gpuRanges.push_back({name, beginNs, endNs});
|
||||
}
|
||||
|
||||
static bool Recording() {
|
||||
return recording.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
static TraceThread* Thread() {
|
||||
return local;
|
||||
}
|
||||
|
||||
static void StopOutput();
|
||||
|
||||
private:
|
||||
inline static std::atomic<bool> recording{false};
|
||||
inline static std::atomic<std::uint64_t> presentCount{0};
|
||||
inline static thread_local TraceThread* local = nullptr;
|
||||
inline static std::mutex gpuMutex;
|
||||
inline static std::vector<TraceGpuRange> gpuRanges;
|
||||
inline static bool gpuDropped = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PIPELINELINKOPTIMIZATION_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PIPELINELINKOPTIMIZATION_HPP
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
inline bool PipelineLinkTimeOptimizationUsable(VkDriverId driver) {
|
||||
return driver != VK_DRIVER_ID_NVIDIA_PROPRIETARY;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -81,7 +81,7 @@ inline bool DrawOpcode(std::uint32_t opcode) { return opcode == 0x27 || opcode =
|
|||
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 || header == 0xffff1000u; }
|
||||
inline bool FillerPacket(std::uint32_t header) { return (header >> 30u) == 2u; }
|
||||
inline std::size_t PacketWords(std::uint32_t header) { return FillerPacket(header) ? 1u : static_cast<std::size_t>((header >> 16u) & 0x3fffu) + 2u; }
|
||||
std::string_view UnsupportedReason(std::uint32_t header);
|
||||
void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue);
|
||||
|
|
@ -140,15 +140,12 @@ struct MemoryCopy {
|
|||
std::size_t bytes;
|
||||
};
|
||||
std::optional<MemoryCopy> DecodeMemoryCopy(std::span<const std::uint32_t> packet);
|
||||
std::uint32_t DmaSource(std::span<const std::uint32_t> packet);
|
||||
std::uint32_t DmaDestination(std::span<const std::uint32_t> 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<const std::uint32_t> packet, const QueueState& queue);
|
||||
std::array<std::uint32_t, 5> ReadDispatchArguments(std::uint64_t arguments, std::uint32_t initiator);
|
||||
std::array<std::uint32_t, 3> PartialGroupThreads(const std::array<std::uint32_t, 3>& groups, const std::array<std::uint32_t, 3>& numThreads);
|
||||
std::array<std::uint32_t, 5> ResolveDispatch(std::span<const std::uint32_t> packet, const QueueState& queue);
|
||||
DrawParameters ResolveDraw(std::span<const std::uint32_t> packet, const QueueState& queue);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTMODE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_PRESENTMODE_HPP
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <algorithm>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
inline VkPresentModeKHR SelectPresentMode(const char* requested, std::span<const VkPresentModeKHR> 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
|
||||
|
|
@ -57,8 +57,6 @@ public:
|
|||
return true;
|
||||
}
|
||||
|
||||
void Print(const char* text) { Print("%s", text); }
|
||||
|
||||
template<typename... TArgs>
|
||||
void Print(const char* format, TArgs... args) {
|
||||
std::array<char, 2048> local{};
|
||||
|
|
|
|||
|
|
@ -9,10 +9,8 @@
|
|||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -24,29 +22,6 @@ public:
|
|||
using mapped_type = std::uint32_t;
|
||||
using value_type = std::pair<const std::uint32_t, std::uint32_t>;
|
||||
|
||||
class Mask {
|
||||
public:
|
||||
explicit Mask(std::vector<std::uint64_t> 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<std::uint64_t> 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<std::uint32_t>(other); }
|
||||
Reference& operator|=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) | value; }
|
||||
Reference& operator&=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) & value; }
|
||||
Reference& operator^=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) ^ value; }
|
||||
Reference& operator+=(std::uint32_t value) { return *this = static_cast<std::uint32_t>(*this) + value; }
|
||||
private:
|
||||
Registers& owner;
|
||||
std::uint32_t offset;
|
||||
};
|
||||
|
||||
class const_iterator {
|
||||
public:
|
||||
using iterator_category = std::forward_iterator_tag;
|
||||
|
|
@ -81,18 +56,6 @@ 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<std::pair<std::uint32_t, std::uint32_t>> entries) {
|
||||
for (const auto& [offset, value] : entries) emplace(offset, value);
|
||||
}
|
||||
|
|
@ -104,48 +67,33 @@ public:
|
|||
const_iterator upper_bound(std::uint32_t offset) const { return {this, nextPresent(static_cast<std::size_t>(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 <class TVisit>
|
||||
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::size_t>(std::countr_zero(bits));
|
||||
visit(static_cast<std::uint32_t>(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<const_iterator, bool> 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<const_iterator, bool> 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};
|
||||
}
|
||||
Reference operator[](std::uint32_t offset) {
|
||||
if (!contains(offset)) emplace(offset, 0);
|
||||
return {*this, offset};
|
||||
std::uint32_t& operator[](std::uint32_t offset) {
|
||||
if (contains(offset)) return values[offset];
|
||||
return mark(offset) = 0;
|
||||
}
|
||||
Reference at(std::uint32_t offset) {
|
||||
std::uint32_t& at(std::uint32_t offset) {
|
||||
if (!contains(offset)) throw std::out_of_range("register is not set");
|
||||
return {*this, offset};
|
||||
return values[offset];
|
||||
}
|
||||
const std::uint32_t& at(std::uint32_t offset) const {
|
||||
if (!contains(offset)) throw std::out_of_range("register is not set");
|
||||
|
|
@ -153,7 +101,6 @@ 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;
|
||||
|
|
@ -166,31 +113,8 @@ public:
|
|||
}
|
||||
return a == end() && b == other.end();
|
||||
}
|
||||
std::uint64_t Fingerprint(const std::shared_ptr<const Mask>& 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<std::uint64_t>(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()) {
|
||||
|
|
@ -213,11 +137,9 @@ private:
|
|||
std::vector<std::uint32_t> values;
|
||||
std::vector<std::uint64_t> present;
|
||||
std::size_t entries = 0;
|
||||
mutable std::shared_ptr<const Mask> mask;
|
||||
mutable std::uint64_t fingerprint = 0;
|
||||
};
|
||||
|
||||
inline Registers BuildInitialContextRegisters() {
|
||||
inline Registers InitialContextRegisters() {
|
||||
Registers result{
|
||||
{0x200, 0}, {0x201, 0}, {0x202, 0xcc0010}, {0x203, 0},
|
||||
{0x204, 0}, {0x205, 0}, {0x206, 1087}, {0x207, 0},
|
||||
|
|
@ -234,13 +156,11 @@ inline Registers BuildInitialContextRegisters() {
|
|||
{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);
|
||||
|
|
@ -250,11 +170,6 @@ inline Registers BuildInitialContextRegisters() {
|
|||
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;
|
||||
|
|
@ -262,12 +177,11 @@ struct Predication {
|
|||
};
|
||||
|
||||
struct QueueState {
|
||||
static constexpr std::size_t ConstantRamWords = 0x3000;
|
||||
Registers shader;
|
||||
Registers context = InitialContextRegisters();
|
||||
Registers userConfig{{0x24a, 0}, {0x24b, 0}};
|
||||
std::optional<Registers> savedContext;
|
||||
std::vector<std::uint32_t> constantRam;
|
||||
std::array<std::uint32_t, 0x3000> constantRam{};
|
||||
std::uint64_t indexBase = 0;
|
||||
std::uint64_t drawIndirectBase = 0;
|
||||
std::uint64_t dispatchIndirectBase = 0;
|
||||
|
|
@ -280,17 +194,6 @@ struct QueueState {
|
|||
void ClearContext() {
|
||||
context = InitialContextRegisters();
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> ConstantRam() const {
|
||||
static const std::array<std::uint32_t, ConstantRamWords> unwritten{};
|
||||
if (constantRam.empty()) return unwritten;
|
||||
return constantRam;
|
||||
}
|
||||
|
||||
std::span<std::uint32_t> WritableConstantRam() {
|
||||
if (constantRam.empty()) constantRam.resize(ConstantRamWords);
|
||||
return constantRam;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ struct DrawRecipe {
|
|||
std::optional<Graphics::State> masked;
|
||||
std::set<std::uint32_t> fragmentOutputs;
|
||||
VkPipelineStageFlags shaderStages = 0;
|
||||
bool hasDepth = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,6 @@ private:
|
|||
};
|
||||
|
||||
static bool read(void* context, std::uint64_t address, std::uint32_t* value);
|
||||
static bool accessible(void* context, std::uint64_t address, std::uint64_t bytes);
|
||||
Page& page(std::uint64_t base);
|
||||
|
||||
// Regions given at construction (the registered shader's code and header), referenced as given:
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue