Merge branch 'main' into feat/relinker-frame-frameless-leaves-next

This commit is contained in:
Yunus Shaikh committed 2026-10-23 03:13:53 +00:00
commit ff16118b63
2568 files changed
+288890 -16375

No files matched your search

+59
View File
@@ -0,0 +1,59 @@
name: Bug report
description: A title, homebrew or test stops, crashes or renders wrongly.
labels: [bug]
body:
- type: markdown
attributes:
value: |
Search open and closed issues and pull requests for the error text first, and comment there if it is already reported.
Game bring-up progress belongs in [Discussions](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up), one thread per title.
A slow scene or a change in speed goes into the [Performance report](https://github.com/boykopovar/AnyPS5/issues/new?template=performance.yml) form, which asks for a [performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace) and its `tools/trace_digest.py` digest.
- type: input
id: summary
attributes:
label: What happens
description: One sentence, with the exact error text if there is one.
validations:
required: true
- type: input
id: title
attributes:
label: Title, test or build step
description: Title ID and content version (for example PPSAxxxxx 01.000.000), a homebrew with its source, the ctest name, or the build step that fails.
validations:
required: true
- type: input
id: commit
attributes:
label: AnyPS5 commit
description: The `main` commit that was built, or the release that was used, and any pull requests merged on top.
validations:
required: true
- type: input
id: system
attributes:
label: System
description: OS, GPU and driver version, and the compiler if you built it yourself.
placeholder: Arch Linux, GCC 16.2.1, RTX 5080 (NVIDIA 615.78.08)
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to reproduce
description: The commands that were run, environment variables included, and how long it takes to stop.
validations:
required: true
- type: textarea
id: log
attributes:
label: Log
description: The lines around the stop.
render: text
validations:
required: true
- type: textarea
id: notes
attributes:
label: What was checked
description: Other devices or commits tried, and whether it is the same every run. Attach the full log and screenshots here.
+8
View File
@@ -0,0 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Game bring-up
url: https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up
about: Progress of one title, one thread per title ID
- name: Question
url: https://github.com/boykopovar/AnyPS5/discussions/categories/q-a
about: Building, converting and running
+3
View File
@@ -0,0 +1,3 @@
self-hosted-runner:
labels:
- anyps5
+1
View File
@@ -13,6 +13,7 @@
- [ ] New third-party code is a submodule built from source
- [ ] Generic behaviour, not specific to one title
- [ ] Depends on: <!-- #PR, or none -->
- [ ] Fixes: <!-- issue it closes as Fixes #N, 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 -->
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
state=$RUNNER_TEMP/area-state.json
[ -s "$state" ] || echo '{}' > "$state"
config=$(gh api "repos/$GITHUB_REPOSITORY/contents/.github/labeler.yml" --jq .sha)
gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 200 --json number,headRefOid \
gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 1000 --json number,headRefOid \
--jq "map({key: (.number | tostring), value: (.headRefOid + \"-$config\")}) | from_entries" > "$RUNNER_TEMP/area-open.json"
jq -r --slurpfile old "$state" 'to_entries[] | select($old[0][.key] != .value) | .key' "$RUNNER_TEMP/area-open.json" | head -n 40 > "$RUNNER_TEMP/area-todo"
jq -c --slurpfile open "$RUNNER_TEMP/area-open.json" --rawfile todo "$RUNNER_TEMP/area-todo" \
+408 -79
View File
@@ -9,7 +9,7 @@ on:
workflow_dispatch:
inputs:
pr:
description: Pull request numbers to build together, separated by spaces or commas (up to 8)
description: Pull request numbers to build together, separated by spaces or commas (up to 40)
required: true
type: string
linux:
@@ -17,13 +17,23 @@ on:
required: false
default: true
type: boolean
full:
description: Run the full Linux and Windows builds (off runs only the relinker builds and tests on Linux, Windows and macOS)
required: false
default: true
type: boolean
select:
description: Build and run only the tests whose build inputs the pull requests change (push builds of main always run every test)
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }}
cancel-in-progress: ${{ github.event_name != 'push' }}
cancel-in-progress: true
env:
CONFIGURE: >-
@@ -47,29 +57,129 @@ jobs:
register-context: ${{ steps.gate.outputs.register-context }}
write-watch: ${{ steps.gate.outputs.write-watch }}
full: ${{ steps.gate.outputs.full }}
relinker-hosts: ${{ steps.gate.outputs.relinker-hosts }}
steps:
- uses: actions/github-script@v8
id: gate
env:
PR_NUMBER: ${{ inputs.pr }}
LINUX_BUILD: ${{ inputs.linux }}
FULL_BUILD: ${{ inputs.full }}
with:
script: |
const { owner, repo } = context.repo;
const workflow = '.github/workflows/build.yml';
const libc = 'core/libs/prx/libc/';
const jobOfLine = (text) => {
let inJobs = false;
let job = '';
return text.split('\n').map((line) => {
if (/^\S/.test(line)) {
inJobs = /^jobs:\s*$/.test(line);
job = '';
} else if (inJobs) {
job = /^ ([\w-]+):\s*$/.exec(line)?.[1] ?? job;
}
return job;
});
};
const patchedJobs = (patch, text) => {
const jobs = jobOfLine(text);
const touched = new Set();
let line = 1;
for (const row of patch.split('\n')) {
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(row);
if (hunk) {
line = Number(hunk[1]);
} else if (row.startsWith('+')) {
touched.add(jobs[line - 1] ?? '');
line += 1;
} else if (row.startsWith('-')) {
touched.add(jobs[line - 2] ?? '').add(jobs[line - 1] ?? '');
} else if (row.startsWith(' ')) {
line += 1;
}
}
return touched;
};
const workflowJobs = new Set();
const readWorkflow = async (files, ref) => {
const file = files.find((file) => file.filename === workflow || file.previous_filename === workflow);
if (!file) {
return;
}
if (file.filename !== workflow || !file.patch) {
workflowJobs.add('*');
return;
}
try {
const { data } = await github.rest.repos.getContent({ owner, repo, path: workflow, ref });
const text = Buffer.from(data.content, 'base64').toString('utf8');
patchedJobs(file.patch, text).forEach((job) => workflowJobs.add(job));
} catch (error) {
core.warning(`Could not read ${workflow} at ${ref}, so every test job runs: ${error.message}`);
workflowJobs.add('*');
}
};
const select = (paths, all) => {
const touches = (...prefixes) => all ||
paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix));
const edits = (job) => ['*', '', 'eligibility', job].some((name) => workflowJobs.has(name));
return {
relinker: edits('relinker') || touches(
'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp',
'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp',
'tools/', 'cmake/', 'CMakeLists.txt'
),
registerContext: edits('register-context') ||
touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc),
writeWatch: edits('write-watch') || touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc),
};
};
const relinkerHosts = (full, linux) => JSON.stringify([
...(full && linux ? [] : [{ os: 'ubuntu-latest', compiler: 'g++' }]),
...(full ? [] : [{ os: 'windows-latest', compiler: 'g++' }]),
{ os: 'macos-latest', compiler: 'clang++' },
{ os: 'macos-latest', compiler: 'clang++', architectures: 'x86_64' },
]);
if (context.eventName === 'push') {
if (context.ref !== 'refs/heads/main') {
throw new Error('Automatic full builds only run on main');
}
let paths = [];
let all = true;
if (!/^0+$/.test(context.payload.before)) {
try {
const { data } = await github.rest.repos.compareCommitsWithBasehead({
owner, repo, basehead: `${context.payload.before}...${context.sha}`
});
const files = data.files ?? [];
all = files.length >= 300;
paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
await readWorkflow(files, context.sha);
} catch (error) {
core.warning(`Could not list the files of this push, so every test job runs: ${error.message}`);
}
}
const run = select(paths, all);
core.setOutput('sha', context.sha);
core.setOutput('pr', '');
core.setOutput('head', '');
core.setOutput('relinker', true);
core.setOutput('register-context', true);
core.setOutput('write-watch', true);
core.setOutput('relinker', run.relinker);
core.setOutput('register-context', run.registerContext);
core.setOutput('write-watch', run.writeWatch);
core.setOutput('full', true);
await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write();
core.setOutput('relinker-hosts', relinkerHosts(true, true));
await core.summary.addRaw(
`Building main at ${context.sha}: full builds enabled, ` +
`macOS relinker ${run.relinker ? 'enabled' : 'skipped'}, ` +
`register context tests ${run.registerContext ? 'enabled' : 'skipped'}, write watch tests ${run.writeWatch ? 'enabled' : 'skipped'}`
).write();
return;
}
const { owner, repo } = context.repo;
const manual = context.eventName === 'workflow_dispatch';
const linux = process.env.LINUX_BUILD !== 'false';
const full = manual && process.env.FULL_BUILD !== 'false';
if (manual) {
for (const username of new Set([context.actor, process.env.GITHUB_TRIGGERING_ACTOR])) {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username });
@@ -77,8 +187,8 @@ jobs:
throw new Error('Only collaborators with write access can start or rerun builds');
}
}
if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,7}$/.test(process.env.PR_NUMBER.trim())) {
throw new Error('Enter up to 8 positive pull request numbers separated by spaces or commas');
if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,39}$/.test(process.env.PR_NUMBER.trim())) {
throw new Error('Enter up to 40 positive pull request numbers separated by spaces or commas');
}
}
const numbers = manual
@@ -90,7 +200,6 @@ jobs:
const pulls = [];
const paths = [];
let truncated = false;
let relinker = manual;
for (const pull_number of numbers) {
let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) {
@@ -110,28 +219,37 @@ jobs:
if (pr.mergeable === false) {
throw new Error(`Pull request #${pull_number} does not merge cleanly into main; rebase it first`);
}
if (!manual) {
const { data: commits } = await github.rest.repos.listCommits({
owner, repo, sha: 'main', author: pr.user.login, per_page: 1
});
relinker = commits.length > 0;
}
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 });
truncated ||= files.length >= 3000;
paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)));
await readWorkflow(files, pr.head.sha);
pulls.push({ number: pull_number, head: pr.head.sha });
}
const touches = (...prefixes) => truncated ||
paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix));
const workflow = '.github/workflows/build.yml';
const libc = 'core/libs/prx/libc/';
relinker = relinker && touches(
'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp',
'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp',
'tools/', 'cmake/', 'CMakeLists.txt', workflow
);
const registerContext = touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow);
const writeWatch = touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow);
const run = select(paths, truncated);
let relinker = run.relinker;
if (relinker && !manual) {
const author = context.payload.pull_request.user.login;
const { data: commits } = await github.rest.repos.listCommits({
owner, repo, sha: 'main', author, per_page: 1
});
relinker = commits.length > 0;
if (!relinker) {
try {
const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:pr is:merged base:main author:${author}`,
per_page: 1
});
if (mergedPRs.incomplete_results) {
throw new Error('the search results are incomplete');
}
relinker = mergedPRs.total_count > 0;
} catch (error) {
core.warning(`Skipping the relinker: could not search the merged pull requests of ${author}: ${error.message}`);
}
}
}
const registerContext = run.registerContext;
const writeWatch = run.writeWatch;
const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' });
core.setOutput('sha', main.commit.sha);
core.setOutput('pr', pulls.map((pull) => pull.number).join(' '));
@@ -139,12 +257,13 @@ jobs:
core.setOutput('relinker', relinker);
core.setOutput('register-context', registerContext);
core.setOutput('write-watch', writeWatch);
core.setOutput('full', manual);
core.setOutput('full', full);
core.setOutput('relinker-hosts', relinkerHosts(full, linux));
const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`);
merged.forEach((line) => core.info(line));
await core.summary.addRaw(
`${merged.join(', then ')}: full builds ${manual ? 'enabled' : 'disabled'}, ` +
`relinker ${relinker ? 'enabled' : 'skipped: no relinker file changed or the author has no commit in main'}, ` +
`${merged.join(', then ')}: full builds ${full ? 'enabled' : 'disabled'}, ` +
`relinker ${relinker ? (full ? (linux ? 'enabled on macOS (the full builds run the Linux and Windows relinker tests)' : 'enabled on Linux and macOS (the full Windows build runs the Windows relinker tests)') : 'enabled') : 'skipped: no relinker file changed, or no commit or merged pull request in main was found for the author'}, ` +
`register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}`
).write();
@@ -156,13 +275,7 @@ jobs:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
compiler: g++
- os: windows-latest
compiler: g++
- os: macos-latest
compiler: clang++
include: ${{ fromJSON(needs.eligibility.outputs.relinker-hosts) }}
env:
WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7
WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip
@@ -184,7 +297,7 @@ jobs:
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
@@ -202,9 +315,40 @@ jobs:
7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null
- if: runner.os == 'Windows'
run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON
- if: ${{ matrix.architectures == 'x86_64' }}
run: sudo softwareupdate --install-rosetta --agree-to-license
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON -DCMAKE_OSX_ARCHITECTURES=${{ matrix.architectures }}
- run: cmake --build build-relinker --parallel 3
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4
relinker-asan:
needs: eligibility
if: ${{ needs.eligibility.outputs.relinker == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
env:
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
PR_HEAD: ${{ needs.eligibility.outputs.head }}
run: |
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
- run: cmake -S . -B build-relinker-asan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ -DANYPS5_RELINKER_ONLY=ON -DANYPS5_ENABLE_ASAN=ON -DBUILD_TESTING=ON
- run: cmake --build build-relinker-asan --parallel 4
- run: ctest --test-dir build-relinker-asan --output-on-failure --timeout 120 --label-regex '^tls_function_coverage$' --no-tests=error
register-context:
needs: eligibility
@@ -218,6 +362,25 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
shell: bash
env:
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
PR_HEAD: ${{ needs.eligibility.outputs.head }}
run: |
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
- run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }}
- run: cmake --build build/register-context --parallel
- run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error
@@ -233,6 +396,25 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
shell: bash
env:
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
PR_HEAD: ${{ needs.eligibility.outputs.head }}
run: |
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
- run: cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release
- run: cmake --build build/write-watch --parallel
- run: ctest --test-dir build/write-watch --output-on-failure --repeat until-fail:3 --no-tests=error
@@ -240,8 +422,14 @@ jobs:
linux:
needs: eligibility
if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 30
runs-on: [self-hosted, linux, x64, anyps5]
timeout-minutes: 60
env:
CCACHE_DIR: /var/cache/anyps5/ccache
CCACHE_MAXSIZE: 30G
CCACHE_COMPILERCHECK: content
MESA_SHADER_CACHE_DIR: /var/cache/anyps5/mesa
MESA_SHADER_CACHE_MAX_SIZE: 4G
steps:
- uses: actions/checkout@v7
with:
@@ -258,46 +446,107 @@ jobs:
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
fetch_options=()
if [ "$(git rev-parse --is-shallow-repository)" = true ]; then fetch_options+=(--unshallow); fi
git fetch --no-tags "${fetch_options[@]}" origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
git submodule update --init --recursive
- uses: hendrikmuhs/[email protected]
with:
key: linux
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
evict-old-files: job
- name: Install lavapipe and the SDL audio, XRandR and Wayland development files
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev libwayland-dev libxkbcommon-dev libegl-dev libdecor-0-dev
- id: mesa
- name: Prepare persistent caches
run: |
echo "version=$(dpkg-query -W -f='${Version}' mesa-vulkan-drivers)" >> "$GITHUB_OUTPUT"
mkdir -p "$CCACHE_DIR" "$MESA_SHADER_CACHE_DIR"
test -w "$CCACHE_DIR" && test -w "$MESA_SHADER_CACHE_DIR"
ccache --zero-stats
ccache --show-config
dpkg-query -W mesa-vulkan-drivers
grep -m1 '^model name' /proc/cpuinfo
echo "cpu=$(grep -E '^(vendor_id|cpu family|model|flags)[[:space:]]*:' /proc/cpuinfo | sort -u | sha256sum | cut -c1-12)" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@v6
with:
path: .mesa-shader-cache
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
restore-keys: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON
- name: Install the wild linker
shell: bash
env:
WILD_VERSION: 0.10.0
WILD_SHA256: 641265506a7c06cfb03181b8916ab663ec8407855db6d4db7f8450667d105283
run: |
tools=$RUNNER_TOOL_CACHE/wild
mkdir -p "$tools" 2>/dev/null && test -w "$tools" || { tools=$RUNNER_TEMP/wild; mkdir -p "$tools"; }
dir=$tools/$WILD_VERSION
tmp=
probe=$(mktemp -d "$RUNNER_TEMP/wild-probe.XXXXXX")
trap 'rm -rf "$tmp" "$probe"' EXIT
if [ ! -x "$dir/bin/ld" ]; then
tmp=$(mktemp -d "$tools/.wild.XXXXXX")
name=wild-linker-$WILD_VERSION-x86_64-unknown-linux-gnu
curl -fsSL --retry 3 -o "$tmp/$name.tar.gz" "https://github.com/davidlattimore/wild/releases/download/$WILD_VERSION/$name.tar.gz"
echo "$WILD_SHA256 $tmp/$name.tar.gz" | sha256sum -c -
tar -xzf "$tmp/$name.tar.gz" -C "$tmp"
mkdir -p "$tmp/out/bin"
mv "$tmp/$name/wild" "$tmp/out/bin/wild"
ln -s wild "$tmp/out/bin/ld"
mv -T "$tmp/out" "$dir" 2>/dev/null || test -x "$dir/bin/ld"
fi
echo 'int main(void) { return 0; }' > "$probe/main.c"
gcc "-B$dir/bin" "$probe/main.c" -o "$probe/main"
readelf -p .comment "$probe/main" | grep -F "Linker: Wild $WILD_VERSION"
echo "LINKER_FLAGS=-B$dir/bin" >> "$GITHUB_ENV"
- name: Size parallelism to what the shared host has free
run: |
cpus=$(nproc)
busy=$(awk '{ printf "%d", $1 + 0.5 }' /proc/loadavg)
mem_mb=$(awk '/^MemTotal:/ { print int($2 / 1024) }' /proc/meminfo)
slots=$(( (mem_mb - 2048) / 1024 ))
slots=$(( slots < 1 ? 1 : slots ))
tests=$(( cpus - busy ))
tests=$(( tests < 1 ? 1 : tests ))
slot_dir=$CCACHE_DIR/compile-slots
mkdir -p -m 1777 "$slot_dir"
test -w "$slot_dir"
echo "cpus=$cpus load=$busy mem_total_mb=$mem_mb -> build jobs $cpus, cold compiles $slots per host, tests $tests"
echo "CMAKE_BUILD_PARALLEL_LEVEL=$cpus" >> "$GITHUB_ENV"
echo "CTEST_PARALLEL_LEVEL=$tests" >> "$GITHUB_ENV"
echo "CCACHE_PREFIX=$GITHUB_WORKSPACE/tools/compile_slot.sh" >> "$GITHUB_ENV"
echo "ANYPS5_COMPILE_SLOT_DIR=$slot_dir" >> "$GITHUB_ENV"
echo "ANYPS5_COMPILE_SLOTS=$slots" >> "$GITHUB_ENV"
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS"
- name: Check that every test executable is registered with ctest
run: python3 tools/check_test_registration.py build
- run: cmake --build build
- run: cmake --build build --target libs
- run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
- if: ${{ inputs.select }}
uses: actions/cache/restore@v6
with:
path: test-graph.json
key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }}
restore-keys: test-graph-${{ runner.os }}-
- name: Select the tests the change reaches
if: ${{ inputs.select }}
env:
BASE: ${{ needs.eligibility.outputs.sha }}
run: |
git diff --name-only "$BASE" HEAD > changed.txt
python3 tools/select_tests.py select build test-graph.json changed.txt
- id: build
run: |
if [ -f build/select.ninja ]; then ninja -C build -f select.ninja -j "$CMAKE_BUILD_PARALLEL_LEVEL" -l "$(nproc)" selected; else cmake --build build -- -l "$(nproc)"; fi
- id: libs
run: cmake --build build --target libs -- -l "$(nproc)"
- run: python3 tools/select_tests.py ctest build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml
env:
ANYPS5_REQUIRE_VULKAN: 1
GALLIUM_OVERRIDE_CPU_CAPS: avx
MESA_SHADER_CACHE_DIR: ${{ github.workspace }}/.mesa-shader-cache
- if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' && !cancelled() }}
- run: python3 tools/check_skips.py build/ctest.xml
- name: Write the test graph of main
if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }}
continue-on-error: true
run: python3 tools/select_tests.py dump build test-graph.json
- if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }}
continue-on-error: true
uses: actions/cache/save@v6
with:
path: .mesa-shader-cache
key: mesa-${{ steps.mesa.outputs.version }}-${{ steps.mesa.outputs.cpu }}-${{ github.sha }}
- run: python3 tools/check_skips.py build/ctest.xml
path: test-graph.json
key: test-graph-${{ runner.os }}-${{ github.sha }}
- name: Report compiler cache statistics
if: ${{ !cancelled() }}
run: ccache --show-stats
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
shell: bash
@@ -314,7 +563,6 @@ jobs:
env:
WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7
WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip
SSL_CERT_FILE: C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt
defaults:
run:
shell: bash
@@ -323,7 +571,6 @@ jobs:
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
submodules: recursive
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
shell: bash
@@ -334,13 +581,41 @@ jobs:
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("refs/pull/$number/head:refs/remotes/pull/$number"); done
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
git submodule update --init --recursive
- id: submodules
name: Key the submodules on their commits
run: |
echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT"
- id: submodule-cache
uses: actions/cache/restore@v6
with:
path: ${{ runner.temp }}/submodule-gitdirs.tar
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
- name: Check out the submodules
run: |
if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi
git submodule update --init --recursive --depth 1 --jobs 8
- if: steps.submodule-cache.outputs.cache-hit != 'true'
run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules
- if: steps.submodule-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: ${{ runner.temp }}/submodule-gitdirs.tar
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
- name: Point the WinLibs CMake at the CA bundle of Git for Windows
run: |
for prefix in ucrt64 mingw64; do
bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt"
if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi
done
echo "::error::No CA bundle found in Git for Windows"; exit 1
- id: toolchain
uses: actions/cache@v6
with:
@@ -353,21 +628,74 @@ jobs:
- run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
- uses: hendrikmuhs/[email protected]
with:
key: windows
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
key: ${{ github.event_name == 'workflow_dispatch' && 'dispatch-windows' || 'windows' }}
restore-keys: ${{ github.event_name == 'workflow_dispatch' && 'windows' || '' }}
save: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
evict-old-files: job
- id: ffmpeg-cache
uses: actions/cache/restore@v6
with:
path: build/externals
key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }}
- id: seed-cache
uses: actions/cache/restore@v6
with:
path: configure-seed.cmake
key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', '.github/workflows/build.yml') }}
- shell: cmd
run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
run: if exist configure-seed.cmake (cmake -C configure-seed.cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") else (cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown")
- if: steps.seed-cache.outputs.cache-hit != 'true'
shell: cmd
run: python tools/configure_seed.py build/CMakeCache.txt configure-seed.cmake
- if: steps.seed-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: configure-seed.cmake
key: ${{ steps.seed-cache.outputs.cache-primary-key }}
- if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: build/externals
key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }}
- name: Check that every test executable is registered with ctest
shell: cmd
run: python tools/check_test_registration.py build
- shell: cmd
run: cmake --build build
- shell: cmd
- if: ${{ inputs.select }}
uses: actions/cache/restore@v6
with:
path: test-graph.json
key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }}
restore-keys: test-graph-${{ runner.os }}-
- name: Select the tests the change reaches
if: ${{ inputs.select }}
env:
BASE: ${{ needs.eligibility.outputs.sha }}
shell: cmd
run: |
git diff --name-only %BASE% HEAD > changed.txt
python tools/select_tests.py select build test-graph.json changed.txt
- id: build
shell: cmd
run: if exist build\select.ninja (ninja -C build -f select.ninja selected) else (cmake --build build)
- id: libs
shell: cmd
run: cmake --build build --target libs
- shell: cmd
run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
- run: python tools/check_skips.py build/ctest.xml --job windows
- name: Write the test graph of main
if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }}
continue-on-error: true
shell: cmd
run: python tools/select_tests.py dump build test-graph.json
- if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }}
continue-on-error: true
uses: actions/cache/save@v6
with:
path: test-graph.json
key: test-graph-${{ runner.os }}-${{ github.sha }}
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
shell: bash
@@ -375,3 +703,4 @@ jobs:
[ -f ctest.log ] || exit 0
awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t }
END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log
+3 -4
View File
@@ -18,15 +18,14 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
ref: ${{ github.event.pull_request.base.ref }}
fetch-depth: 0
persist-credentials: false
- name: Fetch pull request objects without checking out its code
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: git fetch --no-tags origin "$HEAD_SHA"
- name: Check conventions with the trusted base script
- name: Check conventions with the current base branch script
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 -I tools/check_conventions.py --base "$BASE_SHA" --head "$HEAD_SHA"
run: python3 -I tools/check_conventions.py --base HEAD --head "$HEAD_SHA"
+44
View File
@@ -0,0 +1,44 @@
name: Fix label
on:
schedule:
- cron: '45 * * * *'
workflow_dispatch:
permissions: {}
concurrency:
group: fix-label
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: read
pull-requests: write
env:
GH_TOKEN: ${{ github.token }}
LABEL: fix error
steps:
- run: |
gh api graphql --paginate -f owner="${GITHUB_REPOSITORY%/*}" -f name="${GITHUB_REPOSITORY#*/}" -f query='
query($owner: String!, $name: String!, $endCursor: String) {
repository(owner: $owner, name: $name) {
pullRequests(states: OPEN, first: 100, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes { number closingIssuesReferences { totalCount } labels(first: 100) { nodes { name } } }
}
}
}' --jq '.data.repository.pullRequests.nodes[]
| (.closingIssuesReferences.totalCount > 0) as $fix
| ([.labels.nodes[].name] | index(env.LABEL) != null) as $has
| if $fix and ($has | not) then "add \(.number)" elif $has and ($fix | not) then "remove \(.number)" else empty end' > "$RUNNER_TEMP/fix-todo"
label=$(jq -rn 'env.LABEL | @uri')
while read -r action number; do
if [ "$action" = add ]; then
gh api "repos/$GITHUB_REPOSITORY/issues/$number/labels" -f "labels[]=$LABEL" --silent
else
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/$label" --silent
fi
done < "$RUNNER_TEMP/fix-todo"
+4
View File
@@ -28,6 +28,9 @@ jobs:
force_label ci 5319e7 '.github'
force_label agc 1d76db 'libSceAgc*'
force_label shader 1d76db 'core/shader'
force_label 'unblocks title' 0e8a16 'Names a title ID on the Unblocks line'
force_label perf fef2c0 'Performance change'
force_label 'perf numbers missing' e99695 'perf pull request without a before and after summary line'
label relinker 5319e7 'core/relinker'
label libc fbca04 'libc'
@@ -35,4 +38,5 @@ jobs:
label prx c5def5 'Other PRX libraries'
label build bfd4f2 'CMake, submodules, 3rdparty'
label docs 0075ca 'Documentation'
label 'fix error' d73a4a 'Closes an issue (Fixes #N)'
label stale ededed 'Conflicting with main and no activity for 7 days'
+40
View File
@@ -0,0 +1,40 @@
name: Pages progress
on:
push:
branches: [main]
paths:
- core/libs/prx/**
- core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp
permissions:
contents: read
actions: write
pages: read
concurrency:
group: pages-progress
cancel-in-progress: true
jobs:
progress:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Deploy Pages if the progress changed
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
python3 tools/progress.py "$RUNNER_TEMP/progress"
site=$(gh api "repos/$GH_REPO/pages" --jq .html_url)
if curl -fsSL "${site%/}/progress.json?$GITHUB_SHA" -o "$RUNNER_TEMP/deployed.json" &&
cmp -s "$RUNNER_TEMP/deployed.json" "$RUNNER_TEMP/progress/progress.json"; then
echo "Progress unchanged, Pages not deployed" >> "$GITHUB_STEP_SUMMARY"
else
gh workflow run pages.yml --ref main
echo "Progress changed, Pages deployment dispatched" >> "$GITHUB_STEP_SUMMARY"
fi
+14 -13
View File
@@ -3,6 +3,18 @@ name: Pages
on:
push:
branches: [main]
paths:
- .github/workflows/pages.yml
- CMakeLists.txt
- core/relinker/**
- demo/web/**
- tools/build_web.py
- tools/package_release.py
- tools/nid_names.py
- tools/progress.py
- tools/progress_history.py
- tools/rdna_isa.txt
- tools/test_web.py
workflow_dispatch:
release:
types: [published]
@@ -31,21 +43,10 @@ jobs:
with:
ref: main
fetch-depth: 0
- name: Build web converter
shell: bash
run: |
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk"
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.3
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm --target relinker_web --parallel 2
- name: Test web converter
run: python3 tools/test_web.py --build build-wasm
- name: Download latest release libraries
- name: Download matching release converter and libraries
env:
GITHUB_TOKEN: ${{ github.token }}
run: python3 tools/build_web.py --build build-wasm --libraries
run: python3 tools/build_web.py --build build-wasm --release
- run: python3 tools/progress.py build-wasm/web
- run: python3 tools/progress_history.py build-wasm/web
- uses: actions/upload-pages-artifact@v5
+25 -1
View File
@@ -27,6 +27,27 @@ jobs:
if (pr.state !== 'open' || pr.base.ref !== 'main') {
return;
}
if (context.payload.action === 'reopened') {
let maintainer = false;
try {
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({
owner, repo, username: context.payload.sender.login
});
maintainer = ['admin', 'maintain', 'write'].includes(access.role_name ?? access.permission);
} catch (error) {
core.warning(`Could not check the permission of ${context.payload.sender.login}, so the open PR limit applies: ${error.message}`);
}
if (maintainer) {
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, name: 'open PR limit' });
} catch (error) {
if (error.status !== 404) {
core.warning(`Could not remove the open PR limit label: ${error.message}`);
}
}
return;
}
}
const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:pr is:merged base:main author:${pr.user.login}`,
per_page: 1
@@ -42,7 +63,10 @@ jobs:
const openPRs = await github.paginate(github.rest.pulls.list, {
owner, repo, state: 'open', base: 'main', per_page: 100
});
const open = new Set(openPRs.filter((p) => p.user.login === pr.user.login).map((p) => p.number));
const reopened = context.payload.action === 'reopened';
const open = new Set(openPRs
.filter((p) => p.user.login === pr.user.login && (reopened || p.number < pull_number))
.map((p) => p.number));
open.add(pull_number);
if (open.size <= allowedOpen) {
return;
+35
View File
@@ -0,0 +1,35 @@
name: PR labels
on:
pull_request_target:
branches: [main]
types: [opened, edited, reopened, synchronize]
schedule:
- cron: '45 * * * *'
workflow_dispatch:
permissions: {}
concurrency:
group: pr-labels-${{ github.event.pull_request.number || 'all' }}
jobs:
labels:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
pull-requests: write
issues: write
steps:
- uses: actions/checkout@v7
- env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [ -n "$PR_NUMBER" ]; then
python3 -I tools/pr_labels.py --pr "$PR_NUMBER"
else
python3 -I tools/pr_labels.py --all
fi
+3 -2
View File
@@ -38,11 +38,12 @@ jobs:
record() { jq --arg pr "$1" --arg h "$2" '.[$pr] = $h' "$state" > "$state.new" && mv "$state.new" "$state"; }
shopt -s nullglob
marker='<!-- pr-overlap -->'
unpinned() { sed -E 's#/blob/[0-9a-f]{40}/#/blob/main/#g'; }
prev=
for file in "$out"/*.md; do
[ -z "$prev" ] || record "$prev" "$hash"
pr=$(basename "$file" .md)
hash=$(sha256sum "$file" | cut -d' ' -f1)
hash=$(unpinned < "$file" | sha256sum | cut -d' ' -f1)
prev=
[ "$(jq -r --arg pr "$pr" '.[$pr] // empty' "$state")" = "$hash" ] && continue
prev=$pr
@@ -50,7 +51,7 @@ jobs:
--jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | [.id, .body] | @json" | head -n1)
id=$(jq -r '.[0] // empty' <<< "${comment:-null}")
if [ -s "$file" ]; then
[ "$(jq -r '.[1]' <<< "${comment:-null}")" = "$(cat "$file")" ] && continue
[ "$(jq -r '.[1]' <<< "${comment:-null}" | unpinned)" = "$(unpinned < "$file")" ] && continue
if [ -n "$id" ]; then
gh api -X PATCH "repos/$GH_REPO/issues/comments/$id" -F body=@"$file" > /dev/null
else
+44 -7
View File
@@ -41,7 +41,17 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
submodules: recursive
- name: Point the WinLibs CMake at the CA bundle of Git for Windows
if: matrix.platform == 'windows'
shell: bash
run: |
for prefix in ucrt64 mingw64; do
bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt"
if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi
done
echo "::error::No CA bundle found in Git for Windows"; exit 1
- id: toolchain
if: matrix.platform == 'windows'
uses: actions/cache@v6
@@ -49,8 +59,6 @@ jobs:
path: C:\winlibs
key: ${{ env.WINLIBS_ZIP }}
- if: matrix.platform == 'windows' && steps.toolchain.outputs.cache-hit != 'true'
env:
SSL_CERT_FILE: 'C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt'
run: |
curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP"
7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs
@@ -69,8 +77,6 @@ jobs:
- name: Configure Windows
if: matrix.platform == 'windows'
shell: cmd
env:
SSL_CERT_FILE: 'C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt'
run: cmake -S . -B build %CONFIGURE%
- name: Build project and tests (Linux)
if: matrix.platform == 'linux'
@@ -81,7 +87,7 @@ jobs:
run: cmake --build build
- name: Run tests (Linux)
if: matrix.platform == 'linux'
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error -E '^precise_wait$'
env:
ANYPS5_REQUIRE_VULKAN: 1
GALLIUM_OVERRIDE_CPU_CAPS: avx
@@ -105,9 +111,38 @@ jobs:
if-no-files-found: error
compression-level: 0
web:
name: Build and test web converter
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
- name: Build web converter
shell: bash
run: |
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk"
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.3
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm --target relinker_web --parallel 2
- name: Test web converter
run: python3 tools/test_web.py --build build-wasm
- name: Package web converter
shell: bash
run: python3 tools/package_release.py --platform web --build build-wasm --output dist --version "$RELEASE_TAG" --revision "$(git rev-parse HEAD)"
- uses: actions/upload-artifact@v7
with:
name: release-web
path: dist/*
if-no-files-found: error
compression-level: 0
publish:
name: Publish release
needs: build
needs: [build, web]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
@@ -117,6 +152,8 @@ jobs:
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
- uses: actions/download-artifact@v8
with:
pattern: release-*
@@ -125,7 +162,7 @@ jobs:
- name: Check release files and collect documentation
shell: bash
run: |
for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG"; do
for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG" "web-$RELEASE_TAG.zip"; do
test -s "dist/$file"
done
python tools/package_release.py --docs docs/user --output dist
+8
View File
@@ -14,6 +14,14 @@ jobs:
issues: write
pull-requests: write
steps:
- env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr list -R "$GITHUB_REPOSITORY" --label stale -L 1000 --json number,labels \
--jq '.[] | select(all(.labels[]; .name != "needs rebase")) | .number' |
while read -r number; do
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/stale" --silent
done
- uses: actions/stale@v11
with:
only-pr-labels: needs rebase
+39
View File
@@ -0,0 +1,39 @@
name: Web tests
on:
pull_request:
paths:
- .github/workflows/web-tests.yml
- demo/web/**
- tools/test_web.py
permissions:
contents: read
concurrency:
group: web-tests-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
output-directory:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Test archive root selection
shell: bash
run: |
zip_url=$(python3 -c 'from pathlib import Path; import re; print(re.search(r"from \"(https://cdn.jsdelivr.net/[^\"]+)\"", Path("demo/web/archive.mjs").read_text(encoding="utf-8")).group(1))')
curl -fsSL "$zip_url" -o "$RUNNER_TEMP/zip.mjs"
ANYPS5_TEST_ZIP_URL="$zip_url" ANYPS5_TEST_ZIP_PATH="$(python3 -c 'from pathlib import Path; import sys; print(Path(sys.argv[1]).resolve().as_uri())' "$RUNNER_TEMP/zip.mjs")" node demo/web/tests/archive-root.test.mjs demo/web
- name: Check browser module syntax
run: |
node --check demo/web/app.mjs
node --check demo/web/archive.mjs
node --check demo/web/batch.mjs
node --check demo/web/output-directory.mjs
node --check demo/web/worker.mjs
- name: Test browser converter helpers
run: node --test demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs
+117
View File
@@ -0,0 +1,117 @@
name: Windows MSYS2
on:
schedule:
- cron: '23 8 * * *'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: windows-msys2-${{ github.ref }}
cancel-in-progress: true
env:
CONFIGURE: >-
-G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
jobs:
windows-msys2:
runs-on: windows-latest
timeout-minutes: 45
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
persist-credentials: false
- id: submodules
name: Key the submodules on their commits
run: |
echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT"
- id: submodule-cache
uses: actions/cache/restore@v6
with:
path: ${{ runner.temp }}/submodule-gitdirs.tar
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
- name: Check out the submodules
run: |
if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi
git submodule update --init --recursive --depth 1 --jobs 8
- if: steps.submodule-cache.outputs.cache-hit != 'true'
run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules
- if: steps.submodule-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: ${{ runner.temp }}/submodule-gitdirs.tar
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
- name: Use the CA bundle of Git for Windows
run: |
for prefix in ucrt64 mingw64; do
bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt"
if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi
done
echo "::error::No CA bundle found in Git for Windows"; exit 1
- id: msys2
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-cmake
mingw-w64-ucrt-x86_64-ninja
- shell: pwsh
env:
MSYS2_LOCATION: ${{ steps.msys2.outputs.msys2-location }}
run: Add-Content -LiteralPath $env:GITHUB_PATH -Value "$env:MSYS2_LOCATION\ucrt64\bin"
- uses: hendrikmuhs/[email protected]
with:
key: windows-msys2
save: ${{ github.ref == 'refs/heads/main' }}
evict-old-files: job
- id: ffmpeg-cache
uses: actions/cache/restore@v6
with:
path: build/externals
key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }}
- shell: cmd
run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
- if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: build/externals
key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }}
- name: Check that every test executable is registered with ctest
shell: cmd
run: python tools/check_test_registration.py build
- shell: cmd
run: cmake --build build
- shell: cmd
run: cmake --build build --target libs
- shell: cmd
working-directory: build
run: ctest --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
- run: python tools/check_skips.py build/ctest.xml --job windows
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
run: |
[ -f build/ctest.log ] || exit 0
awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t }
END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' build/ctest.log
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: windows-msys2-tests
path: |
build/ctest.log
build/ctest.xml
if-no-files-found: ignore
+18
View File
@@ -31,6 +31,9 @@
[submodule "3rdparty/tracy"]
path = 3rdparty/tracy
url = https://github.com/wolfpld/tracy.git
[submodule "3rdparty/mimalloc"]
path = 3rdparty/mimalloc
url = https://github.com/microsoft/mimalloc.git
[submodule "3rdparty/libjpeg-turbo"]
path = 3rdparty/libjpeg-turbo
url = https://github.com/libjpeg-turbo/libjpeg-turbo.git
@@ -40,3 +43,18 @@
[submodule "3rdparty/mbedtls"]
path = 3rdparty/mbedtls
url = https://github.com/Mbed-TLS/mbedtls.git
[submodule "3rdparty/zstd"]
path = 3rdparty/zstd
url = https://github.com/facebook/zstd.git
[submodule "3rdparty/ZArchive"]
path = 3rdparty/ZArchive
url = https://github.com/Exzap/ZArchive.git
[submodule "3rdparty/vgmstream"]
path = 3rdparty/vgmstream
url = https://github.com/vgmstream/vgmstream.git
[submodule "3rdparty/ogg"]
path = 3rdparty/ogg
url = https://github.com/xiph/ogg.git
[submodule "3rdparty/vorbis"]
path = 3rdparty/vorbis
url = https://github.com/xiph/vorbis.git
Vendored Submodule
+1
Submodule 3rdparty/ZArchive added at 965b66c8d6.
Vendored Submodule
+1
Submodule 3rdparty/mimalloc added at f8401befa6.
Vendored Submodule
+1
Submodule 3rdparty/ogg added at be05b13e98.
Vendored Submodule
+1
Submodule 3rdparty/vgmstream added at 7c520e8f9b.
Vendored Submodule
+1
Submodule 3rdparty/vorbis added at 0657aee69d.
Vendored Submodule
+1
Submodule 3rdparty/zstd added at f8745da6ff.
+15
View File
@@ -0,0 +1,15 @@
# AGENTS.md
AnyPS5 relinks PS5 executables into native Linux or Windows programs that run against reimplemented system libraries; it is not an emulator. The rules are [CONTRIBUTING.md](CONTRIBUTING.md) and [CONVENTIONS](docs/dev/CONVENTIONS.md); these are the ones agents break.
- No comments in code, except `#endif` and namespace ends.
- An export you cannot implement calls `NotImplemented_nid_no_patch(__func__)`; never return a made-up success.
- Implement the function for every input. Never special-case a value, ID or path seen in one title's trace; if the general behaviour is unknown, throw and record it in `docs/dev/debt/<library>.md`.
- New third-party code is a submodule at `3rdparty/<name>`: add its `.gitmodules` entry (`git submodule add <url> 3rdparty/<name>`) and build it from source with CMake. Never `find_package` or copy sources in.
- Known gaps go in `docs/dev/debt/<library>.md`; notes, reports and logs go in the pull request. Add no other files.
- Commit subjects and PR titles are Conventional Commits.
- PR body: copy [the template](.github/pull_request_template.md), keep every line, tick every box, and fill `What`, `Tested`, `Depends on` (#N or none), `Unblocks` (title ID and discussion, or none), `GPUs tested` (AGC, shader recompiler, VideoOut; otherwise n/a) and `AI-assisted` (yes or no).
- Before pushing run `python3 tools/check_conventions.py --base origin/main`; before opening run `python3 tools/pr_overlap.py --branch <branch>`.
- Build and test: [BUILD](docs/dev/BUILD.md); a relinker-only change builds with `-DANYPS5_RELINKER_ONLY=ON`.
- A speed claim shows `tools/trace_digest.py` summaries of `main` and of the change from the same machine and scene.
- AGC, shader recompiler and VideoOut changes: run the same focused tests on `main` and the change with `ANYPS5_REQUIRE_VULKAN=1`, `ANYPS5_NO_SHADER_CACHE=1` and `VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation` using `ctest --test-dir build -R <pattern> -V` from the same machine and GPU. Confirm neither log contains `was not found but was requested by env var VK_INSTANCE_LAYERS`, then compare the logs and confirm the change adds no new `VUID-` validation errors (`ctest` can report `Passed` when validation errors are logged).
+135 -2
View File
@@ -6,10 +6,21 @@ endif()
project(Relinker CXX)
set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests")
set(ANYPS5_WINDOWS_TEST_EXIT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WindowsTestExit.cpp")
function(add_test_executable target)
add_executable(${target} ${ARGN})
set_target_properties(${target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>")
if(MINGW)
target_sources(${target} PRIVATE "${ANYPS5_WINDOWS_TEST_EXIT}")
target_link_options(${target} PRIVATE LINKER:--wrap=main)
endif()
endfunction()
function(add_asan_test_options test options)
if(ANYPS5_ENABLE_ASAN)
set_property(TEST ${test} APPEND PROPERTY ENVIRONMENT_MODIFICATION "ASAN_OPTIONS=string_append::${options}")
endif()
endfunction()
option(BUILD_TESTING "Build and register relinker tests" OFF)
@@ -33,6 +44,9 @@ if(BUILD_TESTING)
get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY)
foreach(runtimeName IN ITEMS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll)
if(NOT EXISTS "${testCompilerDirectory}/${runtimeName}")
if(ANYPS5_RELINKER_ONLY)
continue()
endif()
message(FATAL_ERROR "Cannot locate MinGW runtime: ${runtimeName}")
endif()
file(COPY "${testCompilerDirectory}/${runtimeName}" DESTINATION "${ANYPS5_TEST_OUTPUT_DIRECTORY}")
@@ -51,6 +65,18 @@ include(cmake/SystemDeps.cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(CheckCXXSourceCompiles)
function(anyps5_check_cxx_links_standard_library)
set(CMAKE_TRY_COMPILE_TARGET_TYPE EXECUTABLE)
check_cxx_source_compiles("#include <string>\nint main(int argc, char**) { try { throw std::string(static_cast<std::size_t>(argc), 'x'); } catch (const std::string& text) { return static_cast<int>(text.size()) - argc; } }" ANYPS5_CXX_LINKS_STANDARD_LIBRARY)
endfunction()
anyps5_check_cxx_links_standard_library()
if(NOT ANYPS5_CXX_LINKS_STANDARD_LIBRARY)
message(FATAL_ERROR "The C++ compiler ${CMAKE_CXX_COMPILER} cannot link a program that uses the C++ standard library. "
"Set CMAKE_CXX_COMPILER to the C++ driver (g++ or clang++, not gcc or clang) in a new build directory. "
"Supported toolchains are listed in docs/dev/BUILD.md.")
endif()
if(ANYPS5_ENABLE_ASAN)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux")
@@ -86,13 +112,38 @@ if(BUILD_TESTING)
add_test(NAME request_serializer COMMAND request_serializer_tests)
set_tests_properties(request_serializer PROPERTIES TIMEOUT 20)
find_package(Python3 QUIET COMPONENTS Interpreter)
add_test_executable(shader_ir_use_rewrite_tests
core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp
core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp
core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp
core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp
core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp
core/shader/recompiler/IntermediateRepresentation/src/GuestRegister.cpp)
target_include_directories(shader_ir_use_rewrite_tests PRIVATE
core/shader/recompiler/IntermediateRepresentation/include)
add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests)
set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20)
add_test_executable(pad_command_file_tests core/libs/prx/libScePad/tests/InputCommandFile.cpp)
target_include_directories(pad_command_file_tests PRIVATE core/libs)
add_test(NAME pad_command_file COMMAND pad_command_file_tests)
set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20)
if(Python3_Interpreter_FOUND)
add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND})
set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20)
add_test(NAME shader_cache_runtime_sources COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND})
set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 180)
add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py)
add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_dependencies.py)
add_test(NAME pr_overlap_tools COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap.py)
add_test(NAME pr_labels COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_labels.py)
add_test(NAME progress_history COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_history.py)
add_test(NAME progress_report COMMAND ${Python3_EXECUTABLE}
@@ -107,16 +158,50 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py)
add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py)
add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py)
foreach(shard RANGE 0 11)
if(shard EQUAL 0)
set(conventions_test_name conventions_function_bodies)
else()
set(conventions_test_name conventions_function_bodies_shard_${shard})
endif()
add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/12)
set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30)
endforeach()
add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py)
add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py)
add_test(NAME conventions_git_errors COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_git.py)
add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py)
add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_unknown_nids.py)
add_test(NAME conventions_debt_length COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_debt_length.py)
add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py)
add_test(NAME test_script_registration COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts)
add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py)
add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py)
add_test(NAME select_tests COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py)
add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py)
add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedTests)
add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedPathTests)
add_test(NAME configure_seed_round_trips COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedRoundTripTests)
add_test(NAME configure_seed_parsing COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ChecksParsingTests)
add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
@@ -125,27 +210,57 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py)
add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py)
add_test(NAME hardware_oracle_compare COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_compare_results.py)
set_tests_properties(hardware_oracle_compare PROPERTIES TIMEOUT 120)
add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py)
add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py)
add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py)
add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py)
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py)
add_test(NAME web_release COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_web_release.py)
set_tests_properties(web_release PROPERTIES TIMEOUT 20)
add_test(NAME web_library_assets COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_web.py)
set_tests_properties(web_library_assets PROPERTIES TIMEOUT 20)
add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py)
set_tests_properties(pad_input PROPERTIES TIMEOUT 20)
add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py)
set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20)
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py)
set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20)
add_test(NAME env_reference COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_env_reference.py)
set_tests_properties(env_reference PROPERTIES TIMEOUT 20)
add_test(NAME conventions_env_reference COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_env_reference.py)
set_tests_properties(conventions_env_reference PROPERTIES TIMEOUT 60)
add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py)
add_test(NAME conventions_blob_reads COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_blob_reads.py)
add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py)
add_test(NAME conventions_pr_template COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_pr_template.py)
add_test(NAME check_compile COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py)
add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py)
set_tests_properties(performance_compare PROPERTIES TIMEOUT 20)
endif()
endif()
@@ -231,6 +346,24 @@ set(LIBUSB_ENABLE_UDEV OFF CACHE BOOL "" FORCE)
add_subdirectory(3rdparty/libusb-cmake EXCLUDE_FROM_ALL)
set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON)
include(cmake/ZArchive.cmake)
if(WIN32)
set(MI_BUILD_SHARED ON CACHE BOOL "" FORCE)
set(MI_BUILD_STATIC OFF CACHE BOOL "" FORCE)
set(MI_BUILD_OBJECT OFF CACHE BOOL "" FORCE)
set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(MI_OVERRIDE ON CACHE BOOL "" FORCE)
set(MI_WIN_REDIRECT ON CACHE BOOL "" FORCE)
set(MI_FREE_IS_CHECKED SHARED CACHE STRING "" FORCE)
add_subdirectory(3rdparty/mimalloc EXCLUDE_FROM_ALL)
if(MINGW)
target_compile_definitions(mimalloc PRIVATE MI_MINGW_UCRT64=1)
target_link_options(mimalloc PRIVATE -static
LINKER:--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=strdup,--wrap=_strdup)
target_sources(mimalloc PRIVATE ${CMAKE_SOURCE_DIR}/core/libs/allocator/RuntimeHeap.c)
endif()
endif()
add_subdirectory(core/shader/recompiler)
add_subdirectory(core/Decoder/Jpeg)
add_subdirectory(core/Decoder/Png)
+12 -7
View File
@@ -5,10 +5,11 @@
- Follow the [coding conventions](docs/dev/CONVENTIONS.md): naming, no comments except [technical debt](docs/dev/TechnicalDebt.md), Conventional Commits.
- Every function either does exactly what it is supposed to or throws. Unimplemented exports call `NotImplemented_nid_no_patch(__func__)` (see [libSceAudioIn](core/libs/prx/libSceAudioIn/Export.cpp)).
- Pull requests that add or change shader instruction semantics say where they come from: measured on hardware (which GPU and what was checked, e.g. with the [hardware oracle](docs/dev/HW_ORACLE.md)) or the exact source (ISA section, LLVM, ACO or Mesa file). Reviewers check the semantics on hardware. Cases the source doesn't settle throw, and behaviour not verified on hardware is recorded in [technical debt](docs/dev/TechnicalDebt.md).
- An export's signature or behaviour taken from a title's call sites or from a library's disassembly follows [reading titles and libraries](docs/dev/REVERSE_ENGINEERING.md): a call site settles the arguments, not what the library does.
- A silent stub is allowed only when it unblocks a title and only affects the UI; add it under Silent stubs in its library's [technical debt](docs/dev/debt/) file.
- Missing imports fail at startup with a clear error; don't replace them with fallbacks that keep running.
- Implement the general behaviour of a function, not what one title happens to need.
- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries (NpCppWebApi over NpWebApi2). The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx).
- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries. The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). A host copy may still exist for a wrapped library that some dumps lack entirely, like libSceNpCppWebApi: the title's own module wins whenever one is shipped, so it is only ever consulted by titles that ship nothing.
- Avoid non-standard extensions (`__attribute__`, etc.) where standard C++ is enough. Helper symbols that must not become NIDs use the `_nid_no_patch` or `_nid_no_patch_cut` suffix.
- The relinker uses only the C++20 standard library.
- Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system. With `ANYPS5_USE_SYSTEM_DEPS=ON` the packages packaged by the distributions (SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools) come from the system instead; that resolution lives in [cmake/SystemDeps.cmake](cmake/SystemDeps.cmake) and the default build never reads the system.
@@ -37,9 +38,11 @@ For pull requests, full Linux and Windows builds and tests run only when a colla
gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234
```
This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds.
This manual run also builds and tests the relinker on Linux, Windows and macOS when the pull request changes a file the relinker-only build uses, regardless of the author's previous contributions. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds.
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing:
With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`.
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the checker from the current tip of the base branch, so rules added after a pull request was opened apply to its next run, and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing:
```
python3 tools/check_conventions.py --base origin/main
@@ -55,15 +58,17 @@ The [pull request template](.github/pull_request_template.md) is the checklist f
- Keep the branch up to date with `main` and resolve conflicts yourself; rebasing and force-pushing is fine.
- A pull request that conflicts with `main` and has no activity for 7 days is labeled `stale` with a reminder. It is not closed.
- If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise).
- If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first.
- If a pull request fixes a reported issue, write `Fixes #N` on its checklist line (`none` otherwise). GitHub closes the issue on merge, and the `fix error` label is added within the hour.
- If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first. Such a pull request gets the `unblocks title` label.
- For GPU changes, list each tested GPU's vendor, model, driver and OS on the checklist line. CI runs GPU tests only on lavapipe, so reviewers may ask for a run on another vendor before merging.
- Fixes a title needs go to `main` as pull requests. A Game bring-up discussion lists the open pull requests and local changes the title still needs; update the list when one is merged, closed or replaced.
- The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`.
- Run the tests before opening the pull request and describe what was tested (OS, title or homebrew).
- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form.
- Changes to the AGC driver, the shader recompiler or VideoOut name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging.
- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form. A pull request titled `perf(...)` or `perf:` is labeled `perf`, and `perf numbers missing` until its description holds two valid summary lines with different commits.
- Changes to the AGC driver, the shader recompiler or VideoOut run the same focused tests on `main` and the change with `VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation`, `ANYPS5_NO_SHADER_CACHE=1` and `ANYPS5_REQUIRE_VULKAN=1` using `ctest -V` from the same machine and GPU, confirm neither log reports `was not found but was requested by env var VK_INSTANCE_LAYERS`, confirm the change introduces no new `VUID-` validation errors compared with `main`, and name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging.
- Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments.
- Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it.
## Documentation
Text in the repository is dry, strict and concise. Record known gaps, unknown NIDs and signatures in [technical debt](docs/dev/TechnicalDebt.md) instead of new files. An entry goes in `docs/dev/debt/<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. An entry states the gap first (what differs from the console, what is unverified, what throws) and describes the implemented behaviour only as far as the gap needs it. A pull request that closes a gap removes its entry, or cuts it to what is still open, instead of rewriting it to describe the corrected behaviour. An entry goes in `docs/dev/debt/<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.
+6 -4
View File
@@ -2,15 +2,15 @@
Tool for automatic executables porting to Linux and Windows.
The relinker-only build runs on macOS, including Apple silicon.
[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
![anyps5_collage1](https://github.com/user-attachments/assets/0f8e13e8-1cb7-4c69-8075-39978e138994)
[Discord](https://discord.gg/BHFztBPUe)
![anyps5_collage1](https://github.com/user-attachments/assets/ed2f0b2d-33d3-46d7-ab88-767ca872fc99)
Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process.
[Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Architecture](docs/dev/ARCHITECTURE.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md)
[**Discord**](https://discord.gg/BHFztBPUe), [Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Architecture](docs/dev/ARCHITECTURE.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md)
## Status
@@ -38,6 +38,8 @@ SDL-mapped game controllers are supported, including analog sticks and triggers.
This project is intended for interoperability, research, preservation, and compatibility purposes. It does not include, distribute, or require copyrighted software, firmware, cryptographic keys, or proprietary libraries. Users are responsible for ensuring that any binaries used with this project are obtained and used in accordance with applicable laws and their respective license terms.
AnyPS5 is an independent project and is not affiliated with, authorized or endorsed by Sony Interactive Entertainment. "PlayStation" and "PS5" are trademarks of Sony Interactive Entertainment Inc.
## License
This project is licensed under the GNU General Public License version 2 only.
+31
View File
@@ -0,0 +1,31 @@
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <process.h>
#include <windows.h>
extern "C" int __real_main(int argc, char** argv);
namespace {
template<typename TFunction>
TFunction LibcFunction(const char* name) {
const HMODULE libc = GetModuleHandleW(L"libc.prx");
if (libc == nullptr) return nullptr;
return reinterpret_cast<TFunction>(reinterpret_cast<void*>(GetProcAddress(libc, name)));
}
}
extern "C" int __wrap_main(int argc, char** argv) {
const int code = __real_main(argc, argv);
const auto terminate = LibcFunction<void (*)(int)>("LibcTerminate_nid_no_patch");
if (terminate == nullptr) return code;
_cexit();
std::cout.flush();
std::clog.flush();
std::cerr.flush();
std::fflush(nullptr);
terminate(code);
return code;
}
+25
View File
@@ -0,0 +1,25 @@
enable_language(C)
set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE)
set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "" FORCE)
add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/zstd/build/cmake ${CMAKE_BINARY_DIR}/3rdparty/zstd EXCLUDE_FROM_ALL)
set_target_properties(libzstd_static PROPERTIES POSITION_INDEPENDENT_CODE ON)
set(ZARCHIVE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/ZArchive)
add_library(zarchive STATIC EXCLUDE_FROM_ALL
${ZARCHIVE_SOURCE_DIR}/src/zarchivereader.cpp
${ZARCHIVE_SOURCE_DIR}/src/zarchivewriter.cpp
${ZARCHIVE_SOURCE_DIR}/src/sha_256.c
)
target_include_directories(zarchive SYSTEM PUBLIC ${ZARCHIVE_SOURCE_DIR}/include)
target_compile_features(zarchive PUBLIC cxx_std_20)
target_link_libraries(zarchive PRIVATE libzstd_static)
set_target_properties(zarchive PROPERTIES POSITION_INDEPENDENT_CODE ON)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(zarchive INTERFACE LINKER:--exclude-libs,libzarchive.a:libzstd.a)
endif()
@@ -19,6 +19,10 @@ struct Header {
std::uint32_t width;
std::uint32_t height;
std::uint32_t channels;
std::uint8_t componentIds[4]{};
std::uint8_t samplingFactors[4]{};
bool progressive = false;
bool app0BeforeFrame = false;
};
enum class Sampling {
@@ -35,6 +39,10 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg);
std::optional<Image> Decode(std::span<const std::uint8_t> jpeg);
std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator);
std::optional<Image> DecodeScaled(std::span<const std::uint8_t> jpeg, std::uint32_t denominator);
} // namespace Decoder::Jpeg
#endif // DECODER_JPEG_HPP
+71 -7
View File
@@ -2,6 +2,7 @@
#include <climits>
#include <cstddef>
#include <memory>
#include <stdexcept>
#define STB_IMAGE_STATIC
@@ -19,8 +20,8 @@ constexpr std::uint32_t MAX_DIMENSION = 0xFFFF;
class Handle {
public:
Handle() : handle(tj3Init(TJINIT_COMPRESS)) {
if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed");
explicit Handle(int type) : handle(tj3Init(type)) {
if (!handle) throw std::runtime_error(type == TJINIT_COMPRESS ? "Jpeg::Encode: encoder initialization failed" : "Jpeg::DecodeScaled: decoder initialization failed");
}
~Handle() { tj3Destroy(handle); }
Handle(const Handle&) = delete;
@@ -63,7 +64,7 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
throw std::invalid_argument("Jpeg::Encode: invalid restart interval");
}
const int subsampling = toSubsampling(sampling);
Handle handle;
Handle handle(TJINIT_COMPRESS);
if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) {
throw std::runtime_error("Jpeg::Encode: encoder configuration failed");
}
@@ -80,6 +81,43 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
return {buffer.data, buffer.data + size};
}
std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator) {
if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) {
throw std::invalid_argument("Jpeg::ScaledDimension: the scale must be 1/1, 1/2, 1/4 or 1/8");
}
const tjscalingfactor factor{1, static_cast<int>(denominator)};
return static_cast<std::uint32_t>(TJSCALED(static_cast<std::uint64_t>(dimension), factor));
}
std::optional<Image> DecodeScaled(std::span<const std::uint8_t> jpeg, std::uint32_t denominator) {
if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) {
throw std::invalid_argument("Jpeg::DecodeScaled: the scale must be 1/1, 1/2, 1/4 or 1/8");
}
if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt;
Handle handle(TJINIT_DECOMPRESS);
if (tj3Set(handle, TJPARAM_STOPONWARNING, 1) < 0) throw std::runtime_error("Jpeg::DecodeScaled: decoder configuration failed");
if (tj3DecompressHeader(handle, jpeg.data(), jpeg.size()) < 0) return std::nullopt;
const int colorspace = tj3Get(handle, TJPARAM_COLORSPACE);
if (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) {
throw std::runtime_error("Jpeg::DecodeScaled: scaled decoding of a CMYK or YCCK image is not implemented");
}
const int width = tj3Get(handle, TJPARAM_JPEGWIDTH);
const int height = tj3Get(handle, TJPARAM_JPEGHEIGHT);
if (width <= 0 || height <= 0) return std::nullopt;
if (tj3SetScalingFactor(handle, tjscalingfactor{1, static_cast<int>(denominator)}) < 0) {
throw std::runtime_error("Jpeg::DecodeScaled: the decoder refused the scaling factor");
}
const std::uint32_t channels = colorspace == TJCS_GRAY ? 1 : 3;
Image image{ScaledDimension(static_cast<std::uint32_t>(width), denominator), ScaledDimension(static_cast<std::uint32_t>(height), denominator), channels, {}};
image.pixels.resize(static_cast<std::size_t>(image.width) * image.height * channels);
if (tj3Decompress8(handle, jpeg.data(), jpeg.size(), image.pixels.data(), 0, channels == 1 ? TJPF_GRAY : TJPF_RGB) < 0) {
return std::nullopt;
}
return image;
}
std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) {
if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt;
@@ -88,7 +126,33 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) {
int channels = 0;
if (!stbi_info_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels)) return std::nullopt;
if (width <= 0 || height <= 0 || channels <= 0) return std::nullopt;
return Header{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels)};
Header header{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels)};
std::size_t offset = 2;
while (offset < jpeg.size()) {
if (jpeg[offset++] != 0xFF) return std::nullopt;
while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset;
if (offset == jpeg.size()) return std::nullopt;
const std::uint8_t marker = jpeg[offset++];
if (marker == 0xD9 || marker == 0xDA) return std::nullopt;
if (marker == 0x01 || (marker >= 0xD0 && marker <= 0xD8)) continue;
if (jpeg.size() - offset < 2) return std::nullopt;
const std::size_t length = (static_cast<std::size_t>(jpeg[offset]) << 8) | jpeg[offset + 1];
if (length < 2 || length > jpeg.size() - offset) return std::nullopt;
if (marker == 0xE0) header.app0BeforeFrame = true;
if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2) {
if (length < 8) return std::nullopt;
header.channels = jpeg[offset + 7];
if (header.channels == 0 || header.channels > 4 || length != 8 + 3 * header.channels) return std::nullopt;
header.progressive = marker == 0xC2;
for (std::uint32_t i = 0; i < header.channels; ++i) {
header.componentIds[i] = jpeg[offset + 8 + 3 * i];
header.samplingFactors[i] = jpeg[offset + 9 + 3 * i];
}
return header;
}
offset += length;
}
return std::nullopt;
}
std::optional<Image> Decode(std::span<const std::uint8_t> jpeg) {
@@ -97,12 +161,12 @@ std::optional<Image> Decode(std::span<const std::uint8_t> jpeg) {
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* decoded = stbi_load_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels, 0);
const std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> decoded(
stbi_load_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels, 0), stbi_image_free);
if (!decoded) return std::nullopt;
Image image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels), {}};
image.pixels.assign(decoded, decoded + static_cast<std::size_t>(image.width) * image.height * image.channels);
stbi_image_free(decoded);
image.pixels.assign(decoded.get(), decoded.get() + static_cast<std::size_t>(image.width) * image.height * image.channels);
return image;
}
+68
View File
@@ -1,5 +1,7 @@
#include "Decoder/Jpeg.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
@@ -108,5 +110,71 @@ int main() {
const std::vector<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8};
Require(!Decoder::Jpeg::Decode(garbage).has_value());
Require(Decoder::Jpeg::ScaledDimension(7, 1) == 7 && Decoder::Jpeg::ScaledDimension(7, 2) == 4);
Require(Decoder::Jpeg::ScaledDimension(7, 4) == 2 && Decoder::Jpeg::ScaledDimension(7, 8) == 1);
Require(Decoder::Jpeg::ScaledDimension(32, 8) == 4 && Decoder::Jpeg::ScaledDimension(33, 8) == 5 && Decoder::Jpeg::ScaledDimension(1, 8) == 1);
Require(Decoder::Jpeg::ScaledDimension(0xFFFF, 2) == 0x8000);
for (const std::uint32_t denominator : {0u, 3u, 16u}) {
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::ScaledDimension(7, denominator); }));
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); }));
}
const auto boxError = [](const std::vector<std::uint8_t>& source, std::uint32_t sourceWidth, std::uint32_t sourceHeight,
std::uint32_t channels, const Decoder::Jpeg::Image& scaled, std::uint32_t denominator) {
long error = 0;
for (std::uint32_t y = 0; y < scaled.height; ++y) {
for (std::uint32_t x = 0; x < scaled.width; ++x) {
for (std::uint32_t c = 0; c < channels; ++c) {
long sum = 0;
long count = 0;
for (std::uint32_t sy = y * denominator; sy < std::min(sourceHeight, (y + 1) * denominator); ++sy) {
for (std::uint32_t sx = x * denominator; sx < std::min(sourceWidth, (x + 1) * denominator); ++sx) {
sum += source[(static_cast<std::size_t>(sy) * sourceWidth + sx) * channels + c];
++count;
}
}
const auto average = static_cast<std::uint8_t>((sum + count / 2) / count);
error += Difference(average, scaled.pixels[(static_cast<std::size_t>(y) * scaled.width + x) * channels + c]);
}
}
}
return error / static_cast<long>(static_cast<std::size_t>(scaled.width) * scaled.height * channels);
};
for (const std::uint32_t denominator : {1u, 2u, 4u, 8u}) {
const auto scaled = Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator);
Require(scaled.has_value() && scaled->channels == 3);
Require(scaled->width == width / denominator && scaled->height == height / denominator);
Require(scaled->pixels.size() == static_cast<std::size_t>(scaled->width) * scaled->height * 3);
Require(boxError(rgb, width, height, 3, *scaled, denominator) < 4);
const auto grayScaled = Decoder::Jpeg::DecodeScaled(grayJpeg, denominator);
Require(grayScaled.has_value() && grayScaled->channels == 1);
Require(grayScaled->width == width / denominator && grayScaled->height == height / denominator);
Require(boxError(gray, width, height, 1, *grayScaled, denominator) < 4);
}
std::vector<std::uint8_t> odd(31 * 23 * 3);
for (std::uint32_t y = 0; y < 23; ++y) {
for (std::uint32_t x = 0; x < 31; ++x) {
std::uint8_t* pixel = &odd[(y * 31 + x) * 3];
pixel[0] = static_cast<std::uint8_t>(x * 8);
pixel[1] = static_cast<std::uint8_t>(y * 11);
pixel[2] = static_cast<std::uint8_t>(200 - x * 2);
}
}
const auto oddJpeg = Decoder::Jpeg::Encode(odd, 31, 23, 3, 95, Decoder::Jpeg::Sampling::Yuv420);
for (const std::uint32_t denominator : {2u, 4u, 8u}) {
const auto scaled = Decoder::Jpeg::DecodeScaled(oddJpeg, denominator);
Require(scaled.has_value() && scaled->channels == 3);
Require(scaled->width == Decoder::Jpeg::ScaledDimension(31, denominator) && scaled->height == Decoder::Jpeg::ScaledDimension(23, denominator));
Require(boxError(odd, 31, 23, 3, *scaled, denominator) < 8);
}
Require(!Decoder::Jpeg::DecodeScaled({}, 2).has_value());
Require(!Decoder::Jpeg::DecodeScaled(garbage, 2).has_value());
const std::vector<std::uint8_t> truncated(rgbJpeg.begin(), rgbJpeg.begin() + static_cast<std::ptrdiff_t>(rgbJpeg.size() / 2));
Require(!Decoder::Jpeg::DecodeScaled(truncated, 2).has_value());
return 0;
}
+3 -3
View File
@@ -152,12 +152,12 @@ std::optional<Image> Decode(std::span<const std::uint8_t> png) {
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* decoded = stbi_load_from_memory(png.data(), static_cast<int>(png.size()), &width, &height, &channels, 4);
const std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> decoded(
stbi_load_from_memory(png.data(), static_cast<int>(png.size()), &width, &height, &channels, 4), stbi_image_free);
if (!decoded) return std::nullopt;
Image image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), {}};
image.pixels.assign(decoded, decoded + static_cast<std::size_t>(image.width) * image.height * 4);
stbi_image_free(decoded);
image.pixels.assign(decoded.get(), decoded.get() + static_cast<std::size_t>(image.width) * image.height * 4);
return image;
}
+296 -18
View File
@@ -1,5 +1,7 @@
if(NOT WIN32)
string(REPLACE "-static " "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ")
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
endforeach()
string(STRIP "${CMAKE_EXE_LINKER_FLAGS}" CMAKE_EXE_LINKER_FLAGS)
endif()
@@ -37,9 +39,16 @@ if(MINGW)
check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS)
unset(CMAKE_REQUIRED_FLAGS)
if(NOT ANYPS5_MINGW_DWARF_EXCEPTIONS)
message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} selects SjLj exceptions with -fno-asynchronous-unwind-tables; the libraries need DWARF unwinding. Use the MinGW-w64 toolchain listed in docs/dev/BUILD.md.")
check_cxx_source_compiles("#if !defined(__SEH__) || defined(__USING_SJLJ_EXCEPTIONS__)\n#error\n#endif\n[[gnu::sysv_abi, gnu::noinline]] void Throw() { throw 1; }\n[[gnu::sysv_abi, gnu::noinline]] int Catch() { try { Throw(); } catch (int value) { return value; } return 0; }\nint main() { return Catch() != 1; }" ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS)
if(NOT ANYPS5_MINGW_SYSV_SEH_EXCEPTIONS)
message(FATAL_ERROR "The Windows libraries require DWARF exceptions or GCC with System V ABI SEH support. Use the toolchains listed in docs/dev/BUILD.md.")
endif()
add_library(windows_seh_personalities OBJECT ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/src/specifics/windows/SehPersonalities.cpp)
target_link_options(windows_seh_personalities INTERFACE LINKER:--undefined=LibcSehPersonalities_nid_no_patch)
link_libraries(windows_seh_personalities)
else()
add_compile_options(-fno-asynchronous-unwind-tables)
endif()
add_compile_options(-fno-asynchronous-unwind-tables)
endif()
function(configure_windows_unwind target)
@@ -48,7 +57,7 @@ function(configure_windows_unwind target)
message(FATAL_ERROR "Windows DWARF unwinding requires objcopy")
endif()
add_custom_command(TARGET ${target} POST_BUILD
COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram "$<TARGET_FILE:${target}>"
COMMAND "${CMAKE_OBJCOPY}" --rename-section .eh_frame=.ehfram --rename-section .eh_fram=.ehfram "$<TARGET_FILE:${target}>"
VERBATIM
)
endif()
@@ -58,6 +67,21 @@ if(MINGW)
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
endforeach()
if(NOT EXISTS "${CMAKE_RANLIB}")
message(FATAL_ERROR "Stable import libraries require ranlib")
endif()
list(LENGTH CMAKE_CXX_CREATE_SHARED_LIBRARY linkCommandCount)
string(FIND "${CMAKE_CXX_CREATE_SHARED_LIBRARY}" "-Wl,--out-implib,<TARGET_IMPLIB> " implibOption)
if(NOT linkCommandCount EQUAL 1 OR implibOption EQUAL -1)
message(FATAL_ERROR "Expected one shared library link command that writes -Wl,--out-implib,<TARGET_IMPLIB>: ${CMAKE_CXX_CREATE_SHARED_LIBRARY}")
endif()
string(REPLACE "-Wl,--out-implib,<TARGET_IMPLIB> " "-Wl,--out-implib,<TARGET_IMPLIB>.new " linkCommand "${CMAKE_CXX_CREATE_SHARED_LIBRARY}")
set(CMAKE_CXX_CREATE_SHARED_LIBRARY
"\"${CMAKE_COMMAND}\" -E env SOURCE_DATE_EPOCH=0 ${linkCommand}"
"\"${CMAKE_RANLIB}\" -D <TARGET_IMPLIB>.new"
"\"${CMAKE_COMMAND}\" -E copy_if_different <TARGET_IMPLIB>.new <TARGET_IMPLIB>"
)
endif()
set(LIBC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc)
@@ -65,6 +89,9 @@ add_library(libc SHARED EXCLUDE_FROM_ALL)
set(LIBS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
add_subdirectory(${LIBC_DIR} ${CMAKE_CURRENT_BINARY_DIR}/prx/libc)
target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(WIN32)
target_compile_options(libc PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp")
endif()
configure_windows_unwind(libc)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(libc PRIVATE LINKER:-z,now)
@@ -87,6 +114,14 @@ set_target_properties(libc PROPERTIES
set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx)
set(LIBC_PATCHED ${PATCHED_DIR}/libc.prx)
set(LIBC_EXPORT_LIST ${CMAKE_CURRENT_BINARY_DIR}/libc.exports)
add_custom_command(
OUTPUT ${LIBC_EXPORT_LIST}
COMMAND $<TARGET_FILE:nid_patcher> --write-export-list ${LIBC_UNPATCHED} ${LIBC_EXPORT_LIST}
DEPENDS nid_patcher libc ${LIBC_UNPATCHED}
COMMENT "Listing libc exports"
)
add_custom_command(
OUTPUT ${LIBC_PATCHED}
@@ -114,6 +149,9 @@ foreach(LIB_DIR ${LIBS_DIRS})
configure_windows_unwind(${LIB_NAME})
endif()
if(WIN32)
target_compile_options(${LIB_NAME} PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp")
endif()
if(LIB_NAME STREQUAL "libkernel")
target_link_libraries(${LIB_NAME} PUBLIC libc)
else()
@@ -146,8 +184,8 @@ foreach(LIB_DIR ${LIBS_DIRS})
add_custom_command(
OUTPUT ${PATCHED_FILE}
COMMAND ${CMAKE_COMMAND} -E copy ${UNPATCHED_FILE} ${PATCHED_FILE}
COMMAND $<TARGET_FILE:nid_patcher> ${LIB_NAME} --preserve-exports "${LIBC_UNPATCHED}" "${PATCHED_FILE}"
DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_UNPATCHED}
COMMAND $<TARGET_FILE:nid_patcher> ${LIB_NAME} --preserve-export-list "${LIBC_EXPORT_LIST}" "${PATCHED_FILE}"
DEPENDS nid_patcher ${LIB_NAME} ${UNPATCHED_FILE} ${LIBC_EXPORT_LIST}
COMMENT "Patching NIDs for ${LIB_NAME}"
)
@@ -155,6 +193,8 @@ foreach(LIB_DIR ${LIBS_DIRS})
endif()
endforeach()
target_link_libraries(libSceUsbStorageDialog PRIVATE libSceUserService)
if(APS5_ENABLE_TRACY)
set_target_properties(TracyClient PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
@@ -173,6 +213,29 @@ if(APS5_ENABLE_TRACY)
list(APPEND PATCHED_OUTPUTS ${TRACY_CLIENT_COPIED})
endif()
if(TARGET mimalloc)
add_executable(pe_import_first ${CMAKE_CURRENT_SOURCE_DIR}/allocator/PeImportFirstMain.cpp ${CMAKE_CURRENT_SOURCE_DIR}/allocator/PeImportFirst.cpp)
target_compile_features(pe_import_first PRIVATE cxx_std_20)
if(MINGW)
target_compile_options(pe_import_first PRIVATE -fasynchronous-unwind-tables)
target_link_options(pe_import_first PRIVATE -municode -static -static-libgcc -static-libstdc++)
endif()
add_custom_command(
OUTPUT ${PATCHED_DIR}/mimalloc.dll
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:mimalloc> ${PATCHED_DIR}/mimalloc.dll
COMMAND $<TARGET_FILE:pe_import_first> ${PATCHED_DIR}/mimalloc.dll mimalloc-redirect.dll
DEPENDS mimalloc pe_import_first
COMMENT "Copying mimalloc.dll beside the libraries, its redirection module imported first"
)
add_custom_command(
OUTPUT ${PATCHED_DIR}/mimalloc-redirect.dll
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE_DIR:mimalloc>/mimalloc-redirect.dll ${PATCHED_DIR}/mimalloc-redirect.dll
DEPENDS mimalloc
COMMENT "Copying mimalloc-redirect.dll beside the libraries"
)
list(APPEND PATCHED_OUTPUTS ${PATCHED_DIR}/mimalloc.dll ${PATCHED_DIR}/mimalloc-redirect.dll)
endif()
add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS})
if(BUILD_TESTING)
@@ -185,6 +248,15 @@ if(BUILD_TESTING)
set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_kernel_sys_sdk_version_tests tests/GuestKernelSysSdkVersion.cpp)
target_include_directories(guest_kernel_sys_sdk_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_kernel_sys_sdk_version_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_sdk_version_tests)
add_test(NAME guest_kernel_sys_sdk_version COMMAND guest_kernel_sys_sdk_version_tests)
if(WIN32)
set_tests_properties(guest_kernel_sys_sdk_version PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
endif()
add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp)
target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20)
@@ -194,10 +266,21 @@ if(BUILD_TESTING)
add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests)
set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10)
add_test_executable(pe_import_first_tests allocator/tests/PeImportFirstTests.cpp allocator/PeImportFirst.cpp)
target_include_directories(pe_import_first_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(pe_import_first_tests PRIVATE cxx_std_20)
if(MINGW)
target_compile_options(pe_import_first_tests PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME pe_import_first COMMAND pe_import_first_tests)
set_tests_properties(pe_import_first PROPERTIES TIMEOUT 10)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_gnu_hash COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_gnu_hash.py $<TARGET_FILE:nid_patcher>)
add_test(NAME nid_elf_duplicates COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_duplicates.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}
@@ -215,6 +298,17 @@ if(BUILD_TESTING)
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_library(host_libstdcxx_exception_module SHARED tests/HostLibstdcxxExceptionModule.cpp)
target_link_libraries(host_libstdcxx_exception_module PRIVATE ${PATCHED_DIR}/libc.prx)
add_dependencies(host_libstdcxx_exception_module libs)
add_test_executable(host_libstdcxx_exception_tests tests/HostLibstdcxxException.cpp)
target_link_libraries(host_libstdcxx_exception_tests PRIVATE host_libstdcxx_exception_module ${PATCHED_DIR}/libc.prx)
add_dependencies(host_libstdcxx_exception_tests libs)
set_target_properties(host_libstdcxx_exception_module host_libstdcxx_exception_tests PROPERTIES BUILD_RPATH ${PATCHED_DIR})
add_test(NAME host_libstdcxx_exception COMMAND host_libstdcxx_exception_tests)
set_tests_properties(host_libstdcxx_exception PROPERTIES TIMEOUT 20)
endif()
if(Python3_Interpreter_FOUND)
add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $<TARGET_FILE:nid_patcher>)
@@ -224,16 +318,23 @@ if(BUILD_TESTING)
target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_tests)
add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests)
add_test_executable(guest_kernel_sys_thread_defaults_tests tests/GuestKernelSysThreadDefaults.cpp)
target_include_directories(guest_kernel_sys_thread_defaults_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_kernel_sys_thread_defaults_tests PRIVATE libkernel_sys libkernel libc)
configure_windows_unwind(guest_kernel_sys_thread_defaults_tests)
add_test(NAME guest_kernel_sys_thread_defaults COMMAND guest_kernel_sys_thread_defaults_tests)
set_tests_properties(guest_kernel_sys_thread_defaults PROPERTIES TIMEOUT 20)
if(Python3_Interpreter_FOUND)
add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>)
endif()
if(WIN32)
set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(guest_kernel_sys guest_kernel_sys_thread_defaults PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
endif()
if(APPLE)
add_subdirectory(tests/write_watch)
add_subdirectory(tests/guest_arena)
endif()
if(WIN32)
add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp)
@@ -248,15 +349,77 @@ if(BUILD_TESTING)
if(WIN32)
add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp)
if(MINGW)
target_compile_options(undeliverable_host_exception_tests PRIVATE -fno-asynchronous-unwind-tables -fno-exceptions)
endif()
add_dependencies(undeliverable_host_exception_tests libc)
add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $<TARGET_FILE:libc>)
set_tests_properties(undeliverable_host_exception PROPERTIES
PASS_REGULAR_EXPRESSION "C\\+\\+ exception St9bad_alloc raised on thread [0-9]+ cannot be delivered: 0x[0-9a-f]+ undeliverable_host_exception_tests\\.exe\\+0x[0-9a-f]+ has no unwind data")
endif()
add_test_executable(crash_text_tests tests/CrashText.cpp)
target_include_directories(crash_text_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(MINGW)
target_compile_options(crash_text_tests PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME crash_text COMMAND crash_text_tests)
if(WIN32)
add_test_executable(windows_crash_text_tests tests/WindowsCrashText.cpp)
add_dependencies(windows_crash_text_tests libc)
if(MINGW)
target_compile_options(windows_crash_text_tests PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME windows_crash_text COMMAND windows_crash_text_tests $<TARGET_FILE:libc>)
set_tests_properties(windows_crash_text PROPERTIES TIMEOUT 20
PASS_REGULAR_EXPRESSION "faulting[^~]*FATAL: unhandled exception 0xc0000005[^~]* \\[r12\\] \"allocation failed in the heap\"[\r\n]+ \\[r13\\] u\"LowLevelFatalError \\[File:Unknown\\] \\[Line: 42\\] Out of memory\"[\r\n]+ \\[r14\\] \"PAGE END\"[.][.][.]"
FAIL_REGULAR_EXPRESSION "\\[rbx\\] u?\"|did not fault")
endif()
add_test_executable(guest_voice_port_attr_tests tests/GuestVoicePortAttr.cpp)
target_include_directories(guest_voice_port_attr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_voice_port_attr_tests PRIVATE libSceVoice libc)
configure_windows_unwind(guest_voice_port_attr_tests)
add_test(NAME guest_voice_port_attr COMMAND guest_voice_port_attr_tests)
if(WIN32)
set_tests_properties(guest_voice_port_attr PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVoice>")
endif()
add_test_executable(guest_gnm_neo_retail_tests tests/GuestGnmNeoRetail.cpp)
target_include_directories(guest_gnm_neo_retail_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_gnm_neo_retail_tests PRIVATE libSceGnmDriverForNeoMode libc)
configure_windows_unwind(guest_gnm_neo_retail_tests)
add_test(NAME guest_gnm_neo_retail COMMAND guest_gnm_neo_retail_tests)
if(WIN32)
set_tests_properties(guest_gnm_neo_retail PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGnmDriverForNeoMode>")
endif()
add_test_executable(guest_move_tests tests/GuestMove.cpp)
target_include_directories(guest_move_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_move_tests PRIVATE libSceMove libc)
configure_windows_unwind(guest_move_tests)
add_test(NAME guest_move COMMAND guest_move_tests)
if(WIN32)
set_tests_properties(guest_move PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceMove>")
endif()
add_test_executable(guest_np_party_tests tests/GuestNpParty.cpp)
target_include_directories(guest_np_party_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_party_tests PRIVATE libSceNpParty libc)
configure_windows_unwind(guest_np_party_tests)
add_test(NAME guest_np_party COMMAND guest_np_party_tests)
if(WIN32)
set_tests_properties(guest_np_party PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpParty>")
endif()
add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp)
target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libSceVideoRecordingP libc)
configure_windows_unwind(guest_offline_imports_tests)
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
if(Python3_Interpreter_FOUND)
@@ -268,6 +431,17 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
endif()
add_test_executable(guest_entitlements_tests tests/GuestEntitlements.cpp)
target_include_directories(guest_entitlements_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_entitlements_tests PRIVATE libSceNpEntitlementAccess libc)
configure_windows_unwind(guest_entitlements_tests)
add_test(NAME guest_entitlements COMMAND guest_entitlements_tests)
add_test(NAME guest_entitlements_missing COMMAND guest_entitlements_tests missing)
if(WIN32)
set_tests_properties(guest_entitlements guest_entitlements_missing PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpEntitlementAccess>")
endif()
add_test_executable(guest_signin_dialog_tests tests/GuestSigninDialog.cpp)
target_include_directories(guest_signin_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_signin_dialog_tests PRIVATE libSceSigninDialog libc)
@@ -278,6 +452,36 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSigninDialog>")
endif()
add_test_executable(guest_usb_storage_dialog_tests tests/GuestUsbStorageDialog.cpp)
target_include_directories(guest_usb_storage_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libSceUserService libc)
configure_windows_unwind(guest_usb_storage_dialog_tests)
add_test(NAME guest_usb_storage_dialog COMMAND guest_usb_storage_dialog_tests)
if(WIN32)
set_tests_properties(guest_usb_storage_dialog PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceUsbStorageDialog>")
endif()
add_test_executable(guest_game_live_streaming_tests tests/GuestGameLiveStreaming.cpp)
target_include_directories(guest_game_live_streaming_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_game_live_streaming_tests PRIVATE libSceGameLiveStreaming libc)
configure_windows_unwind(guest_game_live_streaming_tests)
add_test(NAME guest_game_live_streaming COMMAND guest_game_live_streaming_tests)
if(WIN32)
set_tests_properties(guest_game_live_streaming PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGameLiveStreaming>")
endif()
add_test_executable(guest_netctl_ap_tests tests/GuestNetCtlAp.cpp)
target_include_directories(guest_netctl_ap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_netctl_ap_tests PRIVATE libSceNetCtlAp libc)
configure_windows_unwind(guest_netctl_ap_tests)
add_test(NAME guest_netctl_ap COMMAND guest_netctl_ap_tests)
if(WIN32)
set_tests_properties(guest_netctl_ap PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNetCtlAp>")
endif()
add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp)
target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc)
@@ -328,6 +532,26 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
endif()
add_test_executable(guest_rudp_context_lifecycle_tests tests/GuestRudpContextLifecycle.cpp)
target_include_directories(guest_rudp_context_lifecycle_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_rudp_context_lifecycle_tests PRIVATE libSceRudp libc)
configure_windows_unwind(guest_rudp_context_lifecycle_tests)
add_test(NAME guest_rudp_context_lifecycle COMMAND guest_rudp_context_lifecycle_tests)
if(WIN32)
set_tests_properties(guest_rudp_context_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
endif()
add_test_executable(guest_rudp_poll_tests tests/GuestRudpPoll.cpp)
target_include_directories(guest_rudp_poll_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_rudp_poll_tests PRIVATE libSceRudp libc)
configure_windows_unwind(guest_rudp_poll_tests)
add_test(NAME guest_rudp_poll COMMAND guest_rudp_poll_tests)
if(WIN32)
set_tests_properties(guest_rudp_poll PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<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)
@@ -378,6 +602,18 @@ if(BUILD_TESTING)
set_tests_properties(savedata_memory_growth savedata_memory_growth_read_failure PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSaveData.native>")
endif()
add_test_executable(savedata_memory_journal_tests tests/SaveDataMemoryJournal.cpp)
target_include_directories(savedata_memory_journal_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(savedata_memory_journal_tests PRIVATE libSceSaveData.native)
configure_windows_unwind(savedata_memory_journal_tests)
if(MINGW)
target_compile_options(savedata_memory_journal_tests PRIVATE -fasynchronous-unwind-tables)
endif()
add_test(NAME savedata_memory_journal COMMAND savedata_memory_journal_tests)
if(WIN32)
set_tests_properties(savedata_memory_journal PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSaveData.native>")
endif()
add_library(host_thread_local_test_library SHARED tests/HostThreadLocalLibrary.cpp)
set_target_properties(host_thread_local_test_library PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>")
target_include_directories(host_thread_local_test_library PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
@@ -395,6 +631,26 @@ if(BUILD_TESTING)
set_tests_properties(prx_thread_local_destructors PROPERTIES TIMEOUT 300)
endif()
add_test_executable(guest_np_cpp_web_api_intrusive_ptr_tests tests/GuestNpCppWebApiIntrusivePtr.cpp)
target_include_directories(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE libSceNpCppWebApi libc)
configure_windows_unwind(guest_np_cpp_web_api_intrusive_ptr_tests)
add_test(NAME guest_np_cpp_web_api_intrusive_ptr COMMAND guest_np_cpp_web_api_intrusive_ptr_tests)
if(WIN32)
set_tests_properties(guest_np_cpp_web_api_intrusive_ptr PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCppWebApi>")
endif()
add_test_executable(guest_np_cpp_web_api_signed_out_tests tests/GuestNpCppWebApiSignedOut.cpp)
target_include_directories(guest_np_cpp_web_api_signed_out_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_cpp_web_api_signed_out_tests PRIVATE libSceNpCppWebApi libc)
configure_windows_unwind(guest_np_cpp_web_api_signed_out_tests)
add_test(NAME guest_np_cpp_web_api_signed_out COMMAND guest_np_cpp_web_api_signed_out_tests)
if(WIN32)
set_tests_properties(guest_np_cpp_web_api_signed_out PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCppWebApi>")
endif()
add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp)
target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData)
@@ -435,16 +691,6 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<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)
@@ -474,4 +720,36 @@ if(BUILD_TESTING)
set_tests_properties(guest_acm PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAcm>")
endif()
add_test_executable(guest_voice_chat_tests tests/GuestVoiceChat.cpp)
target_include_directories(guest_voice_chat_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_voice_chat_tests PRIVATE libSceVoiceChat libc)
configure_windows_unwind(guest_voice_chat_tests)
add_test(NAME guest_voice_chat COMMAND guest_voice_chat_tests)
if(WIN32)
set_tests_properties(guest_voice_chat PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVoiceChat>")
endif()
add_test_executable(guest_np_game_intent_tests tests/GuestNpGameIntent.cpp)
target_include_directories(guest_np_game_intent_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_game_intent_tests PRIVATE libSceNpGameIntent libc)
configure_windows_unwind(guest_np_game_intent_tests)
add_test(NAME guest_np_game_intent COMMAND guest_np_game_intent_tests)
if(WIN32)
set_tests_properties(guest_np_game_intent PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpGameIntent>")
endif()
add_test_executable(guest_vorbis_dec_tests tests/GuestVorbisDec.cpp)
target_include_directories(guest_vorbis_dec_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_definitions(guest_vorbis_dec_tests PRIVATE OV_EXCLUDE_STATIC_CALLBACKS)
target_link_libraries(guest_vorbis_dec_tests PRIVATE libSceVorbisDec vorbisenc vorbisfile vorbis ogg libc)
configure_windows_unwind(guest_vorbis_dec_tests)
add_test(NAME guest_vorbis_dec COMMAND guest_vorbis_dec_tests)
set_tests_properties(guest_vorbis_dec PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(guest_vorbis_dec PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVorbisDec>")
endif()
endif()
+358 -35
View File
@@ -160,18 +160,23 @@ struct VirtualQueryInfo {
std::uint64_t offset;
std::int32_t protection;
std::int32_t memory_type;
std::uint32_t is_flexible : 1;
std::uint32_t is_direct : 1;
std::uint32_t is_stack : 1;
std::uint32_t is_pooled : 1;
std::uint32_t is_committed : 1;
std::uint32_t is_gpu_prt : 1;
std::uint32_t amm_usage : 1;
std::uint32_t reserved : 1;
std::uint8_t is_flexible : 1;
std::uint8_t is_direct : 1;
std::uint8_t is_stack : 1;
std::uint8_t is_pooled : 1;
std::uint8_t is_committed : 1;
std::uint8_t is_gpu_prt : 1;
std::uint8_t amm_usage : 1;
std::uint8_t reserved : 1;
char name[32];
std::uint8_t gpu_mask_id;
std::uint8_t reserved2;
};
static_assert(sizeof(VirtualQueryInfo) == 72);
static_assert(alignof(VirtualQueryInfo) == 8);
static_assert(offsetof(VirtualQueryInfo, name) == 33);
static_assert(offsetof(VirtualQueryInfo, gpu_mask_id) == 65);
static_assert(offsetof(VirtualQueryInfo, reserved2) == 66);
struct KernelSemaPrivate;
struct KernelEventFlagPrivate;
@@ -328,7 +333,7 @@ struct VaList {
};
#pragma pack(pop)
using FiberEntry = void* (*)(std::uint64_t, void*);
using FiberEntry = void (APS5_VABI*)(std::uint64_t argOnInitialize, std::uint64_t argOnRun);
struct FiberOptParam {
std::uint32_t magic;
@@ -427,6 +432,48 @@ struct AcmBatchError {
std::uint32_t reserved[8];
};
struct AcmReverbBuffer {
void* samples;
const void* blockMetadata;
};
struct AcmReverbInput {
std::uint32_t blockSize;
std::uint32_t blockCount;
std::uint32_t channelCount;
std::uint32_t sampleFormat;
std::uint32_t currentBlock;
std::uint32_t availableBlocks;
const AcmReverbBuffer* const* history;
const void* const* inputChannels;
std::uint32_t pendingBlocks;
};
struct AcmReverbFilter {
std::uint32_t blockSize;
std::uint32_t blockCount;
std::uint32_t channelCount;
std::uint32_t sampleFormat;
const void* blockMetadata;
const AcmReverbBuffer* const* channels;
};
struct AcmReverbOutput {
std::uint32_t blockSize;
std::uint32_t channelCount;
void* const* samples;
void* const* workspace;
float gain;
float previousGain;
void* const* previousSamples;
};
static_assert(sizeof(AcmReverbBuffer) == 16);
static_assert(offsetof(AcmReverbInput, history) == 24);
static_assert(offsetof(AcmReverbInput, pendingBlocks) == 40);
static_assert(sizeof(AcmReverbFilter) == 32);
static_assert(offsetof(AcmReverbOutput, previousSamples) == 32);
struct AcmBatchInfo {
void* buffer;
std::size_t offset;
@@ -552,6 +599,47 @@ struct AudioOut2MasteringStatesHeader {
std::uint32_t states_id;
};
struct AudioOut2MasteringStatesDescriptor {
std::uint32_t id;
std::uint32_t size;
};
struct AudioOut2MasteringCompressorStates {
AudioOut2MasteringStatesDescriptor descriptor;
std::uint32_t reserved[2];
float input_rms[3][8];
float compression_coeff[3][8];
};
struct AudioOut2MasteringCompressorStatesV2 {
AudioOut2MasteringStatesDescriptor descriptor;
std::uint32_t reserved[2];
float input_rms[4][8];
float compression_coeff[4][8];
};
struct AudioOut2MasteringLimiterStates {
AudioOut2MasteringStatesDescriptor descriptor;
std::uint32_t reserved[2];
float input_peak[8];
float output_peak[8];
float gain_peak[8];
};
struct AudioOut2MasteringStates {
AudioOut2MasteringStatesHeader states_header;
std::uint32_t reserved[3];
AudioOut2MasteringCompressorStates compressor_states;
AudioOut2MasteringLimiterStates limiter_states;
};
struct AudioOut2MasteringStatesV2 {
AudioOut2MasteringStatesHeader states_header;
std::uint32_t reserved[3];
AudioOut2MasteringCompressorStatesV2 compressor_states;
AudioOut2MasteringLimiterStates limiter_states;
};
struct AudioOutOutputParam {
int handle;
const void* ptr;
@@ -675,10 +763,13 @@ struct AvPlayerVideoEx {
std::uint8_t luma_bit_depth;
std::uint8_t chroma_bit_depth;
bool video_full_range_flag;
std::uint8_t reserved1[37];
std::uint8_t reserved1[5];
double frame_rate;
std::uint8_t reserved2[24];
};
static_assert(sizeof(AvPlayerVideoEx) == 80);
static_assert(offsetof(AvPlayerVideoEx, crop_left_offset) == 20 && offsetof(AvPlayerVideoEx, pitch) == 36);
static_assert(offsetof(AvPlayerVideoEx, frame_rate) == 48);
struct AvPlayerTimedTextEx {
std::uint8_t language_code[4];
@@ -884,13 +975,60 @@ struct AudiodecM4aacInfo {
std::int32_t i_result;
};
struct VoiceEvent {
std::int32_t type;
std::uint32_t reserved;
void* userData;
union {
struct {
std::uint16_t count;
std::uint16_t ports[7];
} dataReady;
struct {
std::uint16_t port;
std::uint8_t state;
std::uint8_t reserved[13];
} deviceState;
};
};
using VoiceEventHandler = void (APS5_VABI*)(const VoiceEvent* event);
struct VoiceThreadsParams {
std::uint32_t mode;
std::int32_t firstPriority;
std::uint64_t firstAffinity;
std::int32_t secondPriority;
std::uint32_t reserved20;
std::uint64_t secondAffinity;
std::int32_t thirdPriority;
std::uint32_t reserved36;
std::uint64_t thirdAffinity;
};
static_assert(sizeof(VoiceEvent) == 32);
static_assert(offsetof(VoiceEvent, userData) == 8);
static_assert(offsetof(VoiceEvent, dataReady) == 16);
static_assert(offsetof(VoiceEvent, deviceState) == 16);
static_assert(sizeof(VoiceThreadsParams) == 48);
static_assert(offsetof(VoiceThreadsParams, firstAffinity) == 8);
static_assert(offsetof(VoiceThreadsParams, secondPriority) == 16);
static_assert(offsetof(VoiceThreadsParams, secondAffinity) == 24);
static_assert(offsetof(VoiceThreadsParams, thirdPriority) == 32);
static_assert(offsetof(VoiceThreadsParams, thirdAffinity) == 40);
struct VoiceInitParam {
std::int32_t app_type;
std::uint64_t on_event;
VoiceEventHandler on_event;
void* user_data;
std::uint8_t reserved[32 - sizeof(std::int32_t) - sizeof(std::uint64_t) - sizeof(void*)];
std::uint8_t reserved[16];
};
static_assert(sizeof(VoiceInitParam) == 40);
static_assert(offsetof(VoiceInitParam, on_event) == 8);
static_assert(offsetof(VoiceInitParam, user_data) == 16);
static_assert(offsetof(VoiceInitParam, reserved) == 24);
struct VoicePortParam {
std::int32_t port_type;
std::uint16_t threshold;
@@ -913,6 +1051,30 @@ struct VoicePortInfo {
std::uint16_t reserved;
};
struct NpAuthClientId;
struct NpAuthAuthorizedAppCodeParameter {
std::uint64_t size;
std::int32_t userId;
const NpAuthClientId* clientId;
const char* scope;
std::uint32_t option;
};
static_assert(sizeof(NpAuthAuthorizedAppCodeParameter) == 40);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, clientId) == 16);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, scope) == 24);
static_assert(offsetof(NpAuthAuthorizedAppCodeParameter, option) == 32);
struct VoiceResourceInfo {
std::uint16_t maxVoiceInputPorts;
std::uint16_t maxVoiceOutputPorts;
std::uint16_t maxDeviceInputPorts;
std::uint16_t maxDeviceOutputPorts;
};
static_assert(sizeof(VoiceResourceInfo) == 8);
struct VoiceStartParam {
void* container;
std::uint32_t mem_size;
@@ -1098,8 +1260,8 @@ struct ImeKeycode {
std::uint64_t timestamp;
};
using ImeTextFilter = std::int32_t (*)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
using ImeExtKeyboardFilter = int (*)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
using ImeTextFilter = std::int32_t (APS5_VABI*)(char16_t* outText, std::uint32_t* outTextLength, const char16_t* sourceText, std::uint32_t sourceTextLength);
using ImeExtKeyboardFilter = int (APS5_VABI*)(const ImeKeycode* sourceKeycode, std::uint16_t* outKeycode, std::uint32_t* outStatus, void* reserved);
struct ImeExtendedParam {
std::uint32_t option;
@@ -1261,7 +1423,12 @@ struct NetEpollEvent {
NetEpollData data;
};
struct NetCtlNatInfo { std::uint8_t opaque[128]; };
struct NetCtlNatInfo {
std::uint32_t size;
std::uint8_t opaque[12];
};
static_assert(sizeof(NetCtlNatInfo) == 16);
union NetCtlInfo { std::uint8_t opaque[256]; };
@@ -1272,14 +1439,14 @@ struct NetCtlIfStat {
std::uint32_t reserved[8];
};
using NetCtlCallback = void (*)(int, void*);
using NetCtlCallback = void (APS5_VABI *)(int, void*);
struct HttpEpoll;
using HttpEpollHandle = HttpEpoll*;
using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*);
using HttpCookieRecvCallback = int (*)(int, const char*, const char*, std::uint64_t, void*);
using HttpAuthInfoCallback = int (*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*);
using HttpsCallback = int (APS5_VABI*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (APS5_VABI*)(int, std::int32_t, std::int32_t*, const char*, void*);
using HttpCookieRecvCallback = int (APS5_VABI*)(int, const char*, const char*, std::uint64_t, void*);
using HttpAuthInfoCallback = int (APS5_VABI*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*);
struct HttpNBEvent { std::uint8_t opaque[64]; };
@@ -1338,6 +1505,24 @@ struct NpUnifiedEntitlementLabel {
char padding[3];
};
struct NpServiceEntitlementLabel {
char data[7];
std::uint8_t reserved[13];
};
struct NpEntitlementAccessTransactionId {
char data[65];
std::uint8_t reserved[7];
};
struct NpEntitlementAccessListParameter;
struct NpEntitlementAccessUnifiedInfo;
struct NpEntitlementAccessServiceInfo;
static_assert(sizeof(NpUnifiedEntitlementLabel) == 20);
static_assert(sizeof(NpServiceEntitlementLabel) == 20);
static_assert(sizeof(NpEntitlementAccessTransactionId) == 72);
struct NpEntitlementAccessInitParam { char reserved[32]; };
struct NpEntitlementAccessBootParam { char reserved[32]; };
@@ -1364,10 +1549,6 @@ struct NpUniversalDataSystemMemoryStat {
std::size_t current_inuse_size;
};
struct NpUniversalDataSystemEvent {};
struct NpUniversalDataSystemEventPropertyObject {};
struct NpUniversalDataSystemEventPropertyArray {};
struct NpUniversalDataSystemStorageStat {
std::size_t in_events;
std::size_t out_events;
@@ -1386,6 +1567,11 @@ struct NpGameIntentData {
std::uint8_t padding[7];
};
struct NpGameIntentInitParam {
std::size_t size;
std::uint8_t reserved[32];
};
struct NpGameIntentInfo {
std::size_t size;
std::int32_t user_id;
@@ -1402,6 +1588,26 @@ struct NpWebApi2ResponseInformationOption {
std::size_t response_data_size;
};
struct NpWebApi2IntInitializeArgs {
std::int32_t lib_http_ctx_id;
std::int32_t reserved;
std::uint64_t pool_size;
const char* name;
std::uint64_t struct_size;
};
static_assert(sizeof(NpWebApi2IntInitializeArgs) == 32);
struct NpWebApi2IntInitialize2Args {
std::int32_t lib_http_ctx_id;
std::int32_t reserved;
std::uint64_t pool_size;
const char* name;
std::uint32_t push_config_group;
std::int32_t reserved2;
std::uint64_t struct_size;
};
static_assert(sizeof(NpWebApi2IntInitialize2Args) == 40);
struct GameUpdateCheckParam {
std::size_t size;
std::uint32_t option;
@@ -1467,7 +1673,13 @@ struct SaveDataSearchInfo {
std::uint8_t reserved[32];
};
static_assert(sizeof(SaveDataSearchInfo) == 48);
struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; };
struct SaveDataMemoryData {
void* buf;
std::size_t buf_size;
std::size_t offset;
std::uint8_t reserved[40];
};
static_assert(sizeof(SaveDataMemoryData) == 64);
struct SaveDataMount3 {
int user_id;
@@ -1606,6 +1818,34 @@ struct AppContentBootParam {
struct AppContentMountPoint { char data[16]; };
using ContentExportMallocCallback = void* (APS5_VABI *)(std::size_t size, void* userData);
using ContentExportFreeCallback = void (APS5_VABI *)(void* memory, void* userData);
using ContentExportDataCallback = std::int32_t (APS5_VABI *)(const void** data, std::size_t* size, void* userData);
struct ContentExportInitParam {
ContentExportMallocCallback mallocFunc;
ContentExportFreeCallback freeFunc;
void* userData;
};
static_assert(sizeof(ContentExportInitParam) == 24);
static_assert(offsetof(ContentExportInitParam, freeFunc) == 8);
static_assert(offsetof(ContentExportInitParam, userData) == 16);
struct ContentExportSfParam {
std::size_t size;
const char* metadata;
const std::int64_t* contentIds;
std::uint32_t contentCount;
std::uint32_t option;
};
static_assert(sizeof(ContentExportSfParam) == 32);
static_assert(offsetof(ContentExportSfParam, metadata) == 8);
static_assert(offsetof(ContentExportSfParam, contentIds) == 16);
static_assert(offsetof(ContentExportSfParam, contentCount) == 24);
static_assert(offsetof(ContentExportSfParam, option) == 28);
struct ContentExportInitParam2 {
void* malloc_func;
void* free_func;
@@ -1750,23 +1990,29 @@ struct JpegEncOutputInfo {
};
struct JpegDecCreateParam {
std::uint32_t size;
std::uint32_t this_size;
std::uint32_t attribute;
std::uint32_t max_image_width;
};
struct JpegDecParseParam {
const void* jpeg_mem_addr;
std::uint32_t jpeg_mem_size;
std::uint32_t reserved0;
std::uint16_t decode_mode;
std::uint16_t down_scale;
};
struct JpegDecDecodeParam {
const void* jpeg_mem_addr;
void* image_mem_addr;
void* coefficient_mem_addr;
std::uint32_t jpeg_mem_size;
std::uint32_t image_mem_size;
std::uint32_t coefficient_mem_size;
std::uint16_t decode_mode;
std::uint16_t down_scale;
std::uint16_t pixel_format;
std::uint16_t reserved0;
std::uint16_t alpha_value;
std::uint32_t image_pitch;
};
@@ -1774,8 +2020,13 @@ struct JpegDecImageInfo {
std::uint32_t image_width;
std::uint32_t image_height;
std::uint16_t color_space;
std::uint16_t bit_depth;
std::uint32_t image_flag;
std::uint16_t component_number;
std::uint8_t component_id[4];
std::uint8_t sampling_factor[4];
std::uint32_t coefficient_mem_size;
std::uint32_t suitable_csc_attribute;
std::uint32_t output_image_width;
std::uint32_t output_image_height;
};
struct PlayGoInitParams {
@@ -1802,11 +2053,22 @@ union PlayGoOptionalChunk {
};
struct PlayGoDialogResult {
std::int32_t errorCode;
std::int32_t result;
std::int32_t reserved[9];
std::uint8_t reserved[32];
};
using RudpEventHandler = void (*)(int ctx_id, int event_id, int error_code, void* arg);
struct RudpPollEvent {
std::int32_t contextId;
std::uint16_t events;
std::uint16_t returnedEvents;
};
static_assert(sizeof(RudpPollEvent) == 8);
static_assert(offsetof(RudpPollEvent, events) == 4);
static_assert(offsetof(RudpPollEvent, returnedEvents) == 6);
using RudpEventHandler = void (APS5_VABI*)(int ctxId, int eventId, int errorCode, void* arg);
struct SystemServiceStatus {
std::int32_t event_num = 0;
@@ -1826,6 +2088,15 @@ struct SystemServiceDisplaySafeAreaInfo {
std::uint8_t reserved[128];
};
struct SystemServicePlayerDialogParam {
std::uint64_t size;
std::int32_t mode;
std::int32_t user_id;
std::uint64_t target_account_id;
std::uint8_t reserved[40];
};
static_assert(sizeof(SystemServicePlayerDialogParam) == 0x40);
struct SystemServiceHdrToneMapLuminance {
float max_full_frame_tone_map_luminance;
float max_tone_map_luminance;
@@ -1866,6 +2137,13 @@ struct SystemGestureTouchRecognizerInformation {
struct SystemGestureTouchEvent { std::uint8_t reserve[168]; };
struct SystemGestureTouchPadData {
std::int32_t pad_handle;
std::int32_t report_number;
const PadData* pad_data_buffer;
std::uint8_t reserve[8];
};
struct UserServiceLoginUserIdList { int user_id[4]; };
struct UserServiceRegisteredUserIdList { int user_id[16]; };
@@ -1887,12 +2165,16 @@ struct UserServiceGamePresets {
std::uint32_t audio_language;
};
struct UltMutexOptParam {
std::uint32_t reserved_header[2];
struct alignas(8) UltMutexOptParam {
std::uint32_t header[3];
std::uint32_t buildVersion;
std::uint32_t attribute;
std::uint32_t reserved0;
std::uint32_t reserved[27];
};
static_assert(sizeof(UltMutexOptParam) == 128);
static_assert(offsetof(UltMutexOptParam, attribute) == 16);
struct UltUlthreadRuntimeOptParam { std::uint8_t bytes[128]; };
using UltUlthreadEntry = std::int32_t (APS5_VABI *)(std::uint64_t);
@@ -1943,6 +2225,42 @@ struct VideoOutVblankStatus {
std::uint8_t pad1[6] = {};
};
struct VideoOutBufferAttribute {
std::uint32_t pixel_format;
std::uint32_t tiling_mode;
std::uint32_t aspect_ratio;
std::uint32_t width;
std::uint32_t height;
std::uint32_t pitch_in_pixel;
std::uint32_t option;
std::uint32_t reserved0;
std::uint64_t reserved1;
};
static_assert(sizeof(VideoOutBufferAttribute) == 40);
struct VideoOutFlipStatusPs4 {
std::uint64_t count = 0;
std::uint64_t processTime = 0;
std::uint64_t tsc = 0;
std::int64_t flipArg = 0;
std::uint64_t submitTsc = 0;
std::uint64_t reserved0 = 0;
std::int32_t gcQueueNum = 0;
std::int32_t flipPendingNum = 0;
std::int32_t currentBuffer = 0;
std::uint32_t reserved1 = 0;
};
static_assert(sizeof(VideoOutFlipStatusPs4) == 64);
struct VideoOutVblankStatusPs4 {
std::uint64_t count = 0;
std::uint64_t processTime = 0;
std::uint64_t tsc = 0;
std::uint8_t flags = 0;
std::uint8_t pad[7] = {};
};
static_assert(sizeof(VideoOutVblankStatusPs4) == 32);
struct VideoOutOutputStatus {
std::uint32_t resolution = 0;
std::uint32_t dynamicRange = 0;
@@ -1971,6 +2289,11 @@ struct VideoOutColorSettings {
std::uint32_t reserved[3] = {};
};
struct VideoOutColorSpaceConversionSettings {
std::uint32_t internalData[8] = {};
};
static_assert(sizeof(VideoOutColorSpaceConversionSettings) == 32);
using atexit_func_t = void (*)();
+94
View File
@@ -0,0 +1,94 @@
#include "PeImportFirst.hpp"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
namespace Allocator {
namespace {
std::uint32_t read32(const std::vector<unsigned char>& bytes, std::size_t offset) {
if (offset + 4 > bytes.size()) throw std::runtime_error("truncated image");
std::uint32_t value = 0;
std::memcpy(&value, bytes.data() + offset, sizeof(value));
return value;
}
std::uint16_t read16(const std::vector<unsigned char>& bytes, std::size_t offset) {
if (offset + 2 > bytes.size()) throw std::runtime_error("truncated image");
std::uint16_t value = 0;
std::memcpy(&value, bytes.data() + offset, sizeof(value));
return value;
}
std::string lower(std::string text) {
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return text;
}
}
void MoveImportFirst(std::vector<unsigned char>& bytes, const std::string& dll) {
const auto pe = read32(bytes, 0x3c);
if (read32(bytes, pe) != 0x00004550u) throw std::runtime_error("not a PE image");
const auto sections = read16(bytes, pe + 6);
const auto optionalSize = read16(bytes, pe + 20);
const auto optional = pe + 24;
if (read16(bytes, optional) != 0x20bu) throw std::runtime_error("not a PE32+ image");
const auto importRva = read32(bytes, optional + 112 + 8);
if (importRva == 0) throw std::runtime_error("the image has no imports");
const auto sectionTable = optional + optionalSize;
const auto fileOffset = [&](std::uint32_t rva) -> std::size_t {
for (std::uint16_t index = 0; index < sections; ++index) {
const auto header = sectionTable + index * 40u;
const auto virtualSize = read32(bytes, header + 8);
const auto virtualAddress = read32(bytes, header + 12);
const auto rawSize = read32(bytes, header + 16);
const auto rawOffset = read32(bytes, header + 20);
if (rva >= virtualAddress && rva < virtualAddress + std::max(virtualSize, rawSize)) {
if (rva - virtualAddress >= rawSize) throw std::runtime_error("RVA outside the section's file data");
return rawOffset + (rva - virtualAddress);
}
}
throw std::runtime_error("RVA outside every section");
};
const auto directory = fileOffset(importRva);
constexpr std::size_t DescriptorBytes = 20;
std::vector<std::array<unsigned char, DescriptorBytes>> descriptors;
std::size_t found = SIZE_MAX;
for (std::size_t offset = directory;; offset += DescriptorBytes) {
if (offset + DescriptorBytes > bytes.size()) throw std::runtime_error("truncated import directory");
std::array<unsigned char, DescriptorBytes> descriptor{};
std::memcpy(descriptor.data(), bytes.data() + offset, DescriptorBytes);
if (std::all_of(descriptor.begin(), descriptor.end(), [](unsigned char value) { return value == 0; })) break;
const auto nameOffset = fileOffset(read32(bytes, offset + 12));
std::string name;
for (auto at = nameOffset; at < bytes.size() && bytes[at] != 0; ++at) name.push_back(static_cast<char>(bytes[at]));
if (lower(name) == lower(dll)) found = descriptors.size();
descriptors.push_back(descriptor);
}
if (found == SIZE_MAX) throw std::runtime_error("the image does not import " + dll);
std::rotate(descriptors.begin(), descriptors.begin() + static_cast<std::ptrdiff_t>(found), descriptors.begin() + static_cast<std::ptrdiff_t>(found) + 1);
for (std::size_t index = 0; index < descriptors.size(); ++index) std::memcpy(bytes.data() + directory + index * DescriptorBytes, descriptors[index].data(), DescriptorBytes);
}
void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll) {
std::vector<unsigned char> bytes;
{
std::ifstream input(path, std::ios::binary);
if (!input) throw std::runtime_error("cannot open " + path.string());
bytes.assign(std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>());
}
MoveImportFirst(bytes, dll);
std::ofstream output(path, std::ios::binary | std::ios::trunc);
if (!output || !output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()))) throw std::runtime_error("cannot write " + path.string());
}
}
+15
View File
@@ -0,0 +1,15 @@
#ifndef CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP
#define CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP
#include <filesystem>
#include <string>
#include <vector>
namespace Allocator {
void MoveImportFirst(std::vector<unsigned char>& image, const std::string& dll);
void MoveImportFirstInFile(const std::filesystem::path& path, const std::string& dll);
}
#endif
+38
View File
@@ -0,0 +1,38 @@
#include "PeImportFirst.hpp"
#include <cstdio>
#include <exception>
#include <filesystem>
#include <string>
namespace {
int Usage() {
std::fprintf(stderr, "usage: pe_import_first <file> <dll>\n");
return 2;
}
int Run(const std::filesystem::path& file, const std::string& dll) {
try {
Allocator::MoveImportFirstInFile(file, dll);
} catch (const std::exception& error) {
std::fprintf(stderr, "pe_import_first: %s: %s\n", file.string().c_str(), error.what());
return 1;
}
return 0;
}
}
#ifdef _WIN32
int wmain(int argc, wchar_t** argv) {
if (argc != 3) return Usage();
const auto dll = std::filesystem::path(argv[2]).u8string();
return Run(argv[1], std::string(dll.begin(), dll.end()));
}
#else
int main(int argc, char** argv) {
if (argc != 3) return Usage();
return Run(argv[1], argv[2]);
}
#endif
+38
View File
@@ -0,0 +1,38 @@
#include <windows.h>
#include <stdint.h>
#include <string.h>
void* __wrap_malloc(size_t bytes) {
return HeapAlloc(GetProcessHeap(), 0, bytes != 0 ? bytes : 1);
}
void* __wrap_calloc(size_t count, size_t size) {
if (size != 0 && count > SIZE_MAX / size) return NULL;
const size_t bytes = count * size;
return HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, bytes != 0 ? bytes : 1);
}
void __wrap_free(void* block) {
if (block != NULL) HeapFree(GetProcessHeap(), 0, block);
}
void* __wrap_realloc(void* block, size_t bytes) {
if (block == NULL) return __wrap_malloc(bytes);
if (bytes == 0) {
__wrap_free(block);
return NULL;
}
return HeapReAlloc(GetProcessHeap(), 0, block, bytes);
}
char* __wrap__strdup(const char* text) {
const size_t bytes = strlen(text) + 1;
char* copy = __wrap_malloc(bytes);
if (copy != NULL) memcpy(copy, text, bytes);
return copy;
}
char* __wrap_strdup(const char* text) {
return __wrap__strdup(text);
}
@@ -0,0 +1,157 @@
#include "allocator/PeImportFirst.hpp"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
constexpr std::size_t PeOffset = 0x40;
constexpr std::size_t OptionalOffset = PeOffset + 24;
constexpr std::size_t OptionalBytes = 240;
constexpr std::size_t SectionTable = OptionalOffset + OptionalBytes;
constexpr std::uint32_t SectionRva = 0x1000;
constexpr std::size_t SectionOffset = 0x200;
constexpr std::size_t SectionBytes = 0x200;
constexpr std::size_t DescriptorBytes = 20;
void require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
void put16(std::vector<unsigned char>& bytes, std::size_t offset, std::uint16_t value) {
std::memcpy(bytes.data() + offset, &value, sizeof(value));
}
void put32(std::vector<unsigned char>& bytes, std::size_t offset, std::uint32_t value) {
std::memcpy(bytes.data() + offset, &value, sizeof(value));
}
std::uint32_t get32(const std::vector<unsigned char>& bytes, std::size_t offset) {
std::uint32_t value = 0;
std::memcpy(&value, bytes.data() + offset, sizeof(value));
return value;
}
std::vector<unsigned char> image(const std::vector<std::string>& dlls, std::uint16_t magic = 0x20b) {
std::vector<unsigned char> bytes(SectionOffset + SectionBytes, 0);
bytes[0] = 'M';
bytes[1] = 'Z';
put32(bytes, 0x3c, PeOffset);
put32(bytes, PeOffset, 0x00004550u);
put16(bytes, PeOffset + 4, 0x8664);
put16(bytes, PeOffset + 6, 1);
put16(bytes, PeOffset + 20, OptionalBytes);
put16(bytes, OptionalOffset, magic);
put32(bytes, OptionalOffset + 108, 16);
put32(bytes, OptionalOffset + 112 + 8, SectionRva);
put32(bytes, OptionalOffset + 112 + 12, static_cast<std::uint32_t>((dlls.size() + 1) * DescriptorBytes));
std::memcpy(bytes.data() + SectionTable, ".idata", 6);
put32(bytes, SectionTable + 8, SectionBytes);
put32(bytes, SectionTable + 12, SectionRva);
put32(bytes, SectionTable + 16, SectionBytes);
put32(bytes, SectionTable + 20, SectionOffset);
auto name = (dlls.size() + 1) * DescriptorBytes;
for (std::size_t index = 0; index < dlls.size(); ++index) {
const auto descriptor = SectionOffset + index * DescriptorBytes;
put32(bytes, descriptor, static_cast<std::uint32_t>(SectionRva + 0x100 + index * 0x10));
put32(bytes, descriptor + 4, static_cast<std::uint32_t>(index + 1));
put32(bytes, descriptor + 12, static_cast<std::uint32_t>(SectionRva + name));
put32(bytes, descriptor + 16, static_cast<std::uint32_t>(SectionRva + 0x180 + index * 0x10));
std::memcpy(bytes.data() + SectionOffset + name, dlls[index].c_str(), dlls[index].size() + 1);
name += dlls[index].size() + 1;
}
return bytes;
}
std::string importName(const std::vector<unsigned char>& bytes, std::size_t index) {
const auto rva = get32(bytes, SectionOffset + index * DescriptorBytes + 12);
return reinterpret_cast<const char*>(bytes.data() + SectionOffset + (rva - SectionRva));
}
std::vector<unsigned char> descriptor(const std::vector<unsigned char>& bytes, std::size_t index) {
const auto first = bytes.begin() + static_cast<std::ptrdiff_t>(SectionOffset + index * DescriptorBytes);
return {first, first + static_cast<std::ptrdiff_t>(DescriptorBytes)};
}
void writeFile(const std::filesystem::path& path, const std::vector<unsigned char>& bytes) {
std::ofstream output(path, std::ios::binary | std::ios::trunc);
output.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
require(static_cast<bool>(output), "cannot write " + path.string());
}
std::vector<unsigned char> readFile(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
require(static_cast<bool>(input), "cannot read " + path.string());
return {std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
}
void movesTheNamedImportFirst() {
const auto original = image({"kernel32.dll", "ucrtbase.dll", "mimalloc-redirect.dll", "advapi32.dll"});
auto moved = original;
Allocator::MoveImportFirst(moved, "MIMALLOC-REDIRECT.dll");
require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll" && importName(moved, 2) == "ucrtbase.dll" && importName(moved, 3) == "advapi32.dll",
"the named import did not move first, or the others changed their order");
require(descriptor(moved, 0) == descriptor(original, 2) && descriptor(moved, 1) == descriptor(original, 0) && descriptor(moved, 2) == descriptor(original, 1) && descriptor(moved, 3) == descriptor(original, 3),
"a descriptor did not move whole");
const auto directoryEnd = SectionOffset + 4 * DescriptorBytes;
require(std::equal(moved.begin(), moved.begin() + SectionOffset, original.begin()) && std::equal(moved.begin() + directoryEnd, moved.end(), original.begin() + directoryEnd),
"bytes outside the import descriptors changed");
auto unchanged = original;
Allocator::MoveImportFirst(unchanged, "kernel32.dll");
require(unchanged == original, "moving the first import changed the image");
}
void movesTheImportInAFile(const std::filesystem::path& directory) {
const auto path = directory / "moved.dll";
writeFile(path, image({"kernel32.dll", "mimalloc-redirect.dll"}));
Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll");
const auto moved = readFile(path);
require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll", "the file's import did not move first");
}
void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const std::string& what, const std::vector<unsigned char>& bytes, const std::string& expected) {
const auto path = directory / (what + ".dll");
writeFile(path, bytes);
try {
Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll");
} catch (const std::exception& error) {
require(std::string(error.what()).find(expected) != std::string::npos, what + ": failed with another error: " + error.what());
require(readFile(path) == bytes, what + ": the file changed although the move failed");
return;
}
throw std::runtime_error(what + ": the move did not fail");
}
}
int main() {
auto directory = std::filesystem::temp_directory_path() / std::filesystem::path(u8"aps5-pe-import-first-a\u00e7\u00e3o-\u65e5\u672c-");
directory += std::to_string(std::random_device{}());
try {
std::filesystem::create_directories(directory);
movesTheNamedImportFirst();
movesTheImportInAFile(directory);
failsWithoutTouchingTheFile(directory, "missing-import", image({"kernel32.dll", "ucrtbase.dll"}), "does not import mimalloc-redirect.dll");
auto truncated = image({"kernel32.dll", "mimalloc-redirect.dll", "ucrtbase.dll"});
truncated.resize(SectionOffset + DescriptorBytes + DescriptorBytes / 2);
failsWithoutTouchingTheFile(directory, "truncated-directory", truncated, "truncated import directory");
failsWithoutTouchingTheFile(directory, "pe32", image({"kernel32.dll", "mimalloc-redirect.dll"}, 0x10b), "not a PE32+ image");
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
std::error_code ignored;
std::filesystem::remove_all(directory, ignored);
return 1;
}
std::filesystem::remove_all(directory);
std::cout << "pe import first tests passed\n";
return 0;
}
+1
View File
@@ -91,6 +91,7 @@ inline bool IsDynamicNameTag(std::int64_t tag) {
return tag == kDtNeeded || tag == kDtSoname || tag == kDtRpath || tag == kDtRunpath;
}
constexpr std::uint32_t kShtDynamic = 6u;
constexpr std::uint32_t kShtHash = 5u;
constexpr std::uint32_t kShtGnuVerdef = 0x6ffffffdu;
constexpr std::uint32_t kShtGnuVerneed = 0x6ffffffeu;
constexpr std::uint32_t kShtDynsym = 11u;
@@ -8,6 +8,8 @@ namespace Nid {
std::string NormalizeExportName(const std::string& name);
std::unordered_set<std::string> ReadExportExclusions(const std::string& path);
std::unordered_set<std::string> ReadExportList(const std::string& path);
bool WriteExportList(const std::string& referencePath, const std::string& path);
}
+55
View File
@@ -4,6 +4,7 @@
#include <nid/NidCompute.hpp>
#include <algorithm>
#include <unordered_map>
#include <utility>
#include <cstdint>
#include <cstring>
#include <stdexcept>
@@ -27,6 +28,44 @@ std::uint32_t GnuHash(const std::string& name) {
return h;
}
std::uint32_t SysvHash(const std::string& name) {
std::uint32_t hash = 0u;
for (const unsigned char byte : name) {
hash = (hash << 4u) + byte;
const auto high = hash & 0xf0000000u;
hash ^= high >> 24u;
hash &= ~high;
}
return hash;
}
void ValidateSysvHash(const std::vector<std::uint8_t>& elf, std::size_t offset, std::size_t size, std::size_t symCount) {
using namespace Internal;
RequireElfRange(elf, offset, size, "hash");
if (size < 8u) throw std::runtime_error("invalid ELF hash header");
const auto buckets = Read<std::uint32_t>(elf, offset);
const auto chains = Read<std::uint32_t>(elf, offset + 4u);
if (!buckets || chains != symCount || 8u + (std::uint64_t{buckets} + chains) * 4u > size)
throw std::runtime_error("invalid ELF hash geometry");
}
void RebuildSysvHash(std::vector<std::uint8_t>& elf, std::size_t offset, const std::vector<std::string>& symbolNames) {
using namespace Internal;
const auto bucketCount = Read<std::uint32_t>(elf, offset);
std::vector<std::uint32_t> buckets(bucketCount, 0u);
std::vector<std::uint32_t> chains(symbolNames.size(), 0u);
for (std::size_t index = 1u; index < symbolNames.size(); ++index) {
if (symbolNames[index].empty()) continue;
const auto bucket = SysvHash(symbolNames[index]) % bucketCount;
chains[index] = buckets[bucket];
buckets[bucket] = static_cast<std::uint32_t>(index);
}
for (std::size_t index = 0u; index < buckets.size(); ++index)
Write(elf, offset + 8u + index * 4u, buckets[index]);
for (std::size_t index = 0u; index < chains.size(); ++index)
Write(elf, offset + 8u + buckets.size() * 4u + index * 4u, chains[index]);
}
GnuHashLayout ReadGnuHashLayout(const std::vector<std::uint8_t>& elf, std::size_t sectionOffset) {
using namespace Internal;
@@ -328,6 +367,8 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
std::size_t dynSymOffset = 0u, dynSymSize = 0u;
std::size_t gnuHashOffset = 0u, gnuHashSize = 0u;
std::size_t sysvHashOffset = 0u, sysvHashSize = 0u;
std::uint32_t sysvHashLink = 0u;
std::size_t versymOffset = 0u;
std::uint32_t dynSymSectionIndex = 0u;
std::uint32_t gnuHashSymOffset = 0u;
@@ -346,6 +387,11 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
gnuHashOffset = static_cast<std::size_t>(shdr.sh_offset);
gnuHashSize = static_cast<std::size_t>(shdr.sh_size);
}
if (shdr.sh_type == kShtHash) {
sysvHashOffset = static_cast<std::size_t>(shdr.sh_offset);
sysvHashSize = static_cast<std::size_t>(shdr.sh_size);
sysvHashLink = shdr.sh_link;
}
if (shdr.sh_type == kShtGnuVersym)
versymOffset = static_cast<std::size_t>(shdr.sh_offset);
if (shdr.sh_type == kShtGnuVerdef)
@@ -356,6 +402,10 @@ 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 (sysvHashOffset != 0u) {
if (sysvHashLink != dynSymSectionIndex) throw std::runtime_error("invalid ELF hash symbol table link");
ValidateSysvHash(elf, sysvHashOffset, sysvHashSize, symCount);
}
if (gnuHashOffset != 0u) {
ValidateGnuHash(elf, gnuHashOffset, gnuHashSize, symCount);
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
@@ -408,6 +458,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
};
std::vector<SymbolNameUse> uses;
std::unordered_map<std::string, std::pair<std::uint64_t, std::uint16_t>> patchedExports;
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);
@@ -424,6 +475,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
const auto it = nidMap.find(symName);
if (it != nidMap.end())
newValue = it->second;
const auto [existing, inserted] = patchedExports.try_emplace(newValue, sym.st_value, sym.st_shndx);
if (!inserted && existing->second != std::pair{sym.st_value, sym.st_shndx})
throw std::runtime_error("duplicate patched export name: " + newValue);
} else if (binding != kStbLocal && sym.st_shndx == kShnUndef) {
const bool isUnversioned = versymOffset == 0u ||
(Read<std::uint16_t>(elf, versymOffset + i * sizeof(std::uint16_t)) <= 1u);
@@ -510,6 +564,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
FixupRelocationSymbolIndices(elf, ehdr, dynSymSectionIndex, oldToNewIndex);
RebuildGnuHash(elf, gnuHashOffset, gnuHashSize, symCount, newSymbolNames);
}
if (sysvHashOffset != 0u) RebuildSysvHash(elf, sysvHashOffset, newSymbolNames);
}
}
+48
View File
@@ -4,10 +4,13 @@
#include <nid/MachONidPatcher.hpp>
#include <nid/NidPatcherUtils.hpp>
#include <nid/NativePath.hpp>
#include <algorithm>
#include <bit>
#include <cstring>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <vector>
namespace Nid {
@@ -113,6 +116,24 @@ std::unordered_set<std::string> ReadElfExports(const std::vector<std::uint8_t>&
return result;
}
std::string ReadText(const std::string& path, std::ifstream& input) {
std::string text{std::istreambuf_iterator<char>(input), {}};
if (input.bad()) throw std::runtime_error("cannot read export list: " + path);
return text;
}
std::string FormatExportList(const std::unordered_set<std::string>& exports) {
std::vector<std::string> names(exports.begin(), exports.end());
std::ranges::sort(names);
std::string text;
for (const auto& name : names) {
if (name.find('\n') != std::string::npos) throw std::runtime_error("export name contains a line break: " + name);
text += name;
text += '\n';
}
return text;
}
}
std::string NormalizeExportName(const std::string& name) {
@@ -153,4 +174,31 @@ std::unordered_set<std::string> ReadExportExclusions(const std::string& path) {
return result;
}
std::unordered_set<std::string> ReadExportList(const std::string& path) {
std::ifstream input(path, std::ios::binary);
if (!input) throw std::runtime_error("cannot open export list: " + path);
const auto text = ReadText(path, input);
if (text.empty() || text.back() != '\n') throw std::runtime_error("malformed export list: " + path);
std::unordered_set<std::string> result;
for (std::size_t start = 0; start < text.size();) {
const auto end = text.find('\n', start);
if (end == start) throw std::runtime_error("empty name in export list: " + path);
auto name = text.substr(start, end - start);
if (!result.insert(name).second) throw std::runtime_error("duplicate name in export list: " + name);
start = end + 1;
}
return result;
}
bool WriteExportList(const std::string& referencePath, const std::string& path) {
const auto text = FormatExportList(ReadExportExclusions(referencePath));
if (std::ifstream existing(path, std::ios::binary); existing && ReadText(path, existing) == text) return false;
std::ofstream output(path, std::ios::binary | std::ios::trunc);
if (!output) throw std::runtime_error("cannot write export list: " + path);
output.write(text.data(), static_cast<std::streamsize>(text.size()));
output.close();
if (!output) throw std::runtime_error("cannot write export list: " + path);
return true;
}
}
+78 -19
View File
@@ -8,6 +8,7 @@
#include <limits>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>
namespace Nid {
@@ -116,7 +117,45 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
if (stringsRegionStart == std::numeric_limits<std::uint32_t>::max())
throw std::runtime_error("could not determine strings region start");
const std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment;
std::uint32_t rawLimit = ((edataSection.SizeOfRawData + fileAlignment - 1u) / fileAlignment) * fileAlignment;
const auto dllName = ReadCStr(pe, RvaToOffset(pe, exportTable.Name, peHeaderOffset, numberOfSections, sizeOfOptionalHeader));
const bool repackDllName = exportTable.Name >= edataSection.VirtualAddress &&
exportTable.Name - edataSection.VirtualAddress < rawLimit;
if (repackDllName) stringsRegionStart = std::min(stringsRegionStart, exportTable.Name - edataSection.VirtualAddress);
struct Forwarder {
std::size_t EntryOffset;
std::string Target;
};
std::vector<Forwarder> forwarders;
std::vector<std::uint32_t> directRvas;
const auto functionsArrayOffset = RvaToOffset(pe, exportTable.AddressOfFunctions, peHeaderOffset, numberOfSections, sizeOfOptionalHeader);
for (std::uint32_t index = 0u; index < exportTable.NumberOfFunctions; ++index) {
const auto entryOffset = functionsArrayOffset + index * sizeof(std::uint32_t);
const auto rva = Read<std::uint32_t>(pe, entryOffset);
if (rva < exportDir.VirtualAddress || std::uint64_t{rva} - exportDir.VirtualAddress >= exportDir.Size) {
directRvas.push_back(rva);
continue;
}
const auto target = ReadCStr(pe, RvaToOffset(pe, rva, peHeaderOffset, numberOfSections, sizeOfOptionalHeader));
if (target.size() + 1u > exportDir.Size - (rva - exportDir.VirtualAddress))
throw std::runtime_error("forwarder string outside export directory");
forwarders.push_back({entryOffset, target});
stringsRegionStart = std::min(stringsRegionStart, rva - edataSection.VirtualAddress);
}
for (const auto rva : directRvas) {
if (rva >= edataSection.VirtualAddress && rva - edataSection.VirtualAddress >= stringsRegionStart)
rawLimit = std::min(rawLimit, rva - edataSection.VirtualAddress);
}
const auto reserveTable = [&](std::uint32_t rva, std::uint64_t size) {
if (rva < edataSection.VirtualAddress || rva - edataSection.VirtualAddress >= rawLimit) return;
const auto end = std::uint64_t{rva - edataSection.VirtualAddress} + size;
if (end > rawLimit) throw std::runtime_error("export table outside edata section");
stringsRegionStart = std::max(stringsRegionStart, static_cast<std::uint32_t>(end));
};
reserveTable(exportDir.VirtualAddress, sizeof(PeExportDirectory));
reserveTable(exportTable.AddressOfFunctions, std::uint64_t{exportTable.NumberOfFunctions} * 4u);
reserveTable(exportTable.AddressOfNames, std::uint64_t{exportTable.NumberOfNames} * 4u);
reserveTable(exportTable.AddressOfNameOrdinals, std::uint64_t{exportTable.NumberOfNames} * 2u);
std::vector<std::string> finalNames(exportTable.NumberOfNames);
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) {
@@ -125,28 +164,48 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
finalNames[i] = it->second;
}
std::uint32_t requiredSize = stringsRegionStart;
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i)
requiredSize += static_cast<std::uint32_t>(finalNames[i].size()) + 1u;
std::vector<std::uint8_t> strings;
std::unordered_map<std::string, std::uint32_t> stringRvas;
const auto storeString = [&](const std::string& name) {
const auto existing = stringRvas.find(name);
if (existing != stringRvas.end()) return existing->second;
const auto rva = edataSection.VirtualAddress + stringsRegionStart + static_cast<std::uint32_t>(strings.size());
stringRvas.emplace(name, rva);
strings.insert(strings.end(), name.begin(), name.end());
strings.push_back(0u);
return rva;
};
std::vector<std::uint32_t> forwarderRvas;
for (const auto& forwarder : forwarders) forwarderRvas.push_back(storeString(forwarder.Target));
if (!forwarders.empty() && (std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart < exportDir.VirtualAddress ||
std::uint64_t{edataSection.VirtualAddress} + stringsRegionStart + strings.size() > std::uint64_t{exportDir.VirtualAddress} + exportDir.Size))
throw std::runtime_error("not enough export directory space to repack forwarders");
const auto dllNameRva = repackDllName ? storeString(dllName) : exportTable.Name;
std::vector<std::uint32_t> finalNameRvas;
for (const auto& name : finalNames) finalNameRvas.push_back(storeString(name));
const auto requiredSize = std::uint64_t{stringsRegionStart} + strings.size();
if (requiredSize > rawLimit)
throw std::runtime_error("not enough raw space in edata section to repack export names");
std::uint32_t writeOffset = stringsRegionStart;
for (std::uint32_t i = 0u; i < exportTable.NumberOfNames; ++i) {
const std::uint32_t newNameRva = edataSection.VirtualAddress + writeOffset;
const std::size_t newNameFileOffset = static_cast<std::size_t>(edataSection.PointerToRawData) + writeOffset;
if (newNameFileOffset + finalNames[i].size() + 1u > pe.size())
throw std::runtime_error("repack write out of file bounds");
std::memcpy(pe.data() + newNameFileOffset, finalNames[i].data(), finalNames[i].size());
pe[newNameFileOffset + finalNames[i].size()] = 0u;
Write(pe, namesArrayOffset + i * 4u, newNameRva);
writeOffset += static_cast<std::uint32_t>(finalNames[i].size()) + 1u;
const auto directoryEnd = std::max(std::uint64_t{exportDir.VirtualAddress} + exportDir.Size,
std::uint64_t{edataSection.VirtualAddress} + requiredSize);
for (const auto rva : directRvas) {
if (rva != 0u && rva >= exportDir.VirtualAddress && std::uint64_t{rva} < directoryEnd)
throw std::runtime_error("repacked export directory would cover a direct export");
}
const auto directorySize = directoryEnd - exportDir.VirtualAddress;
if (directorySize > std::numeric_limits<std::uint32_t>::max()) throw std::runtime_error("export directory too large");
const auto stringFileOffset = static_cast<std::size_t>(edataSection.PointerToRawData) + stringsRegionStart;
if (stringFileOffset > pe.size() || strings.size() > pe.size() - stringFileOffset)
throw std::runtime_error("repack write out of file bounds");
std::memcpy(pe.data() + stringFileOffset, strings.data(), strings.size());
for (std::size_t index = 0u; index < finalNameRvas.size(); ++index)
Write(pe, namesArrayOffset + index * 4u, finalNameRvas[index]);
for (std::size_t index = 0u; index < forwarders.size(); ++index)
Write(pe, forwarders[index].EntryOffset, forwarderRvas[index]);
Write(pe, exportDirOffset + offsetof(PeExportDirectory, Name), dllNameRva);
Write(pe, dataDirectoryOffset + offsetof(PeDataDirectory, Size), static_cast<std::uint32_t>(directorySize));
const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, writeOffset);
const std::uint32_t newVirtualSize = std::max(edataSection.VirtualSize, static_cast<std::uint32_t>(requiredSize));
auto patchedSection = edataSection;
patchedSection.VirtualSize = newVirtualSize;
Write(pe, edataSectionOffset, patchedSection);
+21 -4
View File
@@ -19,16 +19,33 @@ void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data)
std::ofstream f(Nid::Internal::NativePath(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()));
f.close();
if (!f) throw std::runtime_error("cannot write: " + path);
}
}
int Run(int argc, char* argv[]) {
if (argc < 3) {
std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library>] <file> [file2 ...]\n";
std::cerr << "Usage: nid_patcher <library_name> [--preserve-exports <unpatched-library> | --preserve-export-list <export-list>] <file> [file2 ...]\n"
" nid_patcher --write-export-list <unpatched-library> <export-list>\n";
return 1;
}
if (std::string(argv[1]) == "--write-export-list") {
if (argc != 4) {
std::cerr << "FAIL: --write-export-list requires a reference library and an export list\n";
return 2;
}
try {
std::cout << (Nid::WriteExportList(argv[2], argv[3]) ? "WROTE: " : "UNCHANGED: ") << argv[3] << "\n";
} catch (const std::exception& e) {
std::cerr << "FAIL: " << argv[3] << ": " << e.what() << "\n";
return 2;
}
return 0;
}
const std::string libraryName = argv[1];
std::unordered_set<std::string> excludedExports;
std::vector<std::string> paths;
@@ -36,10 +53,10 @@ int Run(int argc, char* argv[]) {
bool hasReference = false;
for (int i = 2; i < argc; ++i) {
const std::string argument = argv[i];
if (argument == "--preserve-exports") {
if (hasReference || i + 1 == argc) throw std::runtime_error("--preserve-exports requires exactly one reference library");
if (argument == "--preserve-exports" || argument == "--preserve-export-list") {
if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference");
hasReference = true;
excludedExports = Nid::ReadExportExclusions(argv[++i]);
excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]);
} else {
if (argument.starts_with("--")) throw std::runtime_error("unknown option: " + argument);
paths.push_back(argument);
@@ -0,0 +1,69 @@
import base64
import hashlib
import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, patch, read_dynsym, IMPORTS, NID_NAME, SHN_UNDEF, STT_FUNC
def nid(name):
digest = hashlib.sha1(name.encode() + bytes.fromhex("518d64a635ded8c1e6b039b1c3e55230")).digest()
return base64.b64encode(digest[:8][::-1]).decode().rstrip("=").replace("/", "-")
def image(names, addresses=None):
if addresses is None:
addresses = [0x1200 + index * 0x20 for index in range(len(names))]
entries = [(None, 1, SHN_UNDEF, 0, 0)]
entries += [(name, 1, SHN_UNDEF, 0, 0) for name in IMPORTS]
entries += [(name, 1, 9, STT_FUNC, address) for name, address in zip(names, addresses)]
return bytes(build_image(entries, False))
class ElfDuplicateTests(unittest.TestCase):
def test_exports_patched_to_one_name_are_rejected(self):
cases = (
["sceAlphaComputeLength", "sceAlphaComputeLength_nid_disambig1", "sceBetaCopyString"],
["sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_disambig12", "sceBetaCopyString"],
["sceBetaCopyString", "sceAlphaComputeLength_nid_no_patch", "sceAlphaComputeLength_nid_no_patch_nid_no_patch_cut"],
)
for names in cases:
with self.subTest(names=names):
before = image(names)
result, after = patch(PATCHER, before, "duplicates.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertIn(b"duplicate patched export name", result.stderr)
self.assertEqual(after, before)
def test_aliases_of_one_address_are_patched(self):
names = ["sceAlphaComputeLength", "sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_no_patch_cut", "sceBetaCopyString"]
result, after = patch(PATCHER, image(names, [0x1200, 0x1200, 0x1200, 0x1220]), "aliases.so")
self.assertEqual(result.returncode, 0, result.stderr)
patched = [(entry[0], entry[2]) for entry in read_dynsym(after) if entry[1] == 9]
self.assertEqual(sorted(name for name, address in patched if address == 0x1200),
sorted(["sceAlphaComputeLength", "sceAlphaComputeLength", nid("sceAlphaComputeLength")]))
self.assertEqual([name for name, address in patched if address == 0x1220], [nid("sceBetaCopyString")])
self.assertTrue(NID_NAME.match(nid("sceAlphaComputeLength")))
def test_aliases_of_different_addresses_are_rejected(self):
names = ["sceAlphaComputeLength", "sceAlphaComputeLength_nid_postfix", "sceAlphaComputeLength_nid_no_patch_cut"]
before = image(names, [0x1200, 0x1200, 0x1220])
result, after = patch(PATCHER, before, "aliases.so")
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
self.assertIn(b"duplicate patched export name: sceAlphaComputeLength", result.stderr)
self.assertEqual(after, before)
def test_distinct_exports_are_still_patched(self):
names = ["sceAlphaComputeLength_nid_disambig1", "sceBetaCopyString_nid_postfix", "sceGammaKeep_nid_no_patch_cut"]
result, after = patch(PATCHER, image(names), "distinct.so")
self.assertEqual(result.returncode, 0, result.stderr)
patched = [entry[0] for entry in read_dynsym(after) if entry[1] == 9]
self.assertEqual(len(patched), 3)
self.assertEqual(len(set(patched)), 3)
self.assertIn("sceGammaKeep", patched)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()
+83 -1
View File
@@ -3,7 +3,32 @@ import sys
import unittest
from pathlib import Path
from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash
from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash, sections
def sysv_hash(name):
value = 0
for byte in name.encode("utf-8"):
value = ((value << 4) + byte) & 0xFFFFFFFF
high = value & 0xF0000000
value = (value ^ (high >> 24)) & ~high
return value
def sysv_lookup(image, name):
table = next(section for section in sections(image) if section[1] == 5)
buckets, chains = struct.unpack_from("<II", image, table[4])
entries = read_dynsym(image)
index = struct.unpack_from("<I", image, table[4] + 8 + (sysv_hash(name) % buckets) * 4)[0]
visited = set()
while index:
if index >= chains or index in visited:
raise AssertionError("invalid SysV hash chain")
visited.add(index)
if entries[index][0] == name:
return index
index = struct.unpack_from("<I", image, table[4] + 8 + buckets * 4 + index * 4)[0]
return None
class GnuHashTests(unittest.TestCase):
@@ -70,6 +95,63 @@ class GnuHashTests(unittest.TestCase):
self.assertEqual(struct.unpack_from("<I", after, HASH_OFFSET)[0], 0)
class SysvHashTests(unittest.TestCase):
def image(self, both, buckets):
entries = symbols(False)
entries.append(("retained_nid_no_patch", 1, 9, 2, 0x1400))
entries[3:] = sorted(entries[3:], key=lambda entry: gnu_hash(entry[0]) % 4)
image = build_image(entries, False)
offset = 0x600
size = 8 + (buckets + len(entries)) * 4
struct.pack_into("<II", image, offset, buckets, len(entries))
for index, entry in enumerate(entries[1:], 1):
bucket_offset = offset + 8 + (sysv_hash(entry[0]) % buckets) * 4
head = struct.unpack_from("<I", image, bucket_offset)[0]
struct.pack_into("<I", image, offset + 8 + buckets * 4 + index * 4, head)
struct.pack_into("<I", image, bucket_offset, index)
section = (0, 5, 2, 0, offset, size, 1, 0, 4, 4)
struct.pack_into("<IIQQQQIIQQ", image, SHOFF + (4 if both else 3) * 64, *section)
if both:
struct.pack_into("<H", image, 0x3C, 5)
return image
def test_rebuilt_sysv_hash_finds_renamed_and_preserved_symbols(self):
for both in (False, True):
for buckets in (1, 3, 7):
with self.subTest(both=both, buckets=buckets):
image = self.image(both, buckets)
before = read_dynsym(image)
for name, _, _ in before[1:]:
self.assertIsNotNone(sysv_lookup(image, name), name)
result, after = patch(PATCHER, image, "sysv.so")
self.assertEqual(result.returncode, 0, result.stderr)
entries = read_dynsym(after)
if not both:
self.assertEqual([(entry[1], entry[2]) for entry in entries],
[(entry[1], entry[2]) for entry in before])
else:
self.assertNotEqual([entry[2] for entry in entries], [entry[2] for entry in before])
for index, (name, _, _) in enumerate(entries[1:], 1):
self.assertEqual(sysv_lookup(after, name), index, name)
self.assertEqual(entries[sysv_lookup(after, "retained_nid_no_patch")][2], 0x1400)
for name, _, value in before[3:]:
if name != "retained_nid_no_patch":
self.assertIsNone(sysv_lookup(after, name), name)
renamed = next(entry[0] for entry in entries if entry[2] == value)
self.assertRegex(renamed, r"^[A-Za-z0-9+-]{11}$")
self.assertIsNone(sysv_lookup(after, "missing_nid_no_patch"))
def test_invalid_sysv_geometry_is_rejected_without_writing(self):
for field, value in ((0, 0), (4, 1000), (4, 1)):
with self.subTest(field=field, value=value):
image = self.image(False, 3)
struct.pack_into("<I", image, 0x600 + field, value)
result, after = patch(PATCHER, image, "invalid-sysv.so")
self.assertEqual(result.returncode, 2, result.stderr)
self.assertIn(b"hash", result.stderr)
self.assertEqual(after, image)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()
+49
View File
@@ -1,6 +1,7 @@
"""Check nid_patcher on small byte-built Mach-O dylibs: the export trie, the chained imports and what it refuses."""
import hashlib
import os
import struct
import subprocess
import sys
@@ -211,6 +212,53 @@ def check_preserved(patcher, directory):
assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes())
def write_export_list(patcher, reference, listing):
return subprocess.run([str(patcher), "--write-export-list", str(reference), str(listing)], capture_output=True, text=True, timeout=30)
def check_export_list(patcher, directory):
reference = Path(directory) / "listed.dylib"
listing = Path(directory) / "listed.exports"
reference.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit", 0, 0x108)], []))
result = write_export_list(patcher, reference, listing)
assert result.returncode == 0, (result.stdout, result.stderr)
assert listing.read_bytes() == b"exit\nputs\n", listing.read_bytes()
os.utime(listing, ns=(1_000_000_000_000_000_000, 1_000_000_000_000_000_000))
reference.write_bytes(build_dylib([("_exit", 0, 0x110), ("_puts_nid_postfix", 0, 0x118)], []))
result = write_export_list(patcher, reference, listing)
assert result.returncode == 0, (result.stdout, result.stderr)
assert listing.stat().st_mtime_ns == 1_000_000_000_000_000_000, "an unchanged export list was rewritten"
reference.write_bytes(build_dylib([("_puts", 0, 0x100)], []))
result = write_export_list(patcher, reference, listing)
assert result.returncode == 0, (result.stdout, result.stderr)
assert listing.read_bytes() == b"puts\n", listing.read_bytes()
assert listing.stat().st_mtime_ns != 1_000_000_000_000_000_000, "a changed export list kept its time"
path = Path(directory) / "listed-preserve.dylib"
path.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], []))
result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30)
assert result.returncode == 0, (result.stdout, result.stderr)
assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes())
malformed_lists = [
("unterminated", b"puts", "malformed export list"),
("blank", b"puts\n\nexit\n", "empty name in export list"),
("twice", b"puts\nputs\n", "duplicate name in export list"),
("empty", b"", "malformed export list"),
]
for name, content, message in malformed_lists:
malformed = Path(directory) / (name + ".exports")
malformed.write_bytes(content)
target = Path(directory) / (name + ".dylib")
target.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], []))
result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(malformed), str(target)], capture_output=True, text=True, timeout=30)
assert result.returncode != 0 and message in result.stdout + result.stderr, (name, result.stdout, result.stderr)
result = subprocess.run([str(patcher), "libtest", "--preserve-exports", str(reference), "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30)
assert result.returncode != 0 and "exactly one reference" in result.stdout + result.stderr, (result.stdout, result.stderr)
def check_refusals(patcher, directory):
growing = [("_ab", 0, 0x100), ("_cd", 0, 0x108)]
result, _ = patch(patcher, directory, "signed.dylib", build_dylib(growing, [], signed=True))
@@ -227,6 +275,7 @@ def main():
check_shrinking(patcher, directory)
check_growing(patcher, directory)
check_preserved(patcher, directory)
check_export_list(patcher, directory)
check_refusals(patcher, directory)
print("NID patcher Mach-O tests passed")
+27
View File
@@ -1,4 +1,5 @@
import subprocess
import signal
import sys
import tempfile
import unittest
@@ -6,6 +7,11 @@ from pathlib import Path
from test_macho_nids import build_dylib, exports_of, nid
try:
import resource
except ImportError:
resource = None
PATCHER = Path(sys.argv.pop(1)).resolve()
@@ -51,6 +57,27 @@ class NativePathTests(unittest.TestCase):
self.assertFalse(missing.exists())
self.assertEqual(existing.read_bytes(), b"unchanged")
@unittest.skipUnless(resource is not None and hasattr(signal, "SIGXFSZ"), "requires POSIX file size limits")
def test_failed_file_write_is_reported_without_success(self):
for limit in (0, 1024):
with self.subTest(limit=limit), tempfile.TemporaryDirectory() as temporary:
file = Path(temporary) / "library.prx"
original = build_dylib([("_puts_nid_postfix", 0, 0x100)], [])
file.write_bytes(original)
def restrict_output():
signal.signal(signal.SIGXFSZ, signal.SIG_IGN)
resource.setrlimit(resource.RLIMIT_FSIZE, (limit, limit))
result = subprocess.run([str(PATCHER), "libtest", str(file)], capture_output=True,
encoding="utf-8", errors="replace", timeout=10,
preexec_fn=restrict_output)
self.assertEqual(file.stat().st_size, limit)
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
self.assertIn("cannot write", result.stderr)
self.assertIn(str(file), result.stderr)
self.assertNotIn("OK:", result.stdout)
if __name__ == "__main__":
unittest.main()
+177
View File
@@ -29,6 +29,77 @@ def reference(*, headers=False, address=0x2000, ordinal=0):
return image
def forwarding_image(names, bits=64, dll_last=False, direct_data=False):
optional_size = 240 if bits == 64 else 224
directory_offset = 112 if bits == 64 else 96
image = bytearray(1536)
image[:2] = b"MZ"
struct.pack_into("<I", image, 0x3C, 0x40)
struct.pack_into("<IHHIIIHH", image, 0x40, 0x4550, 0x8664 if bits == 64 else 0x14C,
2, 0, 0, 0, optional_size, 0x2022)
struct.pack_into("<H", image, 0x58, 0x20B if bits == 64 else 0x10B)
struct.pack_into("<II", image, 0x58 + 32, 4096, 512)
struct.pack_into("<II", image, 0x58 + 56, 0x3000, 512)
struct.pack_into("<I", image, 0x58 + directory_offset - 4, 16)
functions = [0x2000, "KERNEL32.GetCurrentProcessId", "NTDLL.#123"]
functions += [0x2000] * (len(names) - 3)
functions.append(functions[1])
count = len(functions)
strings_start = 40 + count * 4 + len(names) * 6
data = bytearray(strings_start)
def append(text):
rva = 0x1000 + len(data)
data.extend(text.encode() + b"\0")
return rva
dll_rva = None if dll_last else append("forwarder.dll")
function_rvas, name_rvas = [], []
for index, function in enumerate(functions):
function_rvas.append(append(function) if isinstance(function, str) else function)
if index < len(names):
name_rvas.append(append(names[index]))
if dll_last:
dll_rva = append("forwarder.dll")
for index, rva in enumerate(function_rvas):
struct.pack_into("<I", data, 40 + index * 4, rva)
for index, rva in enumerate(name_rvas):
struct.pack_into("<I", data, 40 + count * 4 + index * 4, rva)
struct.pack_into("<H", data, 40 + count * 4 + len(names) * 4 + index * 2, index)
struct.pack_into("<IIHHIIIIIII", data, 0, 0, 0, 0, 0, dll_rva, 1, count, len(names),
0x1000 + 40, 0x1000 + 40 + count * 4, 0x1000 + 40 + count * 4 + len(names) * 4)
struct.pack_into("<II", image, 0x58 + directory_offset, 0x1000, len(data))
for index, (name, va, offset) in enumerate(((b".edata", 0x1000, 512), (b".text", 0x2000, 1024))):
struct.pack_into("<8sIIIIIIHHI", image, 0x58 + optional_size + index * 40,
name, len(data) if index == 0 else 512, va, 512, offset, 0, 0, 0, 0, 0x40000040)
if direct_data:
struct.pack_into("<I", data, 40, 0x1000 + len(data))
image[512 + len(data):512 + len(data) + 4] = b"DATA"
struct.pack_into("<I", image, 0x58 + optional_size + 8, len(data) + 4)
if len(data) > 512:
raise ValueError("fixture exceeds the export section")
image[512:512 + len(data)] = data
return image
def forwarding_exports(image, bits):
directory_offset = 112 if bits == 64 else 96
export_rva, export_size = struct.unpack_from("<II", image, 0x58 + directory_offset)
table = struct.unpack_from("<IIHHIIIIIII", image, 512)
def string(rva):
offset = 512 + rva - 0x1000
return image[offset:image.index(0, offset)].decode()
addresses = [struct.unpack_from("<I", image, 512 + table[8] - 0x1000 + index * 4)[0]
for index in range(table[6])]
names = [(string(struct.unpack_from("<I", image, 512 + table[9] - 0x1000 + index * 4)[0]),
struct.unpack_from("<H", image, 512 + table[10] - 0x1000 + index * 2)[0])
for index in range(table[7])]
targets = [string(rva) if export_rva <= rva < export_rva + export_size else rva for rva in addresses]
return string(table[4]), names, targets, addresses, (export_rva, export_size)
class ReferenceTests(unittest.TestCase):
def patch_with_reference(self, data):
with tempfile.TemporaryDirectory() as tmp:
@@ -64,6 +135,112 @@ class ReferenceTests(unittest.TestCase):
self.assertEqual(image, original)
class ForwardingExportTests(unittest.TestCase):
def patch(self, image):
with tempfile.TemporaryDirectory() as temporary:
path = Path(temporary) / "forwarder.dll"
path.write_bytes(image)
result = subprocess.run([str(PATCHER), "libforwarder", str(path)], capture_output=True, text=True)
return result, path.read_bytes()
def test_forwarder_targets_and_ordinals_survive_name_repacking(self):
names = ["a", "b", "retained_nid_no_patch"]
for bits in (32, 64):
for dll_last in (False, True):
with self.subTest(bits=bits, dll_last=dll_last):
image = forwarding_image(names, bits, dll_last)
before = forwarding_exports(image, bits)
result, after = self.patch(image)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
dll, exports, targets, addresses, directory = forwarding_exports(after, bits)
self.assertEqual(dll, "forwarder.dll")
self.assertEqual(directory[0], before[4][0])
self.assertGreaterEqual(directory[1], before[4][1])
self.assertEqual(targets, before[2])
self.assertEqual(addresses[0], 0x2000)
self.assertFalse(directory[0] <= addresses[0] < sum(directory))
for rva in addresses[1:]:
self.assertTrue(directory[0] <= rva < sum(directory))
self.assertEqual(addresses[1], addresses[-1])
expected = {nid("a"): 0, nid("b"): 1, "retained_nid_no_patch": 2}
self.assertEqual(exports, sorted(expected.items()))
self.assertNotIn("a", dict(exports))
self.assertNotIn("missing", dict(exports))
def test_directory_grows_to_include_repacked_names(self):
image = forwarding_image(["a", "b", "c"])
before = forwarding_exports(image, 64)
result, after = self.patch(image)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
_, exports, targets, _, directory = forwarding_exports(after, 64)
self.assertGreater(directory[1], before[4][1])
self.assertEqual(targets, before[2])
self.assertEqual(exports, sorted((nid(name), index) for index, name in enumerate(["a", "b", "c"])))
table = struct.unpack_from("<IIHHIIIIIII", after, 512)
for index in range(table[7]):
rva = struct.unpack_from("<I", after, 512 + table[9] - 0x1000 + index * 4)[0]
self.assertLess(rva + len(exports[index][0]), sum(directory))
def test_long_names_reclaim_space_and_preserve_direct_data_classification(self):
names = ["sce" + str(index) + "LongExportName" * 4 for index in range(5)]
image = forwarding_image(names, direct_data=True)
before = forwarding_exports(image, 64)
self.assertGreater(before[4][1] + len(names) * 12, 512)
result, after = self.patch(image)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
dll, exports, targets, _, directory = forwarding_exports(after, 64)
self.assertEqual(dll, "forwarder.dll")
self.assertEqual(directory, before[4])
self.assertEqual(targets, before[2])
self.assertEqual(exports, sorted((nid(name), index) for index, name in enumerate(names)))
direct_offset = 512 + targets[0] - 0x1000
self.assertEqual(after[direct_offset:direct_offset + 4], b"DATA")
def test_name_growth_cannot_overwrite_direct_export_data(self):
image = forwarding_image(["a", "b", "c"], direct_data=True)
result, after = self.patch(image)
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
self.assertIn("not enough raw space", result.stderr)
self.assertEqual(after, image)
def test_external_dll_name_stays_outside_the_repacked_section(self):
image = reference(address=0x1054)
image.extend(bytes(1536 - len(image)))
optional = 0x58
struct.pack_into("<II", image, optional + 32, 4096, 512)
struct.pack_into("<I", image, 512 + 12, 0x2010)
struct.pack_into("<II", image, optional + 240 + 8, 512, 0x1000)
struct.pack_into("<I", image, optional + 240 + 16, 512)
struct.pack_into("<8sIIIIIIHHI", image, optional + 240 + 40,
b".rdata", 512, 0x2000, 512, 1024, 0, 0, 0, 0, 0x40000040)
image[512 + 84:512 + 92] = b"ABCDEFGH"
name = b"this_metadata_should_stay_outside_edata.dll\0"
image[1040:1040 + len(name)] = name
result, after = self.patch(image)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(struct.unpack_from("<I", after, 512 + 12)[0], 0x2010)
self.assertEqual(after[1040:1040 + len(name)], name)
self.assertEqual(after[512 + 84:512 + 92], b"ABCDEFGH")
self.assertEqual(struct.unpack_from("<I", after, 512 + 40)[0], 0x1054)
def test_directory_growth_cannot_reclassify_a_direct_export(self):
image = reference(address=0x1054)
image.extend(bytes(1536 - len(image)))
struct.pack_into("<II", image, 0x58 + 32, 4096, 512)
struct.pack_into("<II", image, 0x58 + 240 + 8, 512, 0x1000)
struct.pack_into("<I", image, 0x58 + 240 + 16, 512)
struct.pack_into("<8sIIIIIIHHI", image, 0x58 + 240 + 40,
b".rdata", 512, 0x2000, 512, 1024, 0, 0, 0, 0, 0x40000040)
struct.pack_into("<I", image, 512 + 12, 0x2010)
image[1040:1040 + 13] = b"external.dll\0"
struct.pack_into("<I", image, 512 + 44, 0x1090)
image[512 + 144:512 + 144 + 17] = b"puts_nid_postfix\0"
result, after = self.patch(image)
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
self.assertIn("cover a direct export", result.stderr)
self.assertEqual(after, image)
if __name__ == "__main__":
PATCHER = Path(sys.argv.pop(1)).resolve()
unittest.main()
@@ -0,0 +1,10 @@
#include <cstddef>
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
APS5_DUMMY_FUN
}
@@ -0,0 +1,10 @@
#include <cstddef>
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
APS5_DUMMY_FUN
}
@@ -0,0 +1,10 @@
#include <cstddef>
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
APS5_DUMMY_FUN
}
@@ -0,0 +1,10 @@
#include <cstddef>
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
APS5_DUMMY_FUN
}
@@ -0,0 +1,10 @@
#include <cstddef>
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
APS5_DUMMY_FUN
}
+43
View File
@@ -0,0 +1,43 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
int APS5_VABI sceAc3EncCreateEncoder() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncDeleteEncoder() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncEncode() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncEncodeAsync() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncFlush() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncIsEncoding() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAc3EncWaitEncode() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
+178 -21
View File
@@ -1,67 +1,224 @@
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <atomic>
#include <mutex>
#include <unordered_set>
#include <unordered_map>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
// Audio co-processor batches complete immediately without producing output; audio is silent.
static constexpr int SCE_ACM_ERROR_INVALID_PARAMETER = static_cast<int>(0x80E30002);
static std::atomic<uint32_t> g_nextId{1};
namespace {
constexpr int SCE_ACM_ERROR_BAD_CONTEXT = static_cast<int>(0x81940002);
constexpr int SCE_ACM_ERROR_BAD_POINTER = static_cast<int>(0x81940006);
std::mutex g_mutex;
std::unordered_set<AcmContextId> g_contexts;
std::atomic<uint32_t> g_nextId{1};
}
extern "C" {
int APS5_VABI sceAcmBatchStartBuffer(AcmContextId context, const void* batch_commands, size_t batch_size, AcmBatchError* batch_error, AcmBatchId* batch) {
(void)context;
(void)batch_commands;
(void)batch_size;
(void)batch_error;
if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER;
*batch = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!batch) return SCE_ACM_ERROR_BAD_POINTER;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error));
*batch = id;
return 0;
}
int APS5_VABI sceAcmBatchStartBuffers(AcmContextId context, uint32_t batch_info_count, const AcmBatchInfo* const batch_info[], AcmBatchError* batch_error, AcmBatchId* batch) {
(void)context;
(void)batch_info_count;
(void)batch_info;
(void)batch_error;
if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER;
*batch = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!batch) return SCE_ACM_ERROR_BAD_POINTER;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error));
*batch = id;
return 0;
}
int APS5_VABI sceAcmBatchWait(AcmContextId context, AcmBatchId batch, uint32_t timeout) {
(void)context;
(void)batch;
(void)timeout;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
return 0;
}
int APS5_VABI sceAcmContextCreate(AcmContextId* context) {
if (!context) return SCE_ACM_ERROR_INVALID_PARAMETER;
*context = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!context) return SCE_ACM_ERROR_BAD_POINTER;
AcmContextId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(g_mutex);
g_contexts.insert(id);
}
*context = id;
return 0;
}
int APS5_VABI sceAcmContextDestroy(AcmContextId context) {
(void)context;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
auto it = g_contexts.find(context);
if (it == g_contexts.end()) return SCE_ACM_ERROR_BAD_CONTEXT;
g_contexts.erase(it);
return 0;
}
// Builds a convolution-reverb command into a batch; batches execute as no-ops, so nothing is encoded.
int APS5_VABI sceAcm_ConvReverb_SharedInput(void) {
int APS5_VABI sceAcm_ConvReverb_SharedInput(AcmBatchInfo* batch, std::uint32_t mode, AcmReverbInput* input, std::uint32_t outputCount, const AcmReverbFilter* const* filters, const float* gains, const AcmReverbOutput* const* outputs) {
return 0;
}
int APS5_VABI sceAcm_FFT(void) {
int APS5_VABI sceAcm_FFT(AcmBatchInfo* batch, std::int32_t transformSize, std::uint32_t transformCount, std::int32_t inputFormat, const void* const* inputs, std::int32_t outputFormat, void* const* outputs, std::int32_t flags) {
if (!batch || transformCount == 0 || transformCount > 0x7FFFFFFF || !inputs || !outputs || (flags & 3) == 3 || inputFormat < 0 || inputFormat > 1 || outputFormat < 0 || outputFormat > 1) return SCE_ACM_ERROR_BAD_POINTER;
if (transformSize != 512 && transformSize != 1024 && transformSize != 2048) return SCE_ACM_ERROR_BAD_POINTER;
const bool realInput = (flags & 1) != 0;
if (transformSize == 2048 && !realInput) return SCE_ACM_ERROR_BAD_POINTER;
const std::uint32_t inputBytes = transformSize * (inputFormat == 1 ? 2 : 4);
const std::uint32_t outputBytes = transformSize * (outputFormat == 1 ? 2 : 4);
const std::uint32_t inputBlocks = (inputBytes >> 9) >> (realInput ? 1 : 0);
const std::uint32_t outputBlocks = outputBytes >> 9;
std::uint32_t transformCommand;
std::uint32_t inputCommand;
std::uint32_t outputCommand;
std::uint32_t setupCommand;
std::uint32_t sizeCode;
if (transformSize == 512) {
sizeCode = 4;
transformCommand = realInput ? 7 : 11;
inputCommand = realInput ? 2 * inputFormat : 9 + inputFormat;
outputCommand = 4 | outputFormat;
setupCommand = realInput ? 6 : 12;
} else if (transformSize == 1024) {
sizeCode = 5;
transformCommand = realInput ? 8 : 13;
inputCommand = realInput ? 2 * inputFormat : 14 | inputFormat;
outputCommand = 5 | (2 * outputFormat);
setupCommand = realInput ? 10 : 12;
} else {
sizeCode = 6;
transformCommand = 6;
inputCommand = 2 * inputFormat;
outputCommand = 4 | outputFormat;
setupCommand = 8;
}
auto* buffer = static_cast<std::byte*>(batch->buffer);
auto offset = batch->offset;
const auto capacity = batch->buffer_size;
const auto appendWord = [&](std::uint64_t value) {
if (offset > capacity || capacity - offset < sizeof(value)) return false;
std::memcpy(buffer + offset, &value, sizeof(value));
offset += sizeof(value);
return true;
};
std::uint64_t header = static_cast<std::uint64_t>(sizeCode) << 8;
if (!(flags & 8)) {
const auto encodedCount = transformCount < 64000 ? transformCount : 64000;
header |= (std::uint64_t{1} << 16) | (static_cast<std::uint64_t>(transformSize) << 8) | (static_cast<std::uint64_t>(flags & 6) << 19) | (static_cast<std::uint64_t>(flags & 1) << 23) | (std::uint64_t{1} << (inputFormat == 1 ? 24 : 26)) | (std::uint64_t{1} << (outputFormat == 1 ? 25 : 27)) | (static_cast<std::uint64_t>(encodedCount) << 28);
}
constexpr int noSpace = static_cast<int>(0x81940009);
constexpr int badAlignment = static_cast<int>(0x8194000A);
if (!appendWord(header) || !appendWord(10 | (setupCommand << 8))) return noSpace;
for (std::uint32_t index = 0; index < transformCount; ++index) {
const auto inputIndex = static_cast<std::size_t>(index) * ((flags & 2) ? 2 : 1);
const auto input = reinterpret_cast<std::uintptr_t>(inputs[inputIndex]);
const auto secondInput = (flags & 2) ? reinterpret_cast<std::uintptr_t>(inputs[inputIndex + 1]) : 0;
if (!(flags & 2) && !input) return SCE_ACM_ERROR_BAD_POINTER;
if ((input | secondInput) & 127) return badAlignment;
const auto inputRecordSize = (flags & 2) ? 24u : 16u;
if (offset > capacity || capacity - offset < inputRecordSize) return noSpace;
appendWord(((flags & 2) ? 7 : 6) | (static_cast<std::uint64_t>(inputBlocks) << 32) | ((flags & 2) ? static_cast<std::uint64_t>(inputBytes / 2) << 48 : 0));
appendWord(input);
if (flags & 2) appendWord(secondInput);
if (!appendWord(10 | (inputCommand << 8)) || !appendWord(10 | (transformCommand << 8)) || !appendWord(10 | (outputCommand << 8))) return noSpace;
const auto outputIndex = static_cast<std::size_t>(index) * ((flags & 4) ? 2 : 1);
const auto output = reinterpret_cast<std::uintptr_t>(outputs[outputIndex]);
const auto secondOutput = (flags & 4) ? reinterpret_cast<std::uintptr_t>(outputs[outputIndex + 1]) : 0;
if ((output | secondOutput) & 127) return badAlignment;
const auto outputRecordSize = (flags & 4) ? 24u : 16u;
if (offset > capacity || capacity - offset < outputRecordSize) return noSpace;
appendWord(((flags & 4) ? 9 : 8) | (static_cast<std::uint64_t>(outputBlocks) << 32) | ((flags & 4) ? static_cast<std::uint64_t>(outputBytes / 2) << 48 : 0));
appendWord(output);
if (flags & 4) appendWord(secondOutput);
}
batch->offset = offset;
return 0;
}
int APS5_VABI sceAcm_Panner(void) {
int APS5_VABI sceAcm_Panner(AcmBatchInfo* batch, std::uint32_t sourceCount, const void* const* sources, std::uint32_t sourceMode, std::uint32_t outputMode, std::uint32_t channelCount, const void* const* sourceParameters, const void* const* sourceMatrices, const void* const* inputChannels, void* const* outputChannels) {
if (!batch || sourceCount == 0 || sourceCount > 65535 || sourceMode > 2 || outputMode > 1 || channelCount == 0 || channelCount > 36 || !sources || !sourceParameters || !sourceMatrices || !outputChannels) return SCE_ACM_ERROR_BAD_POINTER;
std::uintptr_t alignment = reinterpret_cast<std::uintptr_t>(sources) | reinterpret_cast<std::uintptr_t>(sourceParameters) | reinterpret_cast<std::uintptr_t>(sourceMatrices) | reinterpret_cast<std::uintptr_t>(inputChannels) | reinterpret_cast<std::uintptr_t>(outputChannels);
for (std::uint32_t index = 0; index < sourceCount; ++index) {
if (!sources[index] || !sourceParameters[index] || !sourceMatrices[index]) return SCE_ACM_ERROR_BAD_POINTER;
std::uint32_t parameterMode;
std::memcpy(&parameterMode, sourceParameters[index], sizeof(parameterMode));
if (parameterMode > 1) return SCE_ACM_ERROR_BAD_POINTER;
alignment |= reinterpret_cast<std::uintptr_t>(sources[index]) | reinterpret_cast<std::uintptr_t>(sourceParameters[index]) | reinterpret_cast<std::uintptr_t>(sourceMatrices[index]);
}
for (std::uint32_t index = 0; index < channelCount; ++index) {
if (!outputChannels[index]) return SCE_ACM_ERROR_BAD_POINTER;
alignment |= reinterpret_cast<std::uintptr_t>(outputChannels[index]);
if (inputChannels) alignment |= reinterpret_cast<std::uintptr_t>(inputChannels[index]);
}
if (alignment & 127) return static_cast<int>(0x8194000A);
const auto offset = batch->offset;
const auto capacity = batch->buffer_size;
if (offset > capacity || capacity - offset < 8) return static_cast<int>(0x81940009);
auto* buffer = static_cast<std::byte*>(batch->buffer);
const auto writeWord = [buffer](std::size_t position, std::uint64_t value) { std::memcpy(buffer + position, &value, sizeof(value)); };
const auto encodedCount = sourceCount < 255 ? sourceCount : 255;
writeWord(offset, (static_cast<std::uint64_t>(channelCount) << 24) | (static_cast<std::uint64_t>(encodedCount) << 16) | (std::uint64_t{1} << (sourceMode + 32)) | (std::uint64_t{1} << (outputMode + 35)) | 0x300);
if (capacity < 128 || capacity - offset < 152) return static_cast<int>(0x81940009);
const auto descriptorOffset = capacity - 128;
std::memset(buffer + descriptorOffset, 0, 128);
writeWord(descriptorOffset, sourceCount);
writeWord(descriptorOffset + 8, reinterpret_cast<std::uintptr_t>(inputChannels));
writeWord(descriptorOffset + 16, reinterpret_cast<std::uintptr_t>(outputChannels));
writeWord(descriptorOffset + 24, reinterpret_cast<std::uintptr_t>(sources));
writeWord(descriptorOffset + 32, sourceMode | (static_cast<std::uint64_t>(outputMode) << 32));
writeWord(descriptorOffset + 40, channelCount);
writeWord(descriptorOffset + 48, reinterpret_cast<std::uintptr_t>(sourceParameters));
writeWord(descriptorOffset + 56, reinterpret_cast<std::uintptr_t>(sourceMatrices));
writeWord(offset + 8, 0x8000000000000D);
writeWord(offset + 16, reinterpret_cast<std::uintptr_t>(buffer + descriptorOffset));
if (capacity - offset < 160) return static_cast<int>(0x81940009);
writeWord(offset + 24, 10);
batch->offset = offset + 32;
batch->buffer_size = descriptorOffset;
return 0;
}
int APS5_VABI sceAcmBatchJobPriority(void) {
int APS5_VABI sceAcmBatchJobPriority(AcmBatchInfo* batch, std::uint32_t priority) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAcm_ConvReverb_SharedIr() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAcm_IFFT() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAcmBatchJobNotification() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
+8 -7
View File
@@ -72,13 +72,14 @@ std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf,
}
std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t videoOutHandle, std::int32_t displayBufferIndex, std::uint32_t flipMode, std::int64_t flipArg) {
(void)buf;
(void)videoOutHandle;
(void)displayBufferIndex;
(void)flipMode;
(void)flipArg;
NotImplemented_nid_no_patch(__func__);
return nullptr;
auto* packet = Agc::Command::Allocate(buf, AgcDriver::FlipPacketWords, __func__);
packet[0] = AgcDriver::FlipPacketHeader;
packet[1] = videoOutHandle;
packet[2] = static_cast<std::uint32_t>(displayBufferIndex);
packet[3] = flipMode;
packet[4] = static_cast<std::uint32_t>(static_cast<std::uint64_t>(flipArg));
packet[5] = static_cast<std::uint32_t>(static_cast<std::uint64_t>(flipArg) >> 32u);
return packet;
}
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) {
+1 -1
View File
@@ -43,7 +43,7 @@ std::uint32_t APS5_VABI sceAgcAcbAtomicMemGetSize() {
}
std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize() {
return 36;
return 44;
}
std::uint32_t* APS5_VABI sceAgcAcbPrimeUtcl2(CommandBuffer* buf, const volatile void* address, std::uint32_t sizeInBytes) {
+41
View File
@@ -35,6 +35,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/StaticBuffer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/Dma.cpp
@@ -81,8 +82,34 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_atomic_gds_size_tests EXCLUDE_FROM_ALL tests/AtomicGdsSize.cpp)
target_include_directories(agc_atomic_gds_size_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_atomic_gds_size_tests PRIVATE libSceAgc libc)
configure_windows_unwind(agc_atomic_gds_size_tests)
if(BUILD_TESTING)
set_target_properties(agc_atomic_gds_size_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_atomic_gds_size COMMAND agc_atomic_gds_size_tests)
if(WIN32)
set_tests_properties(agc_atomic_gds_size PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
configure_windows_unwind(libSceAgc)
add_test_executable(agc_static_buffer_tests EXCLUDE_FROM_ALL tests/StaticBuffer.cpp)
target_include_directories(agc_static_buffer_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_static_buffer_tests PRIVATE libSceAgc libSceAgcDriver libc)
configure_windows_unwind(agc_static_buffer_tests)
if(BUILD_TESTING)
set_target_properties(agc_static_buffer_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_static_buffer COMMAND agc_static_buffer_tests)
if(WIN32)
set_tests_properties(agc_static_buffer PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_platform_tests EXCLUDE_FROM_ALL tests/Platform.cpp)
target_include_directories(agc_platform_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_platform_tests PRIVATE libSceAgc libc)
@@ -240,6 +267,19 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_instrumentation_query_size_tests EXCLUDE_FROM_ALL tests/InstrumentationQuerySize.cpp)
target_include_directories(agc_instrumentation_query_size_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_instrumentation_query_size_tests PRIVATE libSceAgc libc)
configure_windows_unwind(agc_instrumentation_query_size_tests)
if(BUILD_TESTING)
set_target_properties(agc_instrumentation_query_size_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_instrumentation_query_size_tests COMMAND agc_instrumentation_query_size_tests)
if(WIN32)
set_tests_properties(agc_instrumentation_query_size_tests PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_context_state_size_tests EXCLUDE_FROM_ALL tests/ContextStateSize.cpp)
target_include_directories(agc_context_state_size_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_context_state_size_tests PRIVATE libSceAgc libc)
@@ -415,6 +455,7 @@ configure_windows_unwind(agc_shader_helpers_tests)
if(BUILD_TESTING)
set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests)
set_tests_properties(agc_shader_helpers PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
@@ -1,4 +1,13 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_CB_INCLUDE_SYNC_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_CB_INCLUDE_SYNC_HPP
#include "SceTypes.hpp"
#include <cstdint>
struct CommandBuffer;
extern "C" {
std::uint32_t* APS5_VABI sceAgcCbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint32_t operation, std::uint32_t modifier, std::uint32_t value);
}
#endif
+9
View File
@@ -58,4 +58,13 @@ std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer* buf, std::uint8_t act
return packet;
}
std::uint32_t* APS5_VABI sceAgcCbMemSemaphore(CommandBuffer* buf, const volatile void* address, std::uint32_t operation, std::uint32_t modifier, std::uint32_t value) {
(void)operation;
(void)modifier;
(void)value;
const auto guestAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(address));
const auto maskedAddress = guestAddress & ~0xfULL;
return Agc::Command::Emit(buf, 0x39u, {static_cast<std::uint32_t>(maskedAddress), static_cast<std::uint32_t>(maskedAddress >> 32u), 0x80100000u}, __func__);
}
}
@@ -5,7 +5,14 @@
namespace Agc::Command {
void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function);
enum class RegisterDefaultsTypeLayout {
Compact,
KeyBankPairs,
};
void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function,
RegisterDefaultsTypeLayout typeLayout = RegisterDefaultsTypeLayout::Compact);
int GetDefaultCxStateFlat(void* destination, std::uint32_t size);
}
@@ -1,7 +1,9 @@
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
@@ -96,8 +98,8 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s
Require(count != 0 && count <= 0x3fffu && offset <= 0xffffu && count <= 0x10000u - offset, function, "invalid register range");
std::vector<std::uint32_t> snapshot;
if (values != nullptr) {
CheckAddress(reinterpret_cast<std::uintptr_t>(values), 4, function);
snapshot.assign(values, values + count);
snapshot.resize(count);
std::memcpy(snapshot.data(), values, static_cast<std::size_t>(count) * sizeof(std::uint32_t));
}
auto* packet = Allocate(buffer, count + 2u, function);
packet[0] = Header(opcode, count + 2u);
@@ -107,7 +109,10 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s
}
std::uint32_t* WriteRegisters(CommandBuffer* buffer, std::uint32_t opcode, const volatile ShaderRegister* registers, std::uint32_t count, bool snapshotAll, const char* function) {
Require(count != 0, function, "empty register list");
if (count == 0) {
Require(buffer != nullptr, function, "null command buffer");
return buffer->cursor_up;
}
CheckAddress(reinterpret_cast<std::uintptr_t>(registers), 4, function);
std::vector<ShaderRegister> snapshot;
if (snapshotAll) {
@@ -4,6 +4,7 @@
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstring>
#include <mutex>
#include <vector>
@@ -11,13 +12,21 @@ namespace Agc::Command {
namespace {
struct TypeIndex {
std::uint32_t key;
std::uint32_t bankIndex;
};
static_assert(sizeof(TypeIndex) == 8);
struct RegisterDefaults {
ShaderRegister** tables[4];
std::uint32_t registerCounts[4];
std::uint32_t* types;
void* types;
std::uint32_t count;
};
static_assert(offsetof(RegisterDefaults, types) == 0x30);
static_assert(offsetof(RegisterDefaults, count) == 0x38);
struct CompactBank {
@@ -38,19 +47,49 @@ struct CompactRegisterDefaults {
struct Storage {
RegisterDefaults defaults{};
std::array<std::vector<ShaderRegister*>, 4> pointers;
std::vector<TypeIndex> types;
bool initialized = false;
};
std::mutex defaultsMutex;
std::array<std::array<Storage, 14>, 2> storage;
std::array<std::array<std::array<Storage, 14>, 2>, 2> storage;
struct AgcContextState {
std::uint32_t data[512];
};
static_assert(sizeof(AgcContextState) == 2048);
std::mutex defaultStateMutex;
AgcContextState defaultState{};
bool defaultStateInitialized = false;
void InitDefaultCxState() {
std::memset(&defaultState, 0, sizeof(defaultState));
const auto* source = agcPublicRegDefaultsByVersion[8];
for (std::size_t bank = 0; bank < 4; ++bank) {
const auto& b = source->banks[bank];
if (b.registers == nullptr) continue;
for (std::uint32_t r = 0; r < b.registerCount; ++r) {
const auto offset = b.registers[r].offset;
const auto value = b.registers[r].value;
if (offset < 512) {
defaultState.data[offset] = value;
}
}
}
defaultStateInitialized = true;
}
}
void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function) {
void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function,
RegisterDefaultsTypeLayout typeLayout) {
Require(version < 14, function, "unsupported register defaults version");
const auto* source = internal ? agcInternalRegDefaultsByVersion[version] : agcPublicRegDefaultsByVersion[version];
std::lock_guard lock(defaultsMutex);
auto& destination = storage[internal ? 1 : 0][version];
const auto layoutIndex = typeLayout == RegisterDefaultsTypeLayout::KeyBankPairs ? 1 : 0;
auto& destination = storage[internal ? 1 : 0][layoutIndex][version];
if (!destination.initialized) {
std::array<std::vector<ShaderRegister*>, 4> pointers;
RegisterDefaults defaults{};
@@ -67,13 +106,38 @@ void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* func
defaults.registerCounts[bank] = input.registerCount;
}
Require(source->count == 0 || source->types != nullptr, function, "missing register defaults types");
defaults.types = source->types;
std::vector<TypeIndex> types;
if (typeLayout == RegisterDefaultsTypeLayout::KeyBankPairs) {
types.reserve(source->count);
for (std::uint32_t entry = 0; entry < source->count; ++entry) {
const std::uint32_t bankIndex = source->types[entry * 3 + 1] & 0x3ffu;
Require((bankIndex >> 2) < source->banks[bankIndex & 3u].pointerCount, function, "register defaults type exceeds its bank");
types.push_back({source->types[entry * 3], bankIndex});
}
}
if (typeLayout == RegisterDefaultsTypeLayout::KeyBankPairs) {
defaults.types = types.empty() ? nullptr : types.data();
} else {
defaults.types = source->types;
}
defaults.count = source->count;
destination.pointers = std::move(pointers);
destination.types = std::move(types);
destination.defaults = defaults;
destination.initialized = true;
}
return &destination.defaults;
}
int GetDefaultCxStateFlat(void* destination, std::uint32_t size) {
if (destination == nullptr) return static_cast<int>(0x80890001);
std::lock_guard lock(defaultStateMutex);
if (!defaultStateInitialized) {
InitDefaultCxState();
}
const auto copyBytes = std::min(size, static_cast<std::uint32_t>(sizeof(AgcContextState)));
std::memcpy(destination, defaultState.data, copyBytes);
return 0;
}
}
@@ -42,16 +42,14 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize_0600(CommandBuffer* buf, std::uin
return packet;
}
std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) {
(void)buf;
(void)dataOffsetInBytes;
NotImplemented_nid_no_patch(__func__);
return nullptr;
std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex) {
if (attributeAddress != 0) Agc::Command::CheckAddress(attributeAddress, 16, __func__);
Agc::Command::CheckBits(attributeIndex, 0xffffu, __func__);
return Agc::Command::Emit(buf, 0x91u, {static_cast<std::uint32_t>(attributeAddress), static_cast<std::uint32_t>(attributeAddress >> 32u), attributeIndex}, __func__);
}
std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize() {
NotImplemented_nid_no_patch(__func__);
return 0;
return 16;
}
}
@@ -21,14 +21,13 @@ std::uint32_t* APS5_VABI sceAgcDcbGetLodStats(CommandBuffer* buf, std::uint8_t c
return Agc::Command::Emit(buf, 0x10u, {bufferSizeInBytes, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), control}, __func__);
}
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize() {
NotImplemented_nid_no_patch(__func__);
return 0;
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t queryType) {
if (queryType == 1) return 0x120u;
return queryType == 0 ? 0x10u : 0u;
}
std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize() {
NotImplemented_nid_no_patch(__func__);
return 0;
return 0x10u;
}
}
@@ -1,4 +1,12 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_DCBFLOW_INCLUDE_CONTROL_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_DCBFLOW_INCLUDE_CONTROL_HPP
#include "SceTypes.hpp"
#include <cstdint>
extern "C" {
std::uint32_t* APS5_VABI sceAgcDcbAtomicGds(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData);
std::uint32_t* APS5_VABI sceAgcDcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData);
}
#endif
@@ -2,7 +2,11 @@
#define CORE_LIBS_PRX_LIBSCEAGC_DCBFLOW_INCLUDE_MEMORY_HPP
#include "SceTypes.hpp"
#include <cstdint>
extern "C" std::uint32_t* APS5_VABI sceAgcDcbAcquireMem(CommandBuffer* buf, std::uint8_t engine, std::uint32_t cbDbOp, std::uint32_t gcrControl, const volatile void* base, std::uint64_t sizeBytes, std::uint32_t pollCycles);
extern "C" {
std::uint32_t* APS5_VABI sceAgcDcbAcquireMem(CommandBuffer* buf, std::uint8_t engine, std::uint32_t cbDbOp, std::uint32_t gcrControl, const volatile void* base, std::uint64_t sizeBytes, std::uint32_t pollCycles);
std::uint32_t APS5_VABI sceAgcDcbAtomicGdsGetSize();
}
#endif
@@ -12,10 +12,19 @@
extern "C" {
// unknown signature
void* APS5_VABI sceAgcDcbAtomicGds_0900(void) {
NotImplemented_nid_no_patch(__func__);
return nullptr;
std::uint32_t* APS5_VABI sceAgcDcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) {
const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) |
((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) |
((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u);
return Agc::Command::Emit(buf, 0x1du, {
operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index,
mask & 0x00ff00ffu, data, static_cast<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__);
}
std::uint32_t* APS5_VABI sceAgcDcbAtomicGds(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) {
return sceAgcDcbAtomicGds_0900(buf, control, mode, gdsOp, src, offset, index, mask, data, compareData, extraData);
}
@@ -53,7 +53,7 @@ std::uint32_t APS5_VABI sceAgcDcbAtomicMemGetSize() {
}
std::uint32_t APS5_VABI sceAgcDcbAtomicGdsGetSize() {
return 36;
return 44;
}
std::uint32_t* APS5_VABI sceAgcDcbWriteData(CommandBuffer* buf, std::uint8_t dst, std::uint8_t cachePolicy, std::uint64_t address, const void* data, std::uint32_t numDwords, std::uint8_t increment, std::uint8_t writeConfirm) {
@@ -1,4 +1,17 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_CONTEXTSTATE_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_DCBSTATE_INCLUDE_CONTEXTSTATE_HPP
#include "SceTypes.hpp"
#include <cstdint>
struct CommandBuffer;
extern "C" {
uint32_t* APS5_VABI sceAgcDcbContextStateOp(CommandBuffer* buf, uint32_t operation);
uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, uint32_t operation);
uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(uint32_t operation);
}
#endif
@@ -9,10 +9,7 @@
extern "C" {
uint32_t* APS5_VABI sceAgcDcbContextStateOp(CommandBuffer* buf, uint32_t operation) {
(void)buf;
(void)operation;
NotImplemented_nid_no_patch(__func__);
return nullptr;
return sceAgcDcbContextStateOp_0100(buf, operation);
}
uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(uint32_t operation) {
@@ -7,21 +7,18 @@
#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) {
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(state), alignof(std::uint32_t), __func__);
Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version");
return 0;
}
int APS5_VABI sceAgcInit(std::uint32_t version) {
if (version >= 14) return SCE_AGC_ERROR_INVALID_ARGUMENT;
if (version >= 14) return 0;
Agc::Command::GetRegisterDefaults(version, false, __func__);
Agc::Command::GetRegisterDefaults(version, true, __func__);
return 0;
}
int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) {
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(state), alignof(std::uint32_t), __func__);
return sceAgcInit(version);
}
}
+13
View File
@@ -0,0 +1,13 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
int APS5_VABI sceAgcSuspendPointAndCheckStatus(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
@@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_PACKETINFO_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_PACKETINFO_HPP
#include "SceTypes.hpp"
#include <cstdint>
struct SceAgcMemoryRange {
@@ -8,4 +9,9 @@ struct SceAgcMemoryRange {
uint64_t size;
};
extern "C" {
int APS5_VABI sceAgcGetDataPacketPayloadAddress(uint32_t** addr, uint32_t* cmd, int type);
int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(uint32_t** addr, uint32_t* cmd, int type);
}
#endif
@@ -1,4 +1,9 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_REGISTERDEFAULTS_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_REGISTERDEFAULTS_HPP
#include "SceTypes.hpp"
#include <cstdint>
extern "C" {
int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size);
}
#endif
@@ -0,0 +1,14 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_STATICBUFFER_HPP
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_STATICBUFFER_HPP
#include <cstdint>
#include "SceTypes.hpp"
extern "C" {
int APS5_VABI sceAgcSetStaticBuffer(std::uint32_t index, const void* buffer);
int APS5_VABI sceAgcGetStaticBuffer(std::uint32_t index, void* buffer);
int APS5_VABI sceAgcSetStaticQword(std::uint32_t index, std::uint64_t value);
int APS5_VABI sceAgcGetStaticQword(std::uint32_t index, std::uint64_t* value);
}
#endif
@@ -1,9 +1,16 @@
#include "prx/libSceAgc/Misc/include/Debug.hpp"
#include <atomic>
#include <cstdint>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
namespace {
std::atomic<std::uint32_t> shaderInstrumentation{0};
}
extern "C" {
int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) {
@@ -11,4 +18,13 @@ int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId) {
return 0;
}
int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t flags) {
shaderInstrumentation.store(flags, std::memory_order_relaxed);
return 0;
}
std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation() {
return shaderInstrumentation.load(std::memory_order_relaxed);
}
}
@@ -28,6 +28,10 @@ int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(uint32_t** addr, uint32_t*
return 0;
}
int APS5_VABI sceAgcGetDataPacketPayloadAddress(uint32_t** addr, uint32_t* cmd, int type) {
return sceAgcGetDataPacketPayloadAddress_0090(addr, cmd, type);
}
int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, uint32_t* cmd, int type) {
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(range), alignof(SceAgcMemoryRange), __func__);
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(cmd), alignof(uint32_t), __func__);
@@ -10,8 +10,7 @@
extern "C" {
void* APS5_VABI sceAgcGetRegisterDefaults() {
// The legacy API has no version argument; use the baseline public table.
return Agc::Command::GetRegisterDefaults(0, false, __func__);
return Agc::Command::GetRegisterDefaults(13, false, __func__, Agc::Command::RegisterDefaultsTypeLayout::KeyBankPairs);
}
void* APS5_VABI sceAgcGetRegisterDefaultsInternal() {
@@ -26,4 +25,8 @@ void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version) {
return Agc::Command::GetRegisterDefaults(version, true, __func__);
}
int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size) {
return Agc::Command::GetDefaultCxStateFlat(out_state, size);
}
}
+7 -18
View File
@@ -13,20 +13,12 @@ namespace {
constexpr std::uint64_t semaphoreMemoryAlignment = 0x4000u;
constexpr std::uint64_t semaphoreLabelBytes = 32u;
constexpr std::uint64_t semaphoreCounterBytes = 8u;
std::mutex semaphoreMutex;
std::uint8_t* semaphoreMemory = nullptr;
std::uint64_t semaphoreBytes = 0;
bool hasOnlyZeroLabelTails(const std::uint8_t* memory, std::uint64_t sizeInBytes) {
for (std::uint64_t label = 0; label < sizeInBytes; label += semaphoreLabelBytes) {
for (std::uint64_t byte = semaphoreCounterBytes; byte < semaphoreLabelBytes; ++byte) {
if (memory[label + byte] != 0) return false;
}
}
return true;
}
}
@@ -35,15 +27,12 @@ extern "C" {
int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInBytes) {
const auto address = reinterpret_cast<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");
if (semaphoreBytes != 0) return static_cast<int>(0x8a6c0048u);
if (((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) != 0) return static_cast<int>(0x8a6c0002u);
Agc::Command::Require(memory != nullptr && sizeInBytes != 0, __func__, "null or empty AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(((address | sizeInBytes) & (semaphoreMemoryAlignment - 1u)) == 0, __func__, "AMM semaphore memory or size is not 16 KiB aligned; the console's error is unknown");
Agc::Command::Require(sizeInBytes - 1u <= std::numeric_limits<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);
}
std::memset(labels, 0, sizeInBytes);
semaphoreMemory = labels;
semaphoreBytes = sizeInBytes;
return 0;
@@ -51,9 +40,9 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory(void* memory, std::uint64_t sizeInByte
int APS5_VABI sceAgcGetSemaphoreLabel(std::uint32_t index, void** label) {
std::lock_guard lock(semaphoreMutex);
Agc::Command::Require(semaphoreMemory != nullptr, __func__, "no AMM semaphore memory is registered; the console's error is unknown");
Agc::Command::Require(index < semaphoreBytes / semaphoreLabelBytes, __func__, "the semaphore label lies outside the AMM semaphore memory; the console's error is unknown");
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output; the console's error is unknown");
if (semaphoreBytes == 0) return static_cast<int>(0x8a6c0049u);
if (index >= semaphoreBytes / semaphoreLabelBytes) return static_cast<int>(0x8a6c000bu);
Agc::Command::Require(label != nullptr, __func__, "null semaphore label output");
*label = semaphoreMemory + static_cast<std::uint64_t>(index) * semaphoreLabelBytes;
return 0;
}
@@ -35,7 +35,7 @@ constexpr std::size_t FusedCodeAlignment = 256;
constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'};
constexpr std::size_t TrailerCodeBytesOffset = sizeof(TrailerMagic) + 3 * sizeof(std::uint32_t);
constexpr std::uint32_t Rsrc1VgprMask = 0x3fu;
constexpr std::uint32_t Rsrc1SgprMask = 0xfu << 6u;
constexpr std::uint32_t Rsrc2OffchipLdsEnable = 1u << 18u;
constexpr std::uint32_t Rsrc2UserSgprMask = (0x1fu << 1u) | (1u << 27u);
bool ValidHalves(const Shader* front, const Shader* back) {
@@ -120,15 +120,19 @@ std::uint32_t FrontProgramBytes(const char* function, const Shader* front) {
return static_cast<std::uint32_t>((end - 1) * 4);
}
std::uint32_t MergeRsrc1(std::uint32_t back, std::uint32_t front) {
const auto vgprs = std::max(back & Rsrc1VgprMask, front & Rsrc1VgprMask);
const auto sgprs = std::max(back & Rsrc1SgprMask, front & Rsrc1SgprMask);
return (back & ~(Rsrc1VgprMask | Rsrc1SgprMask)) | vgprs | sgprs;
}
std::uint32_t MergeRsrc2(const char* function, std::uint32_t back, std::uint32_t front) {
if ((front & ~Rsrc2UserSgprMask & ~back) != 0) Fail(function, "front half requires hardware state the back half does not enable");
return (back & ~Rsrc2UserSgprMask) | (front & Rsrc2UserSgprMask);
std::array<std::uint32_t, 2> mergeGeometryResources(std::uint32_t frontRsrc1, std::uint32_t frontRsrc2, std::uint32_t backRsrc1, std::uint32_t backRsrc2) {
const auto frontVgprs = ((frontRsrc1 & Rsrc1VgprMask) + 1u) * 4u;
const auto backVgprs = ((backRsrc1 & Rsrc1VgprMask) + 1u) * 4u;
const auto frontTotal = frontVgprs + (frontRsrc2 >> 28u) * 8u;
const auto backTotal = backVgprs + (backRsrc2 >> 28u) * 8u;
const auto maxTotal = std::max(frontTotal, backTotal);
const auto shared = std::max(frontVgprs, backVgprs) >= maxTotal ? 0u : (maxTotal - std::min(frontTotal, backTotal) + 7u) / 64u;
const auto vgprs = std::max(frontRsrc1 & Rsrc1VgprMask, backRsrc1 & Rsrc1VgprMask);
const auto rsrc1Field = std::max((frontRsrc1 >> 29u) & 3u, (backRsrc1 >> 29u) & 3u);
const auto rsrc2Field = std::max((frontRsrc2 >> 16u) & 3u, (backRsrc2 >> 16u) & 3u);
const auto rsrc1 = (backRsrc1 & ~(Rsrc1VgprMask | (3u << 29u))) | vgprs | (rsrc1Field << 29u);
const auto rsrc2 = (backRsrc2 & ~(0xf0000000u | (3u << 16u) | Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable)) | ((shared & 15u) << 28u) | (rsrc2Field << 16u) | (frontRsrc2 & (Rsrc2UserSgprMask | Rsrc2OffchipLdsEnable));
return {rsrc1, rsrc2};
}
const ShaderRegister* FindHalfRegister(const Shader* shader, std::uint32_t offset) {
@@ -148,10 +152,14 @@ GeometryLayout ComputeGeometryLayout(const char* function, const Shader* front,
const auto* frontRsrc1 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC1_GS);
const auto* frontRsrc2 = FindHalfRegister(front, SPI_SHADER_PGM_RSRC2_GS);
if (frontRsrc1 == nullptr || frontRsrc2 == nullptr) Fail(function, "front half has no GS resource registers");
const auto* backRsrc1 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC1_GS);
const auto* backRsrc2 = FindHalfRegister(back, SPI_SHADER_PGM_RSRC2_GS);
if (backRsrc1 == nullptr || backRsrc2 == nullptr) Fail(function, "back half has no GS resource registers");
const auto resources = mergeGeometryResources(frontRsrc1->value, frontRsrc2->value, backRsrc1->value, backRsrc2->value);
layout.sh.assign(back->sh_registers, back->sh_registers + back->num_sh_registers);
for (auto& reg : layout.sh) {
if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = MergeRsrc1(reg.value, frontRsrc1->value);
else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = MergeRsrc2(function, reg.value, frontRsrc2->value);
if (reg.offset == SPI_SHADER_PGM_RSRC1_GS) reg.value = resources[0];
else if (reg.offset == SPI_SHADER_PGM_RSRC2_GS) reg.value = resources[1];
}
for (std::uint32_t i = 0; i < front->num_sh_registers; ++i) {
const auto& reg = front->sh_registers[i];
@@ -0,0 +1,45 @@
#include "prx/libSceAgc/Misc/include/StaticBuffer.hpp"
#include <cstdint>
#include <cstring>
#include "SceTypes.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
namespace {
constexpr int GRAPHICS5_ERROR_INVALID_STATIC_BUFFER = static_cast<int>(0x8a6c000bu);
constexpr std::uint32_t StaticBufferCount = 6;
constexpr std::size_t StaticBufferBytes = 16;
constexpr std::size_t StaticQwordOffset = 16;
std::uint8_t* StaticTable() {
return static_cast<std::uint8_t*>(AgcDriverStaticTable_nid_postfix());
}
}
extern "C" {
int APS5_VABI sceAgcSetStaticBuffer(std::uint32_t index, const void* buffer) {
if (index >= StaticBufferCount) return GRAPHICS5_ERROR_INVALID_STATIC_BUFFER;
std::memcpy(StaticTable() + std::size_t{index} * StaticBufferBytes, buffer, StaticBufferBytes);
return 0;
}
int APS5_VABI sceAgcGetStaticBuffer(std::uint32_t index, void* buffer) {
if (index >= StaticBufferCount) return GRAPHICS5_ERROR_INVALID_STATIC_BUFFER;
std::memcpy(buffer, StaticTable() + std::size_t{index} * StaticBufferBytes, StaticBufferBytes);
return 0;
}
int APS5_VABI sceAgcSetStaticQword(std::uint32_t index, std::uint64_t value) {
std::memcpy(StaticTable() + StaticQwordOffset + std::size_t{index} * sizeof(value), &value, sizeof(value));
return 0;
}
int APS5_VABI sceAgcGetStaticQword(std::uint32_t index, std::uint64_t* value) {
std::memcpy(value, StaticTable() + StaticQwordOffset + std::size_t{index} * sizeof(*value), sizeof(*value));
return 0;
}
}
@@ -45,34 +45,29 @@ extern "C" {
int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
const auto guestAddress = reinterpret_cast<std::uintptr_t>(address);
const auto alignment = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 4u : 8u;
Agc::Command::CheckAddress(guestAddress, alignment, __func__);
Agc::Command::CheckBits(guestAddress, 0xffffffffffffull, __func__);
cmd[2] = (cmd[2] & 0xffff0000u) | static_cast<std::uint32_t>(guestAddress >> 32u);
const auto lowMask = ((wait[0] >> 8u) & 0xffu) == 0x3cu ? 3u : 7u;
cmd[2] = (cmd[2] & 0xffff0000u) | (static_cast<std::uint32_t>(guestAddress >> 32u) & 0xffffu);
cmd[3] = static_cast<std::uint32_t>(guestAddress);
wait[2] = static_cast<std::uint32_t>(guestAddress);
wait[3] = static_cast<std::uint32_t>(guestAddress >> 32u);
wait[2] = (static_cast<std::uint32_t>(guestAddress) & ~lowMask) | (wait[2] & lowMask);
wait[3] = (static_cast<std::uint32_t>(guestAddress >> 32u) & 0x3ffffu) | (wait[3] & 0xfffc0000u);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference) {
int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::CheckBits(reference, 0xffffffffu, __func__);
wait[4] = static_cast<std::uint32_t>(reference);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask) {
int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::CheckBits(mask, 0xffffffffu, __func__);
wait[((wait[0] >> 8u) & 0xffu) == 0x3cu ? 5 : 6] = static_cast<std::uint32_t>(mask);
return 0;
}
int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction) {
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
Agc::Command::Require(compareFunction <= 6, __func__, "invalid wait comparison");
wait[1] = (wait[1] & ~7u) | compareFunction;
wait[1] = (wait[1] & ~7u) | (compareFunction & 7u);
return 0;
}
@@ -53,12 +53,9 @@ std::uint32_t CreateInterpolantValueSplitF16(std::uint32_t psWord, const ShaderS
: CreateInterpolantValue(psWord, gsSemantic);
}
std::uint32_t UpdateInterpolantValue(std::uint32_t psWord, const ShaderSemantic* gsSemantic) {
const std::uint32_t f16 = (psWord >> 20u) & 0x3u;
if (f16 != 0 && f16 != InterpolantF16HiOnly && ((psWord >> 28u) & 0x3u) != ((psWord >> 30u) & 0x3u)) {
throw std::runtime_error(std::string(__func__) + ": DEFAULT_VAL of an f16 input with different low and high defaults is unknown");
}
return CreateInterpolantValueSplitF16(psWord, gsSemantic);
std::uint32_t CreateInterpolantValueFlatDefault(std::uint32_t psWord, const ShaderSemantic* gsSemantic) {
const std::uint32_t value = CreateInterpolantValue(psWord, gsSemantic);
return ((psWord & 0x00700000u) == 0x00400000u) ? (value | 0x00000020u) : value;
}
int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader* gs, const Shader* ps, CreateInterpolantValueFn createValue, bool fillIdentityTail) {
@@ -72,7 +69,7 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32");
}
AgcDriver::ShaderPreparationScope transaction;
AgcDriver::ShaderPreparationScope transaction({ps != nullptr ? gs : nullptr, ps});
std::array<ShaderRegister, 32> values{};
auto* output = regs;
regs = values.data();
@@ -127,14 +124,20 @@ int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Sh
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true);
}
APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping);
int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true);
}
APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw);
int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false);
int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, false);
}
int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, true);
}
int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueFlatDefault, false);
}
}
@@ -22,7 +22,7 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist
// The simple vertex/pixel pipeline combines two primitive context registers
// and 32 interpolant registers. Publish only after both helpers succeed.
AgcDriver::ShaderPreparationScope transaction;
AgcDriver::ShaderPreparationScope transaction({vertex, pixel});
std::array<ShaderRegister, 34> contextValues{};
std::array<ShaderRegister, 3> primitiveValues{};
sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType);
@@ -8,6 +8,7 @@
#include "SceShaders.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp"
#include "prx/libSceAgc/Shader/include/ShaderUtils.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
@@ -56,7 +57,9 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_
const std::array<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)) {
AgcDriver::ShaderPreparationScope transaction({hs, gs});
AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues);
transaction.Commit();
}
if (cx_regs != nullptr) std::copy(contextValues.begin(), contextValues.end(), cx_regs);
if (uc_regs != nullptr) std::copy(primitiveValues.begin(), primitiveValues.end(), uc_regs);
@@ -1,4 +1,5 @@
#include <stdexcept>
#include <string>
#include "prx/libSceAgc/Shader/include/ShaderUtils.hpp"
using namespace ShaderRegs;
+7 -22
View File
@@ -5,52 +5,37 @@
extern "C" {
int APS5_VABI sceAgcAcbPushMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcAcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcAcbSetMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcAcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcAcquireMemSetEngine() {
int APS5_VABI sceAgcAcquireMemSetEngine(std::uint32_t* packet, std::uint8_t engine) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDcbPushMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcDcbPushMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDcbSetMarkerSpan() {
std::uint32_t* APS5_VABI sceAgcDcbSetMarkerSpan(CommandBuffer* buffer, const char* text, std::uint32_t length, std::uint32_t color) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcGetDefaultCxStateFlat() {
int APS5_VABI sceAgcSetSemaphoreMemory(void* memory, std::uint64_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetSemaphoreMemory() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcCbMemsetExclusive() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetShaderInstrumentation() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcGetShaderInstrumentation() {
std::uint32_t* APS5_VABI sceAgcCbMemsetExclusive(CommandBuffer* buffer, void* destination, const void* pattern, std::uint64_t size) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@@ -0,0 +1,25 @@
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include <array>
#include <cstdio>
#include <cstdint>
extern "C" std::uint32_t APS5_VABI sceAgcAcbAtomicGdsGetSize();
extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode,
std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask,
std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData);
int main() {
constexpr std::uint32_t expectedSize = 11u * sizeof(std::uint32_t);
if (sceAgcAcbAtomicGdsGetSize() != expectedSize) {
std::fprintf(stderr, "ACB atomic GDS size does not match its 11-dword packet\n");
return 1;
}
std::array<std::uint32_t, 64> words{};
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
const auto* packet = sceAgcAcbAtomicGds_0900(&buffer, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
if (packet != words.data() || buffer.cursor_up != words.data() + expectedSize / sizeof(std::uint32_t)) {
std::fprintf(stderr, "ACB atomic GDS builder wrote a different size than its size accessor\n");
return 1;
}
}
+132 -15
View File
@@ -16,17 +16,22 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std:
extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t handle, std::uint32_t index);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
extern "C" int APS5_VABI sceAgcSuspendPoint();
extern "C" int APS5_VABI sceAgcInit(std::uint32_t version);
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults();
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2(std::uint32_t version);
extern "C" void* APS5_VABI sceAgcGetRegisterDefaultsInternal();
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version);
extern "C" int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size);
extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirectMulti(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint32_t countIndirect, std::uint32_t maxCountOrCount, const volatile void* countAddress, std::uint32_t strideInBytes, std::uint64_t modifier);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint32_t reference);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint32_t mask);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation);
@@ -45,6 +50,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, cons
extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64_t indexAddress);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex);
extern "C" std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize();
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr);
extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw);
extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId);
@@ -52,8 +59,6 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf,
namespace {
constexpr int SCE_AGC_ERROR_INVALID_ARGUMENT = static_cast<int>(0x80890001);
void check(bool condition, const char* message) {
if (!condition) {
throw std::runtime_error(message);
@@ -324,6 +329,20 @@ void testIndexBuffer() {
throw std::runtime_error("misaligned index buffer was accepted");
}
void testIndexIndirectArgs() {
Storage storage;
const auto* packet = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x4444u);
const std::array<std::uint32_t, 4> expected{0xc0029100u, 0x22222220u, 0x11111111u, 0x4444u};
check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), packet), "index indirect args packet differs from the console's");
check(sceAgcDcbSetIndexIndirectArgsGetSize() == expected.size() * sizeof(std::uint32_t), "index indirect args size differs from its packet");
const auto* cleared = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0, 0);
check(cleared == packet + expected.size() && cleared[0] == expected[0] && cleared[1] == 0 && cleared[2] == 0 && cleared[3] == 0, "null index attribute base was not written as 0");
const auto before = storage.words;
expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222228u, 0); });
expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x10000u); });
check(storage.words == before && storage.buffer.cursor_up == cleared + expected.size(), "rejected index indirect args modified the buffer");
}
void testIndirectBase() {
Storage storage;
alignas(8) std::uint32_t arguments[4]{};
@@ -426,6 +445,16 @@ void testFlip() {
full.buffer.cursor_down = full.words.data() + 3;
expectFailure([&] { sceAgcAcbWaitUntilSafeForRendering(&full.buffer, 1, 0); });
check(full.buffer.cursor_up == full.words.data(), "failed compute rendering wait advanced cursor");
Storage acbFlip;
acbFlip.words.fill(0xdeadbeefu);
auto* computeFlip = sceAgcAcbSetFlip(&acbFlip.buffer, 0xfedcba98u, -2, 0x12345678u, -0x123456789abcdefLL);
check(computeFlip == acbFlip.words.data() && std::equal(expected.begin(), expected.end(), computeFlip), "compute flip packet mismatch");
check(acbFlip.buffer.cursor_up == computeFlip + 6 && computeFlip[6] == 0xdeadbeefu, "compute flip overran allocation");
expectFailure([] { sceAgcAcbSetFlip(nullptr, 1, 0, 1, 0); });
Storage fullFlip;
fullFlip.buffer.cursor_down = fullFlip.words.data() + 5;
expectFailure([&] { sceAgcAcbSetFlip(&fullFlip.buffer, 1, 0, 1, 0); });
check(fullFlip.buffer.cursor_up == fullFlip.words.data(), "failed compute flip advanced cursor");
check(sceAgcSuspendPoint() == 0, "empty suspend failed");
}
@@ -463,8 +492,14 @@ void testRegisterRange() {
check(packet == storage.words.data() + 6 && storage.buffer.cursor_up == storage.words.data() + 12, "incorrect populated register range allocation");
check(storage.words == expected, "register values were not copied correctly");
const auto* misaligned = reinterpret_cast<const std::uint32_t*>(reinterpret_cast<const unsigned char*>(values.data()) + 1);
expectFailure([&] { sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 4); });
check(storage.words == expected && storage.buffer.cursor_up == storage.words.data() + 12, "misaligned register values modified command buffer");
packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 3);
expected[12] = 0xc0037600u;
expected[13] = 0x8c;
expected[14] = 0x02000000u;
expected[15] = 0x03000000u;
expected[16] = 0x04000000u;
check(packet == storage.words.data() + 12 && storage.buffer.cursor_up == storage.words.data() + 17, "incorrect misaligned register range allocation");
check(storage.words == expected, "misaligned register values were not read byte for byte");
}
void testPacketPayloadAddress() {
@@ -498,18 +533,28 @@ void testMemory() {
check(packet[8] == 0x55667788u && packet[9] == 0x11223344u && packet[12] == 2u, "wait reference or poll interval mismatch");
sceAgcWaitRegMemPatchReference(packet, 7);
check(packet[8] == 7 && packet[9] == 0x11223344u, "reference patch changed the high word");
const auto before = storage.words;
expectFailure([&] { sceAgcWaitRegMemPatchReference(packet, 0x100000000ull); });
check(storage.words == before, "invalid memory operation modified packet memory");
auto* truncated = Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0xffffffff00000001ull, 32, __func__);
check(truncated[8] == 0u && truncated[9] == 1u, "32-bit wait did not keep the low halves of the reference and mask");
sceAgcWaitRegMemPatchMask(packet, 0x0f0f0f0fu);
check(packet[10] == 0x0f0f0f0fu && packet[11] == 0xffffffffu && packet[8] == 7, "64-bit mask patch changed the wrong word");
sceAgcWaitRegMemPatchMask(truncated, 0xff00u);
check(truncated[9] == 0xff00u && truncated[8] == 0u && truncated[10] == 2u, "32-bit mask patch changed the wrong word");
for (auto* waitPacket : {packet, truncated}) {
const auto lowMask = waitPacket == packet ? 7u : 3u;
waitPacket[6] |= lowMask;
waitPacket[7] = 0xa5fc0000u;
const auto tag = waitPacket[2] & 0xffff0000u;
const std::uintptr_t address = 0x1236abcd12345679ull;
check(sceAgcWaitRegMemPatchAddress(waitPacket, reinterpret_cast<const volatile void*>(address)) == 0, "wait address patch failed");
check(waitPacket[2] == (tag | 0xabcdu) && waitPacket[3] == 0x12345679u, "wait wrapper address patch mismatch");
check(waitPacket[6] == ((0x12345679u & ~lowMask) | lowMask) && waitPacket[7] == 0xa5feabcdu, "wait address patch did not preserve control bits");
for (std::uint32_t comparison = 0; comparison <= 255; ++comparison) {
const auto control = waitPacket[5] & ~7u;
check(sceAgcWaitRegMemPatchCompareFunction(waitPacket, static_cast<std::uint8_t>(comparison)) == 0, "wait comparison patch failed");
check(waitPacket[5] == (control | (comparison & 7u)), "wait comparison patch changed control bits");
}
}
const auto beforeMask = storage.words;
expectFailure([&] { sceAgcWaitRegMemPatchMask(packet, 0x100000000ull); });
expectFailure([&] { sceAgcWaitRegMemPatchMask(truncated, 0x100000000ull); });
expectFailure([&] { sceAgcWaitRegMemPatchMask(packet + 4, 1); });
check(storage.words == beforeMask, "invalid mask patch modified packet memory");
}
@@ -624,12 +669,12 @@ void testDefaults() {
check(sceAgcInit_0090(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state");
check(sceAgcInit_0090(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state");
expectFailure([] { sceAgcInit_0090(nullptr, 8); });
expectFailure([&] { sceAgcInit_0090(&state, 14); });
check(sceAgcInit_0090(&state, 14) == 0 && state == 0x12345678, "out-of-range AGC initialization version changed caller state");
check(sceAgcInit(8) == 0, "AGC version initialization failed");
check(sceAgcInit(13) == 0, "AGC version 13 initialization failed");
check(sceAgcInit(14) == SCE_AGC_ERROR_INVALID_ARGUMENT, "unsupported AGC version did not return invalid argument");
check(sceAgcInit(0xffffffffu) == SCE_AGC_ERROR_INVALID_ARGUMENT, "maximum AGC version did not return invalid argument");
check(sceAgcInit(0xffffc278u) == SCE_AGC_ERROR_INVALID_ARGUMENT, "pointer-valued AGC argument did not return invalid argument");
check(sceAgcInit(14) == 0, "out-of-range AGC version did not return success");
check(sceAgcInit(0xffffffffu) == 0, "maximum AGC version did not return success");
check(sceAgcInit(0xffffc278u) == 0, "pointer-valued AGC argument did not return success");
for (std::uint32_t version = 0; version < 14; ++version) {
for (const bool internal : {false, true}) {
auto* first = Agc::Command::GetRegisterDefaults(version, internal, __func__);
@@ -639,10 +684,79 @@ void testDefaults() {
auto* internalDefaults = sceAgcGetRegisterDefaultsInternal();
check(internalDefaults != nullptr && internalDefaults == Agc::Command::GetRegisterDefaults(0, true, __func__) && internalDefaults == sceAgcGetRegisterDefaults2Internal(0), "internal register defaults are not the baseline internal table");
check(internalDefaults != sceAgcGetRegisterDefaults(), "internal register defaults returned the public table");
constexpr int kInvalidArgument = static_cast<int>(0x80890001u);
std::array<std::uint32_t, 512> flatCxBuffer{};
check(sceAgcGetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == kInvalidArgument, "null destination was accepted");
check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), 0) == 0, "zero size copy failed");
check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "flat CX state default retrieval failed");
check(flatCxBuffer[0x001] == 0x11000100u, "flat CX state register 0x001 mismatch");
check(flatCxBuffer[0x007] == 0x0000ffffu, "flat CX state register 0x007 mismatch");
check(flatCxBuffer[0x12d] == 0x3f800000u, "flat CX state register 0x12d mismatch");
check(flatCxBuffer[0x1e0] == 0x20010001u, "flat CX state register 0x1e0 mismatch");
std::array<std::uint32_t, 513> overBuffer{};
overBuffer[512] = 0x12345678u;
check(sceAgcGetDefaultCxStateFlat(overBuffer.data(), sizeof(overBuffer)) == 0, "oversized copy failed");
check(overBuffer[512] == 0x12345678u, "oversized copy overflowed buffer");
check(Agc::Command::GetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == kInvalidArgument, "null command destination was accepted");
check(Agc::Command::GetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "command flat CX state retrieval failed");
expectFailure([] { Agc::Command::GetRegisterDefaults(14, false, __func__); });
expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); });
}
struct DefaultsType {
std::uint32_t key;
std::uint32_t bankIndex;
};
std::uint32_t colourTargetBlock(const void* root) {
const auto* bytes = static_cast<const unsigned char*>(root);
ShaderRegister** contextBlocks = nullptr;
const DefaultsType* types = nullptr;
std::uint32_t count = 0;
std::memcpy(&contextBlocks, bytes, sizeof(contextBlocks));
std::memcpy(&types, bytes + 0x30, sizeof(types));
std::memcpy(&count, bytes + 0x38, sizeof(count));
check(contextBlocks != nullptr && types != nullptr && count > 0, "register defaults root has no context blocks or types");
std::uint32_t contexts = 0;
std::uint32_t matches = 0;
std::uint32_t block = 0;
for (std::uint32_t entry = 0; entry < count; ++entry) {
if ((types[entry].bankIndex & 3u) == 0)
++contexts;
if (types[entry].key != 0x38e92c91u)
continue;
check((types[entry].bankIndex & 3u) == 0, "MRT0 defaults are not in the context bank");
block = types[entry].bankIndex >> 2;
++matches;
}
check(matches == 1 && block < contexts && contextBlocks[block] != nullptr, "MRT0 defaults key is missing, repeated or outside the context blocks");
constexpr std::array<std::uint32_t, 16> offsets{0x318, 0x31b, 0x31c, 0x31d, 0x31e, 0x31f, 0x321, 0x323, 0x324, 0x325, 0x390, 0x398, 0x3a0, 0x3a8, 0x3b0, 0x3b8};
for (std::size_t i = 0; i < offsets.size(); ++i)
check(contextBlocks[block][i].offset == offsets[i], "MRT0 defaults block does not hold the colour target registers");
return block;
}
void testDefaultsTypes() {
const auto* legacy = sceAgcGetRegisterDefaults();
check(legacy != sceAgcGetRegisterDefaults2(13), "legacy register defaults reused the compact versioned table");
check(colourTargetBlock(legacy) == 78, "MRT0 defaults are not block 78 of the 137-entry table");
const auto checkCompactTypes = [](const void* root, std::uint32_t count, std::uint32_t key,
std::uint32_t packedType, std::uint32_t extra) {
const auto* bytes = static_cast<const unsigned char*>(root);
const std::uint32_t* types = nullptr;
std::uint32_t actualCount = 0;
std::memcpy(&types, bytes + 0x30, sizeof(types));
std::memcpy(&actualCount, bytes + 0x38, sizeof(actualCount));
check(types != nullptr && actualCount == count, "versioned register defaults have an invalid compact type table");
check(types[0] == key && types[1] == packedType && types[2] == extra,
"versioned register defaults did not preserve their compact type entry");
};
checkCompactTypes(sceAgcGetRegisterDefaults2(0), 150, 0xe24f806du, 0x00040400u, 0);
checkCompactTypes(sceAgcGetRegisterDefaults2(10), 128, 0xe24f806du, 0x00040400u, 0);
checkCompactTypes(sceAgcGetRegisterDefaults2Internal(0), 15, 0xf58fea31u, 0x00040400u, 0);
}
void testEventWrite() {
Storage storage;
const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1000));
@@ -690,6 +804,7 @@ int main(int argc, char** argv) {
try {
if (argc == 2 && std::strcmp(argv[1], "defaults") == 0) {
testDefaults();
testDefaultsTypes();
LibcRunShutdown_nid_postfix();
std::puts("AGC initialization tests passed");
return 0;
@@ -703,6 +818,7 @@ int main(int argc, char** argv) {
testWorkloads();
testWorkloadStreams();
testIndexBuffer();
testIndexIndirectArgs();
testIndirectBase();
testContextState();
testFlip();
@@ -713,6 +829,7 @@ int main(int argc, char** argv) {
testAtomicGds();
testDebugRaiseException();
testDefaults();
testDefaultsTypes();
LibcRunShutdown_nid_postfix();
std::puts("AGC command tests passed");
return 0;
@@ -8,6 +8,7 @@
#include <stdexcept>
extern "C" std::uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp(CommandBuffer* buf, std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation);
namespace {
@@ -37,6 +38,15 @@ void testSizes() {
check(sceAgcDcbContextStateOp_0100(&buffer, operation) == words.data(), "context state op did not start at the cursor");
const auto written = static_cast<std::uint64_t>(buffer.cursor_up - words.data()) * sizeof(std::uint32_t);
check(written == sizes[operation], "context state size differs from the written packets");
std::array<std::uint32_t, 64> unversionedWords{};
CommandBuffer unversionedBuffer{unversionedWords.data(), unversionedWords.data() + unversionedWords.size(),
unversionedWords.data(), unversionedWords.data() + unversionedWords.size(),
nullptr, nullptr, 0};
check(sceAgcDcbContextStateOp(&unversionedBuffer, operation) == unversionedWords.data(), "context state op did not start at the cursor");
const auto unversionedWritten = static_cast<std::uint64_t>(unversionedBuffer.cursor_up - unversionedWords.data()) * sizeof(std::uint32_t);
check(unversionedWritten == sizes[operation], "context state size differs from the written packets");
check(words == unversionedWords, "unversioned context state op mismatch with versioned op");
}
}
@@ -2,6 +2,7 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
@@ -11,6 +12,9 @@ extern "C" {
std::uint32_t APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize();
std::uint32_t APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize();
std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer*, std::uint8_t, std::uint16_t, std::uint8_t, std::uint8_t, const volatile Label*, std::uint8_t, std::uint64_t, std::uint16_t, std::uint16_t, std::uint8_t, std::uint32_t);
std::uint32_t APS5_VABI sceAgcDcbAtomicGdsGetSize();
std::uint32_t* APS5_VABI sceAgcDcbAtomicGds_0900(CommandBuffer*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint16_t, std::uint16_t, std::uint32_t, std::uint32_t, std::uint64_t, std::uint64_t);
std::uint32_t* APS5_VABI sceAgcDcbAtomicGds(CommandBuffer*, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint16_t, std::uint16_t, std::uint32_t, std::uint32_t, std::uint64_t, std::uint64_t);
}
namespace {
@@ -19,19 +23,43 @@ void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void testEndOfShaderActionSize() {
std::array<std::uint32_t, 16> words{};
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
const auto* packet = sceAgcCbReleaseMem(&buffer, 0x2f, 0, 0, 0, nullptr, 0, 0, 0, 0, 0, 0);
const auto releaseBytes = static_cast<std::uint32_t>((buffer.cursor_up - packet) * sizeof(std::uint32_t));
check(((packet[0] >> 8u) & 0xffu) == 0x49u && releaseBytes == 32, "end-of-shader RELEASE_MEM size mismatch");
check(sceAgcDcbQueueEndOfShaderActionGetSize() == releaseBytes, "DCB end-of-shader action size differs from its RELEASE_MEM");
check(sceAgcDcbQueueEndOfShaderActionGetSize() == sceAgcAcbQueueEndOfShaderActionGetSize(), "DCB and ACB end-of-shader action sizes differ");
}
void testAtomicGds() {
constexpr std::uint32_t expectedBytes = 11u * sizeof(std::uint32_t);
check(sceAgcDcbAtomicGdsGetSize() == expectedBytes, "DCB atomic GDS size does not match 11-dword packet");
std::array<std::uint32_t, 32> words{};
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
const auto* packet = sceAgcDcbAtomicGds_0900(&buffer, 0x261u, 1, 3, 7, 0x100, 0x200, 0x00ff00ffu, 0x12345678u, 0x1122334455667788ull, 0x99aabbccddeeff00ull);
check(packet != nullptr && packet == words.data(), "DCB atomic GDS returned unexpected packet pointer");
check(buffer.cursor_up == words.data() + 11, "DCB atomic GDS cursor advance does not match 11 dwords");
check(packet[0] == 0xc0091d00u, "DCB atomic GDS header opcode mismatch");
std::array<std::uint32_t, 32> unversionedWords{};
CommandBuffer unversionedBuffer{unversionedWords.data(), unversionedWords.data() + unversionedWords.size(), unversionedWords.data(), unversionedWords.data() + unversionedWords.size(), nullptr, nullptr, 0};
const auto* unversionedPacket = sceAgcDcbAtomicGds(&unversionedBuffer, 0x261u, 1, 3, 7, 0x100, 0x200, 0x00ff00ffu, 0x12345678u, 0x1122334455667788ull, 0x99aabbccddeeff00ull);
check(unversionedPacket != nullptr && unversionedPacket == unversionedWords.data(), "unversioned DCB atomic GDS returned unexpected packet pointer");
check(unversionedBuffer.cursor_up == unversionedWords.data() + 11, "unversioned DCB atomic GDS cursor advance does not match 11 dwords");
check(std::equal(words.begin(), words.begin() + 11, unversionedWords.begin()), "versioned and unversioned DCB atomic GDS emitted different packets");
}
}
int main() {
try {
std::array<std::uint32_t, 16> words{};
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
const auto* packet = sceAgcCbReleaseMem(&buffer, 0x2f, 0, 0, 0, nullptr, 0, 0, 0, 0, 0, 0);
const auto releaseBytes = static_cast<std::uint32_t>((buffer.cursor_up - packet) * sizeof(std::uint32_t));
check(((packet[0] >> 8u) & 0xffu) == 0x49u && releaseBytes == 32, "end-of-shader RELEASE_MEM size mismatch");
check(sceAgcDcbQueueEndOfShaderActionGetSize() == releaseBytes, "DCB end-of-shader action size differs from its RELEASE_MEM");
check(sceAgcDcbQueueEndOfShaderActionGetSize() == sceAgcAcbQueueEndOfShaderActionGetSize(), "DCB and ACB end-of-shader action sizes differ");
testEndOfShaderActionSize();
testAtomicGds();
LibcRunShutdown_nid_postfix();
std::puts("AGC end-of-shader action size tests passed");
std::puts("AGC end-of-shader action and atomic GDS size tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
@@ -0,0 +1,48 @@
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include <cstdint>
#include <cstdio>
#include <stdexcept>
extern "C" {
int APS5_VABI sceAgcSetShaderInstrumentation(std::uint32_t);
std::uint32_t APS5_VABI sceAgcGetShaderInstrumentation();
std::uint32_t APS5_VABI sceAgcDcbBeginOcclusionQueryGetSize(std::int32_t);
std::uint32_t APS5_VABI sceAgcDcbEndOcclusionQueryGetSize();
}
namespace {
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void testShaderInstrumentation() {
check(sceAgcGetShaderInstrumentation() == 0, "shader instrumentation starts cleared");
check(sceAgcSetShaderInstrumentation(0x80000005u) == 0, "setting shader instrumentation failed");
check(sceAgcGetShaderInstrumentation() == 0x80000005u, "shader instrumentation flags were not kept");
check(sceAgcSetShaderInstrumentation(0) == 0 && sceAgcGetShaderInstrumentation() == 0, "shader instrumentation was not cleared");
}
void testOcclusionQuerySizes() {
check(sceAgcDcbBeginOcclusionQueryGetSize(0) == 0x10u, "begin occlusion query size for type 0");
check(sceAgcDcbBeginOcclusionQueryGetSize(1) == 0x120u, "begin occlusion query size for type 1");
check(sceAgcDcbBeginOcclusionQueryGetSize(2) == 0, "begin occlusion query size for an unknown type");
check(sceAgcDcbEndOcclusionQueryGetSize() == 0x10u, "end occlusion query size");
}
}
int main() {
try {
testShaderInstrumentation();
testOcclusionQuerySizes();
LibcRunShutdown_nid_postfix();
std::puts("AGC shader instrumentation and occlusion query size tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}
@@ -12,8 +12,10 @@
#include <span>
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_nid_postfix(ShaderRegister* regs, const Shader* gs, const Shader* ps);
namespace {
@@ -61,14 +63,14 @@ void checkIdentity(const Registers& regs, std::uint32_t first, const char* messa
void testIdentity() {
auto regs = filled();
check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, nullptr) == 0, "mapping without shaders failed");
check(sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, nullptr) == 0, "mapping without shaders failed");
checkIdentity(regs, 0, "mapping without a pixel shader is not the identity");
Shader ps{};
regs = filled();
check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed");
check(sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed");
checkIdentity(regs, 0, "mapping without inputs is not the identity");
regs = filled();
check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed");
check(sceAgcUpdateInterpolantMapping_10000(regs.data(), nullptr, &ps) == 0, "update without inputs failed");
check(preparedCount == 0u, "update without inputs prepared registers");
const auto untouched = filled();
check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers");
@@ -94,7 +96,7 @@ void testMapping() {
ps.num_input_semantics = static_cast<std::uint32_t>(inputs.size());
auto split = filled();
check(sceAgcUnknownCreateInterpolantMapping(split.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(sceAgcCreateInterpolantMapping_10000(split.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(std::memcmp(split.data(), preparedMapping.data(), sizeof(split)) == 0, "prepared split mapping differs from published registers");
for (std::uint32_t i = 0; i < inputs.size(); ++i) {
check(split[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "interpolant register offset changed");
@@ -109,7 +111,7 @@ void testMapping() {
auto update = filled();
const auto untouched = filled();
const std::size_t written = inputs.size() * sizeof(ShaderRegister);
check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed");
check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update mapping failed");
check(preparedCount == inputs.size(), "update prepared registers past its inputs");
check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers");
check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping");
@@ -142,7 +144,7 @@ void testUpdateMapping() {
auto update = filled();
const auto untouched = filled();
check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed");
check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update mapping failed");
check(update[0].value == 0x00000625u, "custom interpolant update changed");
check(update[1].value == 0x01380107u, "low f16 half update changed");
check(update[2].value == 0x02480220u, "unmatched high f16 half update changed");
@@ -154,17 +156,50 @@ void testUpdateMapping() {
check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs");
auto create = filled();
check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(sceAgcCreateInterpolantMapping_10000(create.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping");
checkIdentity(create, static_cast<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;
inputs[1].default_value = 0;
inputs[3].default_value = 0;
update = filled();
check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update with split f16 defaults failed");
check(update[1].value == 0x01380007u && update[3].value == 0x03680409u, "f16 DEFAULT_VAL does not come from the low default");
}
void testUnversionedMapping() {
std::array<ShaderSemantic, 5> inputs{
semantic(1, 0, 2), semantic(2, 0, 2), semantic(5, 0, 2), semantic(3, 0, 0), semantic(4, 0, 3)};
inputs[0].is_flat_shaded = 1;
inputs[0].default_value = 1;
inputs[0].default_value_hi = 3;
inputs[1].default_value_hi = 1;
inputs[2].is_flat_shaded = 1;
inputs[2].default_value_hi = 2;
inputs[3].is_flat_shaded = 1;
inputs[3].default_value = 2;
std::array<ShaderSemantic, 4> outputs{semantic(1, 7, 3), semantic(2, 4, 1), semantic(3, 9, 0), semantic(4, 2, 1)};
Shader gs{};
gs.output_semantics = outputs.data();
gs.num_output_semantics = static_cast<std::uint16_t>(outputs.size());
Shader ps{};
ps.input_semantics = inputs.data();
ps.num_input_semantics = static_cast<std::uint32_t>(inputs.size());
const std::array<std::uint32_t, 5> flatDefault{0x02680527u, 0x02380024u, 0x02580020u, 0x00000629u, 0x03180002u};
const std::size_t written = inputs.size() * sizeof(ShaderRegister);
const auto untouched = filled();
auto create = filled();
check(sceAgcCreateInterpolantMapping_nid_postfix(create.data(), &gs, &ps) == 0, "unversioned mapping failed");
for (std::uint32_t i = 0; i < inputs.size(); ++i) {
check(create[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i && create[i].value == flatDefault[i], "unversioned mapping changed");
}
checkIdentity(create, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
auto update = filled();
check(sceAgcUpdateInterpolantMapping_nid_postfix(update.data(), &gs, &ps) == 0, "unversioned update failed");
check(std::memcmp(update.data(), create.data(), written) == 0, "unversioned update differs from its mapping");
check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "unversioned update wrote past its inputs");
}
void testRejections() {
@@ -177,15 +212,15 @@ void testRejections() {
std::memset(regs.data(), 0xcc, sizeof(regs));
const auto saved = regs;
expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcCreateInterpolantMapping_10000(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), &gs, &ps); });
check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers");
ps.num_input_semantics = 1;
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); });
expectFailure([&] { sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcCreateInterpolantMapping_10000(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), &gs, nullptr); });
}
}
@@ -208,8 +243,9 @@ int main() {
testIdentity();
testMapping();
testUpdateMapping();
testUnversionedMapping();
testRejections();
check(mappings == 7u && links == 5u, "mapping preparation call count changed");
check(mappings == 10u && links == 8u, "mapping preparation call count changed");
LibcRunShutdown_nid_postfix();
std::puts("AGC interpolant mapping tests passed");
return 0;
@@ -9,6 +9,7 @@
#include <tuple>
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, std::uint32_t* cmd, int type);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type);
namespace {
@@ -44,6 +45,9 @@ void testRanges() {
std::uint32_t* address = nullptr;
check(sceAgcGetDataPacketPayloadAddress_0090(&address, words.data(), type) == 0 && address == range.base,
"payload range base differs from the payload address");
std::uint32_t* unversionedAddress = nullptr;
check(sceAgcGetDataPacketPayloadAddress(&unversionedAddress, words.data(), type) == 0 && unversionedAddress == address,
"unversioned payload address differs from versioned payload address");
}
}
@@ -60,6 +64,10 @@ void testRejections() {
misalignedRange = reinterpret_cast<SceAgcMemoryRange*>(storage.data() + 2);
}
expectFailure([&] { sceAgcGetDataPacketPayloadRange(misalignedRange, words.data(), 1); });
std::uint32_t* address = nullptr;
expectFailure([&] { sceAgcGetDataPacketPayloadAddress(nullptr, words.data(), 1); });
expectFailure([&] { sceAgcGetDataPacketPayloadAddress(&address, nullptr, 1); });
expectFailure([&] { sceAgcGetDataPacketPayloadAddress(&address, misaligned, 0); });
}
}
Loaded 100 of 2568 files, more files were not shown because too many files have changed in this diff. Show more