mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-22 00:22:57 -05:00
Merge origin/main into PR 2528
Co-authored-by: Aryan Sharma <aryan152015@gmail.com>
This commit is contained in:
commit
c54d294a05
2883 files changed
+405684
-17609
No files matched your search
@@ -0,0 +1,81 @@
|
||||
title: "Game name (PPSA00000)"
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
One thread per title, for every platform: search this category for the title ID first. If a thread exists, reply there, also when your OS or GPU differs from the one it reports.
|
||||
Name the discussion `Game name (title ID)`, for example `DOOM Shareware (PPSA99666)`, without the platform.
|
||||
Bug reports and implementation work belong in issues and pull requests; link them here as the blocker.
|
||||
Per-game progress is also tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), built from public GitHub data. Only share material you have the right to distribute.
|
||||
- type: checkboxes
|
||||
id: searched
|
||||
attributes:
|
||||
label: Existing thread
|
||||
options:
|
||||
- label: I searched this category for the title ID and no thread exists for this title on any platform.
|
||||
required: true
|
||||
- type: input
|
||||
id: title
|
||||
attributes:
|
||||
label: Title
|
||||
description: Name and title ID from `sce_sys/param.json`.
|
||||
placeholder: DOOM Shareware (PPSA99666)
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
### First report
|
||||
The fields below describe one run. Other systems and content versions are added to the thread as replies.
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Content version tested
|
||||
description: The content version from `sce_sys/param.json`.
|
||||
placeholder: 01.001.001
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: commit
|
||||
attributes:
|
||||
label: AnyPS5 commit
|
||||
description: The `main` commit that was built, or the release that was used, and any pull requests merged on top.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: System tested
|
||||
description: OS, GPU and driver version, and the compiler if you built it yourself.
|
||||
placeholder: Arch Linux, GCC 16.2.1, RTX 5080 (NVIDIA 615.78.08)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: reached
|
||||
attributes:
|
||||
label: Furthest point
|
||||
options:
|
||||
- Does not convert
|
||||
- Stops before the first frame
|
||||
- Logos or loading screen
|
||||
- Menu
|
||||
- In game
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: seen
|
||||
attributes:
|
||||
label: What is shown
|
||||
description: The last screen that was seen, the frame rate there, and what works or is missing (picture, audio, input).
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: blocker
|
||||
attributes:
|
||||
label: Current blocker
|
||||
description: The line where it stops, the issue that tracks it, and the open pull requests and local changes the title still needs.
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: How it was run
|
||||
description: Commands and environment variables, and what the title needed beyond the usage guide.
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Performance report
|
||||
description: A slow scene or a change in speed, measured with a performance trace so that reports can be compared.
|
||||
title: "perf: "
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Capture the scene with `APS5_TRACE_FRAMES=<start>:<count>` (presents; `APS5_TRACE_SECONDS=<start>:<length>` also works), as described under [Performance trace](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/BUILD.md#performance-trace). It writes `aps5-trace-<commit>-<pid>.json.gz` and appends one line to `aps5-summary.jsonl`.
|
||||
|
||||
- Run the scene once and restart the title, so the shader and pipeline caches are warm.
|
||||
- Use the same scene, window, resolution and settings for every capture you compare.
|
||||
- Set no other `APS5_TRACE_*` or `APS5_PROFILE_*` switch and do not redirect the log; they cost time.
|
||||
- Capture the base and the change on the same machine in the same session.
|
||||
|
||||
A trace holds timings, scope and thread names and counts, no game data. The summary line names the GPU, driver and OS.
|
||||
- type: input
|
||||
id: scene
|
||||
attributes:
|
||||
label: Title and scene
|
||||
description: Title ID and what the window covers (menu, level, position, cutscene).
|
||||
placeholder: PPSA01325, main menu after the intro
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: observed
|
||||
attributes:
|
||||
label: What is slow or what changed
|
||||
description: What you see (frame time, stutter, a phase that grew) and, for a comparison, the two commits.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary lines
|
||||
description: The line of aps5-summary.jsonl for each capture. It holds the commit, the APS5_* switches, GPU, driver, OS, resolution and frame times.
|
||||
render: json
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: trace
|
||||
attributes:
|
||||
label: Traces
|
||||
description: Attach each aps5-trace-<commit>-<pid>.json.gz (drag it into this field). They open in ui.perfetto.dev.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: digest
|
||||
attributes:
|
||||
label: Digest
|
||||
description: The output of `python3 tools/trace_digest.py <trace>` (with `--stacks <file>.collapsed.txt` if you sampled the worker).
|
||||
- type: textarea
|
||||
id: stacks
|
||||
attributes:
|
||||
label: Worker stacks
|
||||
description: Optional. The collapsed-stack file of APS5_SAMPLE_WORKER (<file stem>.collapsed.txt), attached. It opens in speedscope.app.
|
||||
- type: checkboxes
|
||||
id: protocol
|
||||
attributes:
|
||||
label: Capture
|
||||
options:
|
||||
- label: The caches were warm (the scene ran once and the title was restarted)
|
||||
required: true
|
||||
- label: No other APS5_TRACE_* or APS5_PROFILE_* switch was set and the log was not redirected
|
||||
required: true
|
||||
- label: Captures compared here come from the same machine, scene and settings
|
||||
@@ -0,0 +1,3 @@
|
||||
self-hosted-runner:
|
||||
labels:
|
||||
- anyps5
|
||||
@@ -1,16 +1,19 @@
|
||||
### What
|
||||
|
||||
### Tested
|
||||
<!-- OS, title or homebrew, what was checked. "Not tested" is a valid answer. -->
|
||||
<!-- OS, title or homebrew, what was checked. "Not tested" is a valid answer. A performance change: the summary lines of main and of this PR from the same machine and scene, with the traces attached, captured as the Performance report issue form describes. -->
|
||||
|
||||
### Checklist
|
||||
<!-- Rules behind each item: https://github.com/boykopovar/AnyPS5/blob/main/CONTRIBUTING.md -->
|
||||
- [ ] Based on current `main`; no other open PR implements the same functions
|
||||
- [ ] One topic per PR; follow-ups go in a new PR
|
||||
- [ ] No comments in code except [technical debt](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/CONVENTIONS.md)
|
||||
- [ ] Unimplemented paths throw (`NotImplemented_nid_no_patch`); silent stubs are listed in [TechnicalDebt](https://github.com/boykopovar/AnyPS5/blob/main/docs/dev/TechnicalDebt.md#silent-stubs)
|
||||
- [ ] Unimplemented paths throw (`NotImplemented_nid_no_patch`); silent stubs are listed under Silent stubs in their library's [technical debt](https://github.com/boykopovar/AnyPS5/tree/main/docs/dev/debt) file
|
||||
- [ ] No notes, investigation `.md` files or images added to the repository (attach them to this PR)
|
||||
- [ ] New third-party code is a submodule built from source
|
||||
- [ ] Generic behaviour, not specific to one title
|
||||
- [ ] Depends on: <!-- #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 -->
|
||||
@@ -14,18 +14,34 @@ jobs:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/area-state.json
|
||||
key: area-labels-${{ github.run_id }}
|
||||
restore-keys: area-labels-
|
||||
- id: prs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
state=$RUNNER_TEMP/area-state.json
|
||||
[ -s "$state" ] || echo '{}' > "$state"
|
||||
config=$(gh api "repos/$GITHUB_REPOSITORY/contents/.github/labeler.yml" --jq .sha)
|
||||
gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 1000 --json number,headRefOid \
|
||||
--jq "map({key: (.number | tostring), value: (.headRefOid + \"-$config\")}) | from_entries" > "$RUNNER_TEMP/area-open.json"
|
||||
jq -r --slurpfile old "$state" 'to_entries[] | select($old[0][.key] != .value) | .key' "$RUNNER_TEMP/area-open.json" | head -n 40 > "$RUNNER_TEMP/area-todo"
|
||||
jq -c --slurpfile open "$RUNNER_TEMP/area-open.json" --rawfile todo "$RUNNER_TEMP/area-todo" \
|
||||
'with_entries(select(.key | in($open[0]))) + ($open[0] | with_entries(select(.key | IN($todo | split("\n")[]))))' "$state" > "$RUNNER_TEMP/area-next.json"
|
||||
{
|
||||
echo 'numbers<<EOF'
|
||||
gh pr list -R "$GITHUB_REPOSITORY" --base "${{ github.event.repository.default_branch }}" -L 200 --json number --jq '.[].number'
|
||||
cat "$RUNNER_TEMP/area-todo"
|
||||
echo EOF
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- if: steps.prs.outputs.numbers != ''
|
||||
uses: actions/checkout@v7
|
||||
- if: steps.prs.outputs.numbers != ''
|
||||
uses: actions/labeler@v7
|
||||
with:
|
||||
pr-number: ${{ steps.prs.outputs.numbers }}
|
||||
- run: mv "$RUNNER_TEMP/area-next.json" "$RUNNER_TEMP/area-state.json"
|
||||
- uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/area-state.json
|
||||
key: area-labels-${{ github.run_id }}
|
||||
+500
-81
@@ -9,9 +9,24 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr:
|
||||
description: Pull request number to build
|
||||
description: Pull request numbers to build together, separated by spaces or commas (up to 40)
|
||||
required: true
|
||||
type: string
|
||||
linux:
|
||||
description: Also build and test on Linux
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
full:
|
||||
description: Run the full Linux and Windows builds (off runs only the relinker builds and tests on Linux, Windows and macOS)
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
select:
|
||||
description: Build and run only the tests whose build inputs the pull requests change (push builds of main always run every test)
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -39,28 +54,132 @@ jobs:
|
||||
pr: ${{ steps.gate.outputs.pr }}
|
||||
head: ${{ steps.gate.outputs.head }}
|
||||
relinker: ${{ steps.gate.outputs.relinker }}
|
||||
register-context: ${{ steps.gate.outputs.register-context }}
|
||||
write-watch: ${{ steps.gate.outputs.write-watch }}
|
||||
full: ${{ steps.gate.outputs.full }}
|
||||
relinker-hosts: ${{ steps.gate.outputs.relinker-hosts }}
|
||||
steps:
|
||||
- uses: actions/github-script@v8
|
||||
id: gate
|
||||
env:
|
||||
PR_NUMBER: ${{ inputs.pr }}
|
||||
LINUX_BUILD: ${{ inputs.linux }}
|
||||
FULL_BUILD: ${{ inputs.full }}
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
const workflow = '.github/workflows/build.yml';
|
||||
const libc = 'core/libs/prx/libc/';
|
||||
const jobOfLine = (text) => {
|
||||
let inJobs = false;
|
||||
let job = '';
|
||||
return text.split('\n').map((line) => {
|
||||
if (/^\S/.test(line)) {
|
||||
inJobs = /^jobs:\s*$/.test(line);
|
||||
job = '';
|
||||
} else if (inJobs) {
|
||||
job = /^ ([\w-]+):\s*$/.exec(line)?.[1] ?? job;
|
||||
}
|
||||
return job;
|
||||
});
|
||||
};
|
||||
const patchedJobs = (patch, text) => {
|
||||
const jobs = jobOfLine(text);
|
||||
const touched = new Set();
|
||||
let line = 1;
|
||||
for (const row of patch.split('\n')) {
|
||||
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(row);
|
||||
if (hunk) {
|
||||
line = Number(hunk[1]);
|
||||
} else if (row.startsWith('+')) {
|
||||
touched.add(jobs[line - 1] ?? '');
|
||||
line += 1;
|
||||
} else if (row.startsWith('-')) {
|
||||
touched.add(jobs[line - 2] ?? '').add(jobs[line - 1] ?? '');
|
||||
} else if (row.startsWith(' ')) {
|
||||
line += 1;
|
||||
}
|
||||
}
|
||||
return touched;
|
||||
};
|
||||
const workflowJobs = new Set();
|
||||
const readWorkflow = async (files, ref) => {
|
||||
const file = files.find((file) => file.filename === workflow || file.previous_filename === workflow);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (file.filename !== workflow || !file.patch) {
|
||||
workflowJobs.add('*');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({ owner, repo, path: workflow, ref });
|
||||
const text = Buffer.from(data.content, 'base64').toString('utf8');
|
||||
patchedJobs(file.patch, text).forEach((job) => workflowJobs.add(job));
|
||||
} catch (error) {
|
||||
core.warning(`Could not read ${workflow} at ${ref}, so every test job runs: ${error.message}`);
|
||||
workflowJobs.add('*');
|
||||
}
|
||||
};
|
||||
const select = (paths, all) => {
|
||||
const touches = (...prefixes) => all ||
|
||||
paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix));
|
||||
const edits = (job) => ['*', '', 'eligibility', job].some((name) => workflowJobs.has(name));
|
||||
return {
|
||||
relinker: edits('relinker') || touches(
|
||||
'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp',
|
||||
'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp',
|
||||
'tools/', 'cmake/', 'CMakeLists.txt'
|
||||
),
|
||||
registerContext: edits('register-context') ||
|
||||
touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc),
|
||||
writeWatch: edits('write-watch') || touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc),
|
||||
};
|
||||
};
|
||||
const relinkerHosts = (full, linux) => JSON.stringify([
|
||||
...(full && linux ? [] : [{ os: 'ubuntu-latest', compiler: 'g++' }]),
|
||||
...(full ? [] : [{ os: 'windows-latest', compiler: 'g++' }]),
|
||||
{ os: 'macos-latest', compiler: 'clang++' },
|
||||
{ os: 'macos-latest', compiler: 'clang++', architectures: 'x86_64' },
|
||||
]);
|
||||
if (context.eventName === 'push') {
|
||||
if (context.ref !== 'refs/heads/main') {
|
||||
throw new Error('Automatic full builds only run on main');
|
||||
}
|
||||
let paths = [];
|
||||
let all = true;
|
||||
if (!/^0+$/.test(context.payload.before)) {
|
||||
try {
|
||||
const { data } = await github.rest.repos.compareCommitsWithBasehead({
|
||||
owner, repo, basehead: `${context.payload.before}...${context.sha}`
|
||||
});
|
||||
const files = data.files ?? [];
|
||||
all = files.length >= 300;
|
||||
paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
|
||||
await readWorkflow(files, context.sha);
|
||||
} catch (error) {
|
||||
core.warning(`Could not list the files of this push, so every test job runs: ${error.message}`);
|
||||
}
|
||||
}
|
||||
const run = select(paths, all);
|
||||
core.setOutput('sha', context.sha);
|
||||
core.setOutput('pr', '');
|
||||
core.setOutput('head', '');
|
||||
core.setOutput('relinker', true);
|
||||
core.setOutput('relinker', run.relinker);
|
||||
core.setOutput('register-context', run.registerContext);
|
||||
core.setOutput('write-watch', run.writeWatch);
|
||||
core.setOutput('full', true);
|
||||
await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write();
|
||||
core.setOutput('relinker-hosts', relinkerHosts(true, true));
|
||||
await core.summary.addRaw(
|
||||
`Building main at ${context.sha}: full builds enabled, ` +
|
||||
`macOS relinker ${run.relinker ? 'enabled' : 'skipped'}, ` +
|
||||
`register context tests ${run.registerContext ? 'enabled' : 'skipped'}, write watch tests ${run.writeWatch ? 'enabled' : 'skipped'}`
|
||||
).write();
|
||||
return;
|
||||
}
|
||||
const { owner, repo } = context.repo;
|
||||
const manual = context.eventName === 'workflow_dispatch';
|
||||
const linux = process.env.LINUX_BUILD !== 'false';
|
||||
const full = manual && process.env.FULL_BUILD !== 'false';
|
||||
if (manual) {
|
||||
for (const username of new Set([context.actor, process.env.GITHUB_TRIGGERING_ACTOR])) {
|
||||
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username });
|
||||
@@ -68,45 +187,84 @@ jobs:
|
||||
throw new Error('Only collaborators with write access can start or rerun builds');
|
||||
}
|
||||
}
|
||||
if (!/^[1-9][0-9]*$/.test(process.env.PR_NUMBER)) {
|
||||
throw new Error('Enter a positive pull request number');
|
||||
if (!/^[1-9][0-9]*([ ,]+[1-9][0-9]*){0,39}$/.test(process.env.PR_NUMBER.trim())) {
|
||||
throw new Error('Enter up to 40 positive pull request numbers separated by spaces or commas');
|
||||
}
|
||||
}
|
||||
const pull_number = manual ? Number(process.env.PR_NUMBER) : context.payload.pull_request.number;
|
||||
if (!Number.isSafeInteger(pull_number) || pull_number < 1) {
|
||||
const numbers = manual
|
||||
? [...new Set(process.env.PR_NUMBER.trim().split(/[ ,]+/).map(Number))]
|
||||
: [context.payload.pull_request.number];
|
||||
if (numbers.some((pull_number) => !Number.isSafeInteger(pull_number) || pull_number < 1)) {
|
||||
throw new Error('Invalid pull request number');
|
||||
}
|
||||
let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
|
||||
for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }));
|
||||
const pulls = [];
|
||||
const paths = [];
|
||||
let truncated = false;
|
||||
for (const pull_number of numbers) {
|
||||
let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
|
||||
for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }));
|
||||
}
|
||||
if (!manual && pr.state !== 'open') {
|
||||
await core.summary.addRaw(`PR #${pull_number} was ${pr.merged ? 'merged' : 'closed'} before this run started: nothing to build`).write();
|
||||
return;
|
||||
}
|
||||
if (pr.state !== 'open' || pr.base.ref !== 'main') {
|
||||
throw new Error(`Pull request #${pull_number} must be open and target main`);
|
||||
}
|
||||
if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) {
|
||||
throw new Error('The pull request has newer commits; use the latest run');
|
||||
}
|
||||
if (pr.mergeable === false) {
|
||||
throw new Error(`Pull request #${pull_number} does not merge cleanly into main; rebase it first`);
|
||||
}
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 });
|
||||
truncated ||= files.length >= 3000;
|
||||
paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)));
|
||||
await readWorkflow(files, pr.head.sha);
|
||||
pulls.push({ number: pull_number, head: pr.head.sha });
|
||||
}
|
||||
if (pr.state !== 'open' || pr.base.ref !== 'main') {
|
||||
throw new Error('The pull request must be open and target main');
|
||||
}
|
||||
if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) {
|
||||
throw new Error('The pull request has newer commits; use the latest run');
|
||||
}
|
||||
if (pr.mergeable === false) {
|
||||
throw new Error('The pull request does not merge cleanly into main; rebase it first');
|
||||
}
|
||||
let relinker = manual;
|
||||
if (!manual) {
|
||||
const run = select(paths, truncated);
|
||||
let relinker = run.relinker;
|
||||
if (relinker && !manual) {
|
||||
const author = context.payload.pull_request.user.login;
|
||||
const { data: commits } = await github.rest.repos.listCommits({
|
||||
owner, repo, sha: 'main', author: pr.user.login, per_page: 1
|
||||
owner, repo, sha: 'main', author, per_page: 1
|
||||
});
|
||||
relinker = commits.length > 0;
|
||||
if (!relinker) {
|
||||
try {
|
||||
const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `repo:${owner}/${repo} is:pr is:merged base:main author:${author}`,
|
||||
per_page: 1
|
||||
});
|
||||
if (mergedPRs.incomplete_results) {
|
||||
throw new Error('the search results are incomplete');
|
||||
}
|
||||
relinker = mergedPRs.total_count > 0;
|
||||
} catch (error) {
|
||||
core.warning(`Skipping the relinker: could not search the merged pull requests of ${author}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const registerContext = run.registerContext;
|
||||
const writeWatch = run.writeWatch;
|
||||
const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' });
|
||||
core.setOutput('sha', main.commit.sha);
|
||||
core.setOutput('pr', String(pull_number));
|
||||
core.setOutput('head', pr.head.sha);
|
||||
core.setOutput('pr', pulls.map((pull) => pull.number).join(' '));
|
||||
core.setOutput('head', pulls.map((pull) => pull.head).join(' '));
|
||||
core.setOutput('relinker', relinker);
|
||||
core.setOutput('full', manual);
|
||||
core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`);
|
||||
core.setOutput('register-context', registerContext);
|
||||
core.setOutput('write-watch', writeWatch);
|
||||
core.setOutput('full', full);
|
||||
core.setOutput('relinker-hosts', relinkerHosts(full, linux));
|
||||
const merged = pulls.map((pull) => `PR #${pull.number} at ${pull.head} merged into main at ${main.commit.sha}`);
|
||||
merged.forEach((line) => core.info(line));
|
||||
await core.summary.addRaw(
|
||||
`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` +
|
||||
`relinker ${relinker ? 'enabled' : 'skipped because the author has no commit in main'}`
|
||||
`${merged.join(', then ')}: full builds ${full ? 'enabled' : 'disabled'}, ` +
|
||||
`relinker ${relinker ? (full ? (linux ? 'enabled on macOS (the full builds run the Linux and Windows relinker tests)' : 'enabled on Linux and macOS (the full Windows build runs the Windows relinker tests)') : 'enabled') : 'skipped: no relinker file changed, or no commit or merged pull request in main was found for the author'}, ` +
|
||||
`register context tests ${registerContext ? 'enabled' : 'skipped'}, write watch tests ${writeWatch ? 'enabled' : 'skipped'}`
|
||||
).write();
|
||||
|
||||
relinker:
|
||||
@@ -117,13 +275,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
compiler: g++
|
||||
- os: windows-latest
|
||||
compiler: g++
|
||||
- os: macos-latest
|
||||
compiler: clang++
|
||||
include: ${{ fromJSON(needs.eligibility.outputs.relinker-hosts) }}
|
||||
env:
|
||||
WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7
|
||||
WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip
|
||||
@@ -142,9 +294,15 @@ jobs:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head"
|
||||
if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
git fetch --no-tags --unshallow origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
- if: runner.os == 'Windows'
|
||||
id: toolchain
|
||||
uses: actions/cache@v6
|
||||
@@ -157,11 +315,44 @@ jobs:
|
||||
7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null
|
||||
- if: runner.os == 'Windows'
|
||||
run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
|
||||
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON
|
||||
- if: ${{ matrix.architectures == 'x86_64' }}
|
||||
run: sudo softwareupdate --install-rosetta --agree-to-license
|
||||
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON -DCMAKE_OSX_ARCHITECTURES=${{ matrix.architectures }}
|
||||
- run: cmake --build build-relinker --parallel 3
|
||||
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120
|
||||
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4
|
||||
|
||||
relinker-asan:
|
||||
needs: eligibility
|
||||
if: ${{ needs.eligibility.outputs.relinker == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
git fetch --no-tags --unshallow origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
- run: cmake -S . -B build-relinker-asan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ -DANYPS5_RELINKER_ONLY=ON -DANYPS5_ENABLE_ASAN=ON -DBUILD_TESTING=ON
|
||||
- run: cmake --build build-relinker-asan --parallel 4
|
||||
- run: ctest --test-dir build-relinker-asan --output-on-failure --timeout 120 --label-regex '^tls_function_coverage$' --no-tests=error
|
||||
|
||||
register-context:
|
||||
needs: eligibility
|
||||
if: ${{ needs.eligibility.outputs.register-context == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -169,23 +360,11 @@ jobs:
|
||||
configuration: [Debug, Release]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }}
|
||||
- run: cmake --build build/register-context --parallel
|
||||
- run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error
|
||||
|
||||
linux:
|
||||
needs: eligibility
|
||||
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
shell: bash
|
||||
@@ -193,27 +372,181 @@ jobs:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head"
|
||||
if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi
|
||||
git submodule update --init --recursive
|
||||
- uses: hendrikmuhs/ccache-action@v1.2.24
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
git fetch --no-tags --unshallow origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
- run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }}
|
||||
- run: cmake --build build/register-context --parallel
|
||||
- run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error
|
||||
|
||||
write-watch:
|
||||
needs: eligibility
|
||||
if: ${{ needs.eligibility.outputs.write-watch == 'true' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-15-intel, macos-15]
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
key: linux
|
||||
save: false
|
||||
evict-old-files: job
|
||||
- name: Install lavapipe for the GPU execution tests
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers
|
||||
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
shell: bash
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
git fetch --no-tags --unshallow origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
- run: cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release
|
||||
- run: cmake --build build/write-watch --parallel
|
||||
- run: ctest --test-dir build/write-watch --output-on-failure --repeat until-fail:3 --no-tests=error
|
||||
|
||||
linux:
|
||||
needs: eligibility
|
||||
if: ${{ (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.linux)) && needs.eligibility.outputs.full == 'true' }}
|
||||
runs-on: [self-hosted, linux, x64, anyps5]
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
CCACHE_DIR: /var/cache/anyps5/ccache
|
||||
CCACHE_MAXSIZE: 30G
|
||||
CCACHE_COMPILERCHECK: content
|
||||
MESA_SHADER_CACHE_DIR: /var/cache/anyps5/mesa
|
||||
MESA_SHADER_CACHE_MAX_SIZE: 4G
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
shell: bash
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
fetch_options=()
|
||||
if [ "$(git rev-parse --is-shallow-repository)" = true ]; then fetch_options+=(--unshallow); fi
|
||||
git fetch --no-tags "${fetch_options[@]}" origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
git submodule update --init --recursive
|
||||
- name: Prepare persistent caches
|
||||
run: |
|
||||
mkdir -p "$CCACHE_DIR" "$MESA_SHADER_CACHE_DIR"
|
||||
test -w "$CCACHE_DIR" && test -w "$MESA_SHADER_CACHE_DIR"
|
||||
ccache --zero-stats
|
||||
ccache --show-config
|
||||
dpkg-query -W mesa-vulkan-drivers
|
||||
grep -m1 '^model name' /proc/cpuinfo
|
||||
- name: Install the wild linker
|
||||
shell: bash
|
||||
env:
|
||||
WILD_VERSION: 0.10.0
|
||||
WILD_SHA256: 641265506a7c06cfb03181b8916ab663ec8407855db6d4db7f8450667d105283
|
||||
run: |
|
||||
tools=$RUNNER_TOOL_CACHE/wild
|
||||
mkdir -p "$tools" 2>/dev/null && test -w "$tools" || { tools=$RUNNER_TEMP/wild; mkdir -p "$tools"; }
|
||||
dir=$tools/$WILD_VERSION
|
||||
tmp=
|
||||
probe=$(mktemp -d "$RUNNER_TEMP/wild-probe.XXXXXX")
|
||||
trap 'rm -rf "$tmp" "$probe"' EXIT
|
||||
if [ ! -x "$dir/bin/ld" ]; then
|
||||
tmp=$(mktemp -d "$tools/.wild.XXXXXX")
|
||||
name=wild-linker-$WILD_VERSION-x86_64-unknown-linux-gnu
|
||||
curl -fsSL --retry 3 -o "$tmp/$name.tar.gz" "https://github.com/davidlattimore/wild/releases/download/$WILD_VERSION/$name.tar.gz"
|
||||
echo "$WILD_SHA256 $tmp/$name.tar.gz" | sha256sum -c -
|
||||
tar -xzf "$tmp/$name.tar.gz" -C "$tmp"
|
||||
mkdir -p "$tmp/out/bin"
|
||||
mv "$tmp/$name/wild" "$tmp/out/bin/wild"
|
||||
ln -s wild "$tmp/out/bin/ld"
|
||||
mv -T "$tmp/out" "$dir" 2>/dev/null || test -x "$dir/bin/ld"
|
||||
fi
|
||||
echo 'int main(void) { return 0; }' > "$probe/main.c"
|
||||
gcc "-B$dir/bin" "$probe/main.c" -o "$probe/main"
|
||||
readelf -p .comment "$probe/main" | grep -F "Linker: Wild $WILD_VERSION"
|
||||
echo "LINKER_FLAGS=-B$dir/bin" >> "$GITHUB_ENV"
|
||||
- name: Size parallelism to what the shared host has free
|
||||
run: |
|
||||
cpus=$(nproc)
|
||||
busy=$(awk '{ printf "%d", $1 + 0.5 }' /proc/loadavg)
|
||||
mem_mb=$(awk '/^MemTotal:/ { print int($2 / 1024) }' /proc/meminfo)
|
||||
slots=$(( (mem_mb - 2048) / 1024 ))
|
||||
slots=$(( slots < 1 ? 1 : slots ))
|
||||
tests=$(( cpus - busy ))
|
||||
tests=$(( tests < 1 ? 1 : tests ))
|
||||
slot_dir=$CCACHE_DIR/compile-slots
|
||||
mkdir -p -m 1777 "$slot_dir"
|
||||
test -w "$slot_dir"
|
||||
echo "cpus=$cpus load=$busy mem_total_mb=$mem_mb -> build jobs $cpus, cold compiles $slots per host, tests $tests"
|
||||
echo "CMAKE_BUILD_PARALLEL_LEVEL=$cpus" >> "$GITHUB_ENV"
|
||||
echo "CTEST_PARALLEL_LEVEL=$tests" >> "$GITHUB_ENV"
|
||||
echo "CCACHE_PREFIX=$GITHUB_WORKSPACE/tools/compile_slot.sh" >> "$GITHUB_ENV"
|
||||
echo "ANYPS5_COMPILE_SLOT_DIR=$slot_dir" >> "$GITHUB_ENV"
|
||||
echo "ANYPS5_COMPILE_SLOTS=$slots" >> "$GITHUB_ENV"
|
||||
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS"
|
||||
- name: Check that every test executable is registered with ctest
|
||||
run: python3 tools/check_test_registration.py build
|
||||
- run: cmake --build build
|
||||
- run: cmake --build build --target libs
|
||||
- run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
|
||||
- if: ${{ inputs.select }}
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: test-graph.json
|
||||
key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }}
|
||||
restore-keys: test-graph-${{ runner.os }}-
|
||||
- name: Select the tests the change reaches
|
||||
if: ${{ inputs.select }}
|
||||
env:
|
||||
BASE: ${{ needs.eligibility.outputs.sha }}
|
||||
run: |
|
||||
git diff --name-only "$BASE" HEAD > changed.txt
|
||||
python3 tools/select_tests.py select build test-graph.json changed.txt
|
||||
- id: build
|
||||
run: |
|
||||
if [ -f build/select.ninja ]; then ninja -C build -f select.ninja -j "$CMAKE_BUILD_PARALLEL_LEVEL" -l "$(nproc)" selected; else cmake --build build -- -l "$(nproc)"; fi
|
||||
- id: libs
|
||||
run: cmake --build build --target libs -- -l "$(nproc)"
|
||||
- run: python3 tools/select_tests.py ctest build --test-load "$(nproc)" --output-on-failure --timeout 120 --output-log ctest.log --output-junit ctest.xml
|
||||
env:
|
||||
ANYPS5_REQUIRE_VULKAN: 1
|
||||
GALLIUM_OVERRIDE_CPU_CAPS: avx
|
||||
- run: python3 tools/check_skips.py build/ctest.xml
|
||||
- name: Write the test graph of main
|
||||
if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
run: python3 tools/select_tests.py dump build test-graph.json
|
||||
- if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }}
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: test-graph.json
|
||||
key: test-graph-${{ runner.os }}-${{ github.sha }}
|
||||
- name: Report compiler cache statistics
|
||||
if: ${{ !cancelled() }}
|
||||
run: ccache --show-stats
|
||||
- name: Report tests slower than 30 s
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
@@ -230,7 +563,6 @@ jobs:
|
||||
env:
|
||||
WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7
|
||||
WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip
|
||||
SSL_CERT_FILE: C:\Program Files\Git\mingw64\etc\ssl\certs\ca-bundle.crt
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
@@ -239,7 +571,6 @@ jobs:
|
||||
with:
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
submodules: true
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
shell: bash
|
||||
@@ -247,10 +578,44 @@ jobs:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head"
|
||||
if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi
|
||||
git submodule update --init --recursive
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
git fetch --no-tags --unshallow origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
- id: submodules
|
||||
name: Key the submodules on their commits
|
||||
run: |
|
||||
echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
|
||||
echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT"
|
||||
- id: submodule-cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/submodule-gitdirs.tar
|
||||
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
|
||||
- name: Check out the submodules
|
||||
run: |
|
||||
if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi
|
||||
git submodule update --init --recursive --depth 1 --jobs 8
|
||||
- if: steps.submodule-cache.outputs.cache-hit != 'true'
|
||||
run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules
|
||||
- if: steps.submodule-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ${{ runner.temp }}/submodule-gitdirs.tar
|
||||
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
|
||||
- name: Point the WinLibs CMake at the CA bundle of Git for Windows
|
||||
run: |
|
||||
for prefix in ucrt64 mingw64; do
|
||||
bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt"
|
||||
if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi
|
||||
done
|
||||
echo "::error::No CA bundle found in Git for Windows"; exit 1
|
||||
- id: toolchain
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
@@ -263,21 +628,74 @@ jobs:
|
||||
- run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
|
||||
- uses: hendrikmuhs/ccache-action@v1.2.24
|
||||
with:
|
||||
key: windows
|
||||
save: false
|
||||
key: ${{ github.event_name == 'workflow_dispatch' && 'dispatch-windows' || 'windows' }}
|
||||
restore-keys: ${{ github.event_name == 'workflow_dispatch' && 'windows' || '' }}
|
||||
save: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
|
||||
evict-old-files: job
|
||||
- id: ffmpeg-cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: build/externals
|
||||
key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }}
|
||||
- id: seed-cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: configure-seed.cmake
|
||||
key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', '.github/workflows/build.yml') }}
|
||||
- shell: cmd
|
||||
run: cmake -S . -B build %CONFIGURE%
|
||||
run: if exist configure-seed.cmake (cmake -C configure-seed.cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") else (cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown")
|
||||
- if: steps.seed-cache.outputs.cache-hit != 'true'
|
||||
shell: cmd
|
||||
run: python tools/configure_seed.py build/CMakeCache.txt configure-seed.cmake
|
||||
- if: steps.seed-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: configure-seed.cmake
|
||||
key: ${{ steps.seed-cache.outputs.cache-primary-key }}
|
||||
- if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: build/externals
|
||||
key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }}
|
||||
- name: Check that every test executable is registered with ctest
|
||||
shell: cmd
|
||||
run: python tools/check_test_registration.py build
|
||||
- shell: cmd
|
||||
run: cmake --build build
|
||||
- shell: cmd
|
||||
- if: ${{ inputs.select }}
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: test-graph.json
|
||||
key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }}
|
||||
restore-keys: test-graph-${{ runner.os }}-
|
||||
- name: Select the tests the change reaches
|
||||
if: ${{ inputs.select }}
|
||||
env:
|
||||
BASE: ${{ needs.eligibility.outputs.sha }}
|
||||
shell: cmd
|
||||
run: |
|
||||
git diff --name-only %BASE% HEAD > changed.txt
|
||||
python tools/select_tests.py select build test-graph.json changed.txt
|
||||
- id: build
|
||||
shell: cmd
|
||||
run: if exist build\select.ninja (ninja -C build -f select.ninja selected) else (cmake --build build)
|
||||
- id: libs
|
||||
shell: cmd
|
||||
run: cmake --build build --target libs
|
||||
- shell: cmd
|
||||
run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
|
||||
run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
|
||||
- run: python tools/check_skips.py build/ctest.xml --job windows
|
||||
- name: Write the test graph of main
|
||||
if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
shell: cmd
|
||||
run: python tools/select_tests.py dump build test-graph.json
|
||||
- if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }}
|
||||
continue-on-error: true
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: test-graph.json
|
||||
key: test-graph-${{ runner.os }}-${{ github.sha }}
|
||||
- name: Report tests slower than 30 s
|
||||
if: ${{ !cancelled() }}
|
||||
shell: bash
|
||||
@@ -285,3 +703,4 @@ jobs:
|
||||
[ -f ctest.log ] || exit 0
|
||||
awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t }
|
||||
END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log
|
||||
|
||||
@@ -9,6 +9,10 @@ on:
|
||||
|
||||
permissions: {}
|
||||
|
||||
concurrency:
|
||||
group: conflict-label
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -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"
|
||||
@@ -24,10 +24,13 @@ jobs:
|
||||
force_label() { gh label create "$1" --color "$2" --description "$3" --force; }
|
||||
|
||||
force_label 'needs rebase' b60205 'Conflicts with main, rebase needed'
|
||||
force_label 'intake paused' d93f0b 'Closed while PR intake is limited to authors with at least 20 PRs merged into main'
|
||||
force_label 'open PR limit' d93f0b 'Closed over the open PR limit: 1 without merged PRs, 20 with at least 1, none from 20'
|
||||
force_label ci 5319e7 '.github'
|
||||
force_label agc 1d76db 'libSceAgc*'
|
||||
force_label shader 1d76db 'core/shader'
|
||||
force_label 'unblocks title' 0e8a16 'Names a title ID on the Unblocks line'
|
||||
force_label perf fef2c0 'Performance change'
|
||||
force_label 'perf numbers missing' e99695 'perf pull request without a before and after summary line'
|
||||
|
||||
label relinker 5319e7 'core/relinker'
|
||||
label libc fbca04 'libc'
|
||||
@@ -35,3 +38,5 @@ jobs:
|
||||
label prx c5def5 'Other PRX libraries'
|
||||
label build bfd4f2 'CMake, submodules, 3rdparty'
|
||||
label docs 0075ca 'Documentation'
|
||||
label 'fix error' d73a4a 'Closes an issue (Fixes #N)'
|
||||
label stale ededed 'Conflicting with main and no activity for 7 days'
|
||||
@@ -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
|
||||
@@ -0,0 +1,56 @@
|
||||
name: Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/pages.yml
|
||||
- CMakeLists.txt
|
||||
- core/relinker/**
|
||||
- demo/web/**
|
||||
- tools/build_web.py
|
||||
- tools/package_release.py
|
||||
- tools/nid_names.py
|
||||
- tools/progress.py
|
||||
- tools/progress_history.py
|
||||
- tools/rdna_isa.txt
|
||||
- tools/test_web.py
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_run:
|
||||
workflows: [Release]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
pages:
|
||||
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
- name: Download matching release converter and libraries
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: python3 tools/build_web.py --build build-wasm --release
|
||||
- run: python3 tools/progress.py build-wasm/web
|
||||
- run: python3 tools/progress_history.py build-wasm/web
|
||||
- uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: build-wasm/web
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -3,7 +3,7 @@ name: PR intake
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [main]
|
||||
types: [opened]
|
||||
types: [opened, reopened]
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -27,7 +27,27 @@ jobs:
|
||||
if (pr.state !== 'open' || pr.base.ref !== 'main') {
|
||||
return;
|
||||
}
|
||||
const minimumMergedPRs = 20;
|
||||
if (context.payload.action === 'reopened') {
|
||||
let maintainer = false;
|
||||
try {
|
||||
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner, repo, username: context.payload.sender.login
|
||||
});
|
||||
maintainer = ['admin', 'maintain', 'write'].includes(access.role_name ?? access.permission);
|
||||
} catch (error) {
|
||||
core.warning(`Could not check the permission of ${context.payload.sender.login}, so the open PR limit applies: ${error.message}`);
|
||||
}
|
||||
if (maintainer) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, name: 'open PR limit' });
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
core.warning(`Could not remove the open PR limit label: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `repo:${owner}/${repo} is:pr is:merged base:main author:${pr.user.login}`,
|
||||
per_page: 1
|
||||
@@ -35,10 +55,23 @@ jobs:
|
||||
if (mergedPRs.incomplete_results || !Number.isSafeInteger(mergedPRs.total_count) || mergedPRs.total_count < 0) {
|
||||
throw new Error('Could not reliably count merged pull requests');
|
||||
}
|
||||
if (mergedPRs.total_count >= minimumMergedPRs) {
|
||||
const merged = mergedPRs.total_count;
|
||||
if (merged >= 20) {
|
||||
return;
|
||||
}
|
||||
const name = 'intake paused';
|
||||
const allowedOpen = merged >= 1 ? 20 : 1;
|
||||
const openPRs = await github.paginate(github.rest.pulls.list, {
|
||||
owner, repo, state: 'open', base: 'main', per_page: 100
|
||||
});
|
||||
const reopened = context.payload.action === 'reopened';
|
||||
const open = new Set(openPRs
|
||||
.filter((p) => p.user.login === pr.user.login && (reopened || p.number < pull_number))
|
||||
.map((p) => p.number));
|
||||
open.add(pull_number);
|
||||
if (open.size <= allowedOpen) {
|
||||
return;
|
||||
}
|
||||
const name = 'open PR limit';
|
||||
try {
|
||||
await github.rest.issues.getLabel({ owner, repo, name });
|
||||
} catch (error) {
|
||||
@@ -48,7 +81,7 @@ jobs:
|
||||
try {
|
||||
await github.rest.issues.createLabel({
|
||||
owner, repo, name, color: 'd93f0b',
|
||||
description: 'Closed while PR intake is limited to authors with at least 20 PRs merged into main'
|
||||
description: 'Closed over the open PR limit: 1 without merged PRs, 20 with at least 1, none from 20'
|
||||
});
|
||||
} catch (createError) {
|
||||
if (createError.status !== 422) {
|
||||
@@ -58,7 +91,7 @@ jobs:
|
||||
}
|
||||
}
|
||||
await github.rest.issues.addLabels({ owner, repo, issue_number: pull_number, labels: [name] });
|
||||
const body = 'Thank you for your interest in the project! We are sorry, but we cannot accept more PRs while we work through the existing backlog. This PR has been automatically closed and labeled "intake paused". This is not a judgment of the quality of your changes.';
|
||||
const body = 'Thank you for your contribution! Authors with no pull requests merged into `main` can have 1 open pull request at a time, authors with at least 1 merged can have 20, and from 20 merged there is no limit. This PR has been automatically closed and labeled "open PR limit" because that limit was reached. This is not a judgment of the quality of your changes: reopen it once one of your open pull requests is merged or closed.';
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner, repo, issue_number: pull_number, per_page: 100
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -22,20 +22,36 @@ jobs:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/overlap-state.json
|
||||
key: pr-overlap-${{ github.run_id }}
|
||||
restore-keys: pr-overlap-
|
||||
- env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_REPO: ${{ github.repository }}
|
||||
run: |
|
||||
out=$RUNNER_TEMP/overlap
|
||||
state=$RUNNER_TEMP/overlap-state.json
|
||||
python3 tools/pr_overlap.py --base "${{ github.event.repository.default_branch }}" --out "$out"
|
||||
[ -s "$state" ] || echo '{}' > "$state"
|
||||
record() { jq --arg pr "$1" --arg h "$2" '.[$pr] = $h' "$state" > "$state.new" && mv "$state.new" "$state"; }
|
||||
shopt -s nullglob
|
||||
marker='<!-- 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=$(unpinned < "$file" | sha256sum | cut -d' ' -f1)
|
||||
prev=
|
||||
[ "$(jq -r --arg pr "$pr" '.[$pr] // empty' "$state")" = "$hash" ] && continue
|
||||
prev=$pr
|
||||
comment=$(gh api "repos/$GH_REPO/issues/$pr/comments" --paginate \
|
||||
--jq ".[] | select(.user.login == \"github-actions[bot]\" and (.body | startswith(\"$marker\"))) | [.id, .body] | @json" | head -n1)
|
||||
id=$(jq -r '.[0] // empty' <<< "${comment:-null}")
|
||||
if [ -s "$file" ]; then
|
||||
[ "$(jq -r '.[1]' <<< "${comment:-null}")" = "$(cat "$file")" ] && continue
|
||||
[ "$(jq -r '.[1]' <<< "${comment:-null}" | unpinned)" = "$(unpinned < "$file")" ] && continue
|
||||
if [ -n "$id" ]; then
|
||||
gh api -X PATCH "repos/$GH_REPO/issues/comments/$id" -F body=@"$file" > /dev/null
|
||||
else
|
||||
@@ -45,3 +61,11 @@ jobs:
|
||||
gh api -X DELETE "repos/$GH_REPO/issues/comments/$id"
|
||||
fi
|
||||
done
|
||||
[ -z "$prev" ] || record "$prev" "$hash"
|
||||
reports=$(for file in "$out"/*.md; do basename "$file" .md; done | jq -R . | jq -sc .)
|
||||
jq -c --argjson keep "$reports" 'with_entries(select(.key | IN($keep[])))' "$state" > "$state.new" && mv "$state.new" "$state"
|
||||
- if: always()
|
||||
uses: actions/cache/save@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/overlap-state.json
|
||||
key: pr-overlap-${{ github.run_id }}
|
||||
@@ -1,30 +0,0 @@
|
||||
name: Progress
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
progress:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- run: python3 tools/progress.py out
|
||||
- uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: out
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -41,7 +41,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: true
|
||||
ref: ${{ github.sha }}
|
||||
submodules: recursive
|
||||
- name: Point the WinLibs CMake at the CA bundle of Git for Windows
|
||||
if: matrix.platform == 'windows'
|
||||
shell: bash
|
||||
run: |
|
||||
for prefix in ucrt64 mingw64; do
|
||||
bundle="C:/Program Files/Git/$prefix/etc/ssl/certs/ca-bundle.crt"
|
||||
if [ -f "$bundle" ]; then echo "SSL_CERT_FILE=$bundle" >> "$GITHUB_ENV"; exit 0; fi
|
||||
done
|
||||
echo "::error::No CA bundle found in Git for Windows"; exit 1
|
||||
- id: toolchain
|
||||
if: matrix.platform == 'windows'
|
||||
uses: actions/cache@v6
|
||||
@@ -58,6 +68,9 @@ jobs:
|
||||
with:
|
||||
key: ${{ matrix.platform }}
|
||||
save: false
|
||||
- name: Install lavapipe for the GPU execution tests and the audio development files
|
||||
if: matrix.platform == 'linux'
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers libpipewire-0.3-dev libpulse-dev libasound2-dev libxrandr-dev libwayland-dev libxkbcommon-dev libegl-dev libdecor-0-dev
|
||||
- name: Configure Linux
|
||||
if: matrix.platform == 'linux'
|
||||
run: cmake -S . -B build $CONFIGURE
|
||||
@@ -74,7 +87,10 @@ jobs:
|
||||
run: cmake --build build
|
||||
- name: Run tests (Linux)
|
||||
if: matrix.platform == 'linux'
|
||||
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error
|
||||
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error -E '^precise_wait$'
|
||||
env:
|
||||
ANYPS5_REQUIRE_VULKAN: 1
|
||||
GALLIUM_OVERRIDE_CPU_CAPS: avx
|
||||
- name: Run tests (Windows)
|
||||
if: matrix.platform == 'windows'
|
||||
shell: cmd
|
||||
@@ -95,9 +111,38 @@ jobs:
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
|
||||
web:
|
||||
name: Build and test web converter
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
- name: Build web converter
|
||||
shell: bash
|
||||
run: |
|
||||
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk"
|
||||
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.3
|
||||
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3
|
||||
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
|
||||
emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build build-wasm --target relinker_web --parallel 2
|
||||
- name: Test web converter
|
||||
run: python3 tools/test_web.py --build build-wasm
|
||||
- name: Package web converter
|
||||
shell: bash
|
||||
run: python3 tools/package_release.py --platform web --build build-wasm --output dist --version "$RELEASE_TAG" --revision "$(git rev-parse HEAD)"
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: release-web
|
||||
path: dist/*
|
||||
if-no-files-found: error
|
||||
compression-level: 0
|
||||
|
||||
publish:
|
||||
name: Publish release
|
||||
needs: build
|
||||
needs: [build, web]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
@@ -107,6 +152,8 @@ jobs:
|
||||
GH_REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: release-*
|
||||
@@ -115,12 +162,17 @@ jobs:
|
||||
- name: Check release files and collect documentation
|
||||
shell: bash
|
||||
run: |
|
||||
for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG"; do
|
||||
for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG" "web-$RELEASE_TAG.zip"; do
|
||||
test -s "dist/$file"
|
||||
done
|
||||
python tools/package_release.py --docs docs/user --output dist
|
||||
chmod +x "dist/relinker-$RELEASE_TAG"
|
||||
- name: Prepare release notes
|
||||
shell: bash
|
||||
run: |
|
||||
gh api --method POST "repos/$GH_REPO/releases/generate-notes" -f tag_name="$RELEASE_TAG" > "$RUNNER_TEMP/release-notes.json"
|
||||
python tools/prepare_release_notes.py --input "$RUNNER_TEMP/release-notes.json" --body "$RUNNER_TEMP/release-body.md" --full dist/release-notes.md
|
||||
- name: Create draft and upload files
|
||||
run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --generate-notes
|
||||
run: gh release create "$RELEASE_TAG" dist/* --verify-tag --draft --title "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/release-body.md"
|
||||
- name: Publish complete release
|
||||
run: gh release edit "$RELEASE_TAG" --draft=false
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Stale conflicts
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
warn:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr list -R "$GITHUB_REPOSITORY" --label stale -L 1000 --json number,labels \
|
||||
--jq '.[] | select(all(.labels[]; .name != "needs rebase")) | .number' |
|
||||
while read -r number; do
|
||||
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/stale" --silent
|
||||
done
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
only-pr-labels: needs rebase
|
||||
days-before-issue-stale: -1
|
||||
days-before-issue-close: -1
|
||||
days-before-pr-stale: 7
|
||||
days-before-pr-close: -1
|
||||
stale-pr-label: stale
|
||||
stale-pr-message: 'This pull request has conflicted with `main` and had no activity for 7 days. Rebase it on current `main` so it can be reviewed and merged. It will not be closed automatically.'
|
||||
remove-pr-stale-when-updated: true
|
||||
exempt-draft-pr: true
|
||||
operations-per-run: 200
|
||||
@@ -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
|
||||
+21
@@ -28,6 +28,27 @@
|
||||
[submodule "3rdparty/stb"]
|
||||
path = 3rdparty/stb
|
||||
url = https://github.com/nothings/stb.git
|
||||
[submodule "3rdparty/tracy"]
|
||||
path = 3rdparty/tracy
|
||||
url = https://github.com/wolfpld/tracy.git
|
||||
[submodule "3rdparty/mimalloc"]
|
||||
path = 3rdparty/mimalloc
|
||||
url = https://github.com/microsoft/mimalloc.git
|
||||
[submodule "3rdparty/libjpeg-turbo"]
|
||||
path = 3rdparty/libjpeg-turbo
|
||||
url = https://github.com/libjpeg-turbo/libjpeg-turbo.git
|
||||
[submodule "3rdparty/libusb-cmake"]
|
||||
path = 3rdparty/libusb-cmake
|
||||
url = https://github.com/libusb/libusb-cmake.git
|
||||
[submodule "3rdparty/mbedtls"]
|
||||
path = 3rdparty/mbedtls
|
||||
url = https://github.com/Mbed-TLS/mbedtls.git
|
||||
[submodule "3rdparty/zstd"]
|
||||
path = 3rdparty/zstd
|
||||
url = https://github.com/facebook/zstd.git
|
||||
[submodule "3rdparty/ZArchive"]
|
||||
path = 3rdparty/ZArchive
|
||||
url = https://github.com/Exzap/ZArchive.git
|
||||
[submodule "3rdparty/vgmstream"]
|
||||
path = 3rdparty/vgmstream
|
||||
url = https://github.com/vgmstream/vgmstream.git
|
||||
+1
Submodule 3rdparty/ZArchive added at 965b66c8d6.
+1
Submodule 3rdparty/libusb-cmake added at bea6567b63.
+1
Submodule 3rdparty/mbedtls added at 068ff080b3.
+1
Submodule 3rdparty/mimalloc added at f8401befa6.
+1
Submodule 3rdparty/tracy added at 30997d5ca6.
+1
Submodule 3rdparty/vgmstream added at 7c520e8f9b.
+1
Submodule 3rdparty/zstd added at f8745da6ff.
@@ -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).
|
||||
+273
-13
@@ -1,21 +1,52 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
if(CMAKE_HOST_APPLE AND NOT ANYPS5_RELINKER_ONLY)
|
||||
set(CMAKE_APPLE_SILICON_PROCESSOR x86_64 CACHE STRING "Host processor CMake reports on Apple silicon")
|
||||
set(CMAKE_OSX_ARCHITECTURES x86_64 CACHE STRING "Architectures built on macOS")
|
||||
endif()
|
||||
project(Relinker CXX)
|
||||
|
||||
set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests")
|
||||
set(ANYPS5_WINDOWS_TEST_EXIT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WindowsTestExit.cpp")
|
||||
|
||||
function(add_test_executable target)
|
||||
add_executable(${target} ${ARGN})
|
||||
set_target_properties(${target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>")
|
||||
if(MINGW)
|
||||
target_sources(${target} PRIVATE "${ANYPS5_WINDOWS_TEST_EXIT}")
|
||||
target_link_options(${target} PRIVATE LINKER:--wrap=main)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(add_asan_test_options test options)
|
||||
if(ANYPS5_ENABLE_ASAN)
|
||||
set_property(TEST ${test} APPEND PROPERTY ENVIRONMENT_MODIFICATION "ASAN_OPTIONS=string_append::${options}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
option(BUILD_TESTING "Build and register relinker tests" OFF)
|
||||
option(ANYPS5_RELINKER_ONLY "Build the relinker without system libraries or third-party dependencies" OFF)
|
||||
option(ANYPS5_BUILD_WASM "Build the browser relinker" OFF)
|
||||
if(ANYPS5_BUILD_WASM)
|
||||
if(NOT EMSCRIPTEN)
|
||||
message(FATAL_ERROR "ANYPS5_BUILD_WASM requires emcmake and Emscripten")
|
||||
endif()
|
||||
if(BUILD_TESTING)
|
||||
message(FATAL_ERROR "Use the native build for C++ tests and the browser tests for WASM")
|
||||
endif()
|
||||
set(ANYPS5_RELINKER_CLI OFF CACHE BOOL "Build the native relinker command line" FORCE)
|
||||
add_subdirectory(core/relinker)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
enable_testing()
|
||||
if(MINGW)
|
||||
get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||
foreach(runtimeName IN ITEMS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll)
|
||||
if(NOT EXISTS "${testCompilerDirectory}/${runtimeName}")
|
||||
if(ANYPS5_RELINKER_ONLY)
|
||||
continue()
|
||||
endif()
|
||||
message(FATAL_ERROR "Cannot locate MinGW runtime: ${runtimeName}")
|
||||
endif()
|
||||
file(COPY "${testCompilerDirectory}/${runtimeName}" DESTINATION "${ANYPS5_TEST_OUTPUT_DIRECTORY}")
|
||||
@@ -24,37 +55,212 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
|
||||
option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF)
|
||||
option(ANYPS5_ENABLE_ASAN "Build Linux targets with AddressSanitizer and use an ASan-compatible guest arena range" OFF)
|
||||
option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF)
|
||||
option(APS5_ENABLE_TRACY "Build the Tracy profiler client and report performance timer scopes as Tracy zones" OFF)
|
||||
option(ANYPS5_USE_SYSTEM_DEPS "Resolve SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools from the system instead of the bundled submodules" OFF)
|
||||
add_compile_definitions(APS5_ENABLE_TIMING_LOG=$<BOOL:${APS5_ENABLE_TIMING_LOG}>)
|
||||
include(cmake/SystemDeps.cmake)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(NOT APPLE)
|
||||
include(CheckCXXSourceCompiles)
|
||||
function(anyps5_check_cxx_links_standard_library)
|
||||
set(CMAKE_TRY_COMPILE_TARGET_TYPE EXECUTABLE)
|
||||
check_cxx_source_compiles("#include <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")
|
||||
endif()
|
||||
if(NOT CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang)$")
|
||||
message(FATAL_ERROR "ANYPS5_ENABLE_ASAN requires GCC or Clang")
|
||||
endif()
|
||||
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
|
||||
add_link_options(-fsanitize=address)
|
||||
add_compile_definitions(ANYPS5_ENABLE_ASAN=1)
|
||||
elseif(NOT APPLE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++")
|
||||
endif()
|
||||
|
||||
if(APPLE AND NOT ANYPS5_RELINKER_ONLY)
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-fexperimental-library>)
|
||||
add_link_options(-fexperimental-library)
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(shader_cache_file_read_tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/CacheFileReadTests.cpp)
|
||||
target_include_directories(shader_cache_file_read_tests PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler)
|
||||
add_test(NAME shader_cache_file_read COMMAND shader_cache_file_read_tests)
|
||||
set_tests_properties(shader_cache_file_read PROPERTIES TIMEOUT 20)
|
||||
add_test_executable(request_serializer_tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/RequestSerializerTests.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp)
|
||||
target_include_directories(request_serializer_tests PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ControlFlow/include)
|
||||
add_test(NAME request_serializer COMMAND request_serializer_tests)
|
||||
set_tests_properties(request_serializer PROPERTIES TIMEOUT 20)
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
add_test_executable(shader_ir_use_rewrite_tests
|
||||
core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/GuestRegister.cpp)
|
||||
target_include_directories(shader_ir_use_rewrite_tests PRIVATE
|
||||
core/shader/recompiler/IntermediateRepresentation/include)
|
||||
add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests)
|
||||
set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20)
|
||||
add_test_executable(pad_command_file_tests core/libs/prx/libScePad/tests/InputCommandFile.cpp)
|
||||
target_include_directories(pad_command_file_tests PRIVATE core/libs)
|
||||
add_test(NAME pad_command_file COMMAND pad_command_file_tests)
|
||||
set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/ShaderCacheVersion.cmake ${CMAKE_COMMAND})
|
||||
set_tests_properties(shader_cache_native_paths PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME shader_cache_runtime_sources COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_shader_cache_runtime_sources.py
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler ${CMAKE_COMMAND})
|
||||
set_tests_properties(shader_cache_runtime_sources PROPERTIES TIMEOUT 180)
|
||||
add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py)
|
||||
add_test(NAME pr_overlap_dependencies COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_dependencies.py)
|
||||
add_test(NAME pr_overlap_tools COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap.py)
|
||||
add_test(NAME pr_labels COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_labels.py)
|
||||
add_test(NAME progress_history COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_history.py)
|
||||
add_test(NAME progress_report COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress.py)
|
||||
add_test(NAME progress_encoding COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_encoding.py)
|
||||
add_test(NAME progress_render_sliver COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_render_sliver.py)
|
||||
add_test(NAME progress_wrappers COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_wrappers.py)
|
||||
add_test(NAME progress_read_encoding COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress_read_encoding.py)
|
||||
add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
|
||||
add_test(NAME conventions_function_bodies COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py)
|
||||
add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py)
|
||||
foreach(shard RANGE 0 11)
|
||||
if(shard EQUAL 0)
|
||||
set(conventions_test_name conventions_function_bodies)
|
||||
else()
|
||||
set(conventions_test_name conventions_function_bodies_shard_${shard})
|
||||
endif()
|
||||
add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/12)
|
||||
set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30)
|
||||
endforeach()
|
||||
add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py)
|
||||
add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py)
|
||||
add_test(NAME conventions_git_errors COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_git.py)
|
||||
add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py)
|
||||
add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_unknown_nids.py)
|
||||
add_test(NAME conventions_debt_length COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_debt_length.py)
|
||||
add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py)
|
||||
add_test(NAME test_script_registration COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts)
|
||||
add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py)
|
||||
add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py)
|
||||
add_test(NAME select_tests COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py)
|
||||
add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py)
|
||||
add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedTests)
|
||||
add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedPathTests)
|
||||
add_test(NAME configure_seed_round_trips COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedRoundTripTests)
|
||||
add_test(NAME configure_seed_parsing COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ChecksParsingTests)
|
||||
add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
|
||||
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_debt_split.py)
|
||||
add_test(NAME stubgen_stdout_newlines COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen_stdout_newlines.py)
|
||||
add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py)
|
||||
add_test(NAME hardware_oracle_compare COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_compare_results.py)
|
||||
set_tests_properties(hardware_oracle_compare PROPERTIES TIMEOUT 120)
|
||||
add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py)
|
||||
add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py)
|
||||
add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py)
|
||||
add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py)
|
||||
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
|
||||
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py)
|
||||
add_test(NAME web_release COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_web_release.py)
|
||||
set_tests_properties(web_release PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME web_library_assets COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_web.py)
|
||||
set_tests_properties(web_library_assets PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py)
|
||||
set_tests_properties(pad_input PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py)
|
||||
set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
|
||||
add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_bom_shebang.py)
|
||||
set_tests_properties(conventions_bom_shebang PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME env_reference COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_env_reference.py)
|
||||
set_tests_properties(env_reference PROPERTIES TIMEOUT 20)
|
||||
add_test(NAME conventions_env_reference COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_env_reference.py)
|
||||
set_tests_properties(conventions_env_reference PROPERTIES TIMEOUT 60)
|
||||
add_test(NAME conventions_body_bom COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_body_bom.py)
|
||||
add_test(NAME conventions_blob_reads COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_blob_reads.py)
|
||||
add_test(NAME conventions_agent_files COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_agent_files.py)
|
||||
add_test(NAME conventions_pr_template COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_pr_template.py)
|
||||
add_test(NAME check_compile COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_compile.py)
|
||||
add_test(NAME performance_compare COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_performance_compare.py)
|
||||
set_tests_properties(performance_compare PROPERTIES TIMEOUT 20)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -63,13 +269,17 @@ if(ANYPS5_RELINKER_ONLY)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(APPLE AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64")
|
||||
message(FATAL_ERROR "The full build on macOS is x86-64 only; configure without CMAKE_OSX_ARCHITECTURES or set it to x86_64")
|
||||
endif()
|
||||
|
||||
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_STATIC ON CACHE BOOL "" FORCE)
|
||||
set(SDL_TEST OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_VIDEO ON CACHE BOOL "" FORCE)
|
||||
set(SDL_RENDER OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_X11 ON CACHE BOOL "" FORCE)
|
||||
set(SDL_WAYLAND OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_WAYLAND ON CACHE BOOL "" FORCE)
|
||||
set(SDL_OPENGL OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_OPENGLES OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_VULKAN ON CACHE BOOL "" FORCE)
|
||||
@@ -78,10 +288,28 @@ set(SDL_HAPTIC OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_HIDAPI ON CACHE BOOL "" FORCE)
|
||||
set(SDL_SENSOR OFF CACHE BOOL "" FORCE)
|
||||
set(SDL_STATIC_PIC ON CACHE BOOL "" FORCE)
|
||||
set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "")
|
||||
add_subdirectory(3rdparty/SDL2)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}")
|
||||
if(NOT ANYPS5_USE_SYSTEM_DEPS)
|
||||
set(ANYPS5_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
|
||||
set(CMAKE_EXE_LINKER_FLAGS "")
|
||||
add_subdirectory(3rdparty/SDL2)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${ANYPS5_EXE_LINKER_FLAGS}")
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
get_directory_property(ANYPS5_SDL_X11_XRANDR DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_X11_XRANDR)
|
||||
if(NOT ANYPS5_SDL_X11_XRANDR)
|
||||
message(FATAL_ERROR "SDL has no XRandR support, so X11 display refresh rates cannot be queried. "
|
||||
"Install libxrandr-dev (Debian, Ubuntu) or libXrandr-devel (Fedora), "
|
||||
"enable SDL_X11_XRANDR, then rerun configuration.")
|
||||
endif()
|
||||
get_directory_property(ANYPS5_SDL_PIPEWIRE DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PIPEWIRE)
|
||||
get_directory_property(ANYPS5_SDL_PULSEAUDIO DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_PULSEAUDIO)
|
||||
get_directory_property(ANYPS5_SDL_ALSA DIRECTORY 3rdparty/SDL2 DEFINITION HAVE_ALSA)
|
||||
if(NOT ANYPS5_SDL_PIPEWIRE AND NOT ANYPS5_SDL_PULSEAUDIO AND NOT ANYPS5_SDL_ALSA)
|
||||
message(FATAL_ERROR "SDL found no PipeWire, PulseAudio or ALSA development files, so audio output would be silent. "
|
||||
"Install libpipewire-0.3-dev, libpulse-dev and libasound2-dev (Debian, Ubuntu) "
|
||||
"or pipewire-devel, pulseaudio-libs-devel and alsa-lib-devel (Fedora), then rerun configuration.")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
set(ARCHITECTURE x86_64)
|
||||
add_subdirectory(3rdparty/ffmpeg-core)
|
||||
if(MINGW)
|
||||
@@ -90,18 +318,50 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
target_link_options(ffmpeg INTERFACE LINKER:--exclude-libs,ALL)
|
||||
endif()
|
||||
|
||||
if(APS5_ENABLE_TRACY)
|
||||
include(cmake/Tracy.cmake)
|
||||
endif()
|
||||
|
||||
set(FT_DISABLE_ZLIB ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_BZIP2 ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_PNG ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "" FORCE)
|
||||
set(FT_DISABLE_BROTLI ON CACHE BOOL "" FORCE)
|
||||
set(SKIP_INSTALL_ALL ON CACHE BOOL "" FORCE)
|
||||
add_subdirectory(3rdparty/freetype EXCLUDE_FROM_ALL)
|
||||
set_target_properties(freetype PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
if(NOT ANYPS5_USE_SYSTEM_DEPS)
|
||||
add_subdirectory(3rdparty/freetype EXCLUDE_FROM_ALL)
|
||||
set_target_properties(freetype PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
if(WIN32)
|
||||
get_target_property(freetypeSources freetype SOURCES)
|
||||
list(FILTER freetypeSources EXCLUDE REGEX "ftver\\.rc$")
|
||||
set_target_properties(freetype PROPERTIES SOURCES "${freetypeSources}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(LIBUSB_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(LIBUSB_INSTALL_TARGETS OFF CACHE BOOL "" FORCE)
|
||||
set(LIBUSB_BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
||||
set(LIBUSB_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(LIBUSB_ENABLE_UDEV OFF CACHE BOOL "" FORCE)
|
||||
add_subdirectory(3rdparty/libusb-cmake EXCLUDE_FROM_ALL)
|
||||
set_target_properties(usb-1.0 PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
include(cmake/ZArchive.cmake)
|
||||
if(WIN32)
|
||||
get_target_property(freetypeSources freetype SOURCES)
|
||||
list(FILTER freetypeSources EXCLUDE REGEX "ftver\\.rc$")
|
||||
set_target_properties(freetype PROPERTIES SOURCES "${freetypeSources}")
|
||||
set(MI_BUILD_SHARED ON CACHE BOOL "" FORCE)
|
||||
set(MI_BUILD_STATIC OFF CACHE BOOL "" FORCE)
|
||||
set(MI_BUILD_OBJECT OFF CACHE BOOL "" FORCE)
|
||||
set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(MI_OVERRIDE ON CACHE BOOL "" FORCE)
|
||||
set(MI_WIN_REDIRECT ON CACHE BOOL "" FORCE)
|
||||
set(MI_FREE_IS_CHECKED SHARED CACHE STRING "" FORCE)
|
||||
add_subdirectory(3rdparty/mimalloc EXCLUDE_FROM_ALL)
|
||||
if(MINGW)
|
||||
target_compile_definitions(mimalloc PRIVATE MI_MINGW_UCRT64=1)
|
||||
target_link_options(mimalloc PRIVATE -static
|
||||
LINKER:--wrap=malloc,--wrap=calloc,--wrap=realloc,--wrap=free,--wrap=strdup,--wrap=_strdup)
|
||||
target_sources(mimalloc PRIVATE ${CMAKE_SOURCE_DIR}/core/libs/allocator/RuntimeHeap.c)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_subdirectory(core/shader/recompiler)
|
||||
|
||||
+19
-7
@@ -5,13 +5,14 @@
|
||||
- Follow the [coding conventions](docs/dev/CONVENTIONS.md): naming, no comments except [technical debt](docs/dev/TechnicalDebt.md), Conventional Commits.
|
||||
- Every function either does exactly what it is supposed to or throws. Unimplemented exports call `NotImplemented_nid_no_patch(__func__)` (see [libSceAudioIn](core/libs/prx/libSceAudioIn/Export.cpp)).
|
||||
- Pull requests that add or change shader instruction semantics say where they come from: measured on hardware (which GPU and what was checked, e.g. with the [hardware oracle](docs/dev/HW_ORACLE.md)) or the exact source (ISA section, LLVM, ACO or Mesa file). Reviewers check the semantics on hardware. Cases the source doesn't settle throw, and behaviour not verified on hardware is recorded in [technical debt](docs/dev/TechnicalDebt.md).
|
||||
- A silent stub is allowed only when it unblocks a title and only affects the UI; add it to [silent stubs](docs/dev/TechnicalDebt.md#silent-stubs).
|
||||
- An export's signature or behaviour taken from a title's call sites or from a library's disassembly follows [reading titles and libraries](docs/dev/REVERSE_ENGINEERING.md): a call site settles the arguments, not what the library does.
|
||||
- A silent stub is allowed only when it unblocks a title and only affects the UI; add it under Silent stubs in its library's [technical debt](docs/dev/debt/) file.
|
||||
- Missing imports fail at startup with a clear error; don't replace them with fallbacks that keep running.
|
||||
- Implement the general behaviour of a function, not what one title happens to need.
|
||||
- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries (NpCppWebApi over NpWebApi2). The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx).
|
||||
- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries. The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). A host copy may still exist for a wrapped library that some dumps lack entirely, like libSceNpCppWebApi: the title's own module wins whenever one is shipped, so it is only ever consulted by titles that ship nothing.
|
||||
- Avoid non-standard extensions (`__attribute__`, etc.) where standard C++ is enough. Helper symbols that must not become NIDs use the `_nid_no_patch` or `_nid_no_patch_cut` suffix.
|
||||
- The relinker uses only the C++20 standard library.
|
||||
- Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system.
|
||||
- Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system. With `ANYPS5_USE_SYSTEM_DEPS=ON` the packages packaged by the distributions (SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools) come from the system instead; that resolution lives in [cmake/SystemDeps.cmake](cmake/SystemDeps.cmake) and the default build never reads the system.
|
||||
- Don't add tests that only check that a symbol is exported: a missing export already fails at startup.
|
||||
- Each test finishes in 30 s on the CI runners, Vulkan tests included (Linux runs them on lavapipe), counting the first run with no shader cache. ctest runs in parallel there, so a slow test competes for the cores and times out on some runs and not others. Split or shrink a test that gets close instead of raising its timeout. CI reports every test over 30 s as a warning on the pull request.
|
||||
|
||||
@@ -37,9 +38,11 @@ For pull requests, full Linux and Windows builds and tests run only when a colla
|
||||
gh workflow run build.yml --repo boykopovar/AnyPS5 --ref main --field pr=1234
|
||||
```
|
||||
|
||||
This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main`; full builds never run automatically for pull requests. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds.
|
||||
This manual run also builds and tests the relinker on Linux, Windows and macOS when the pull request changes a file the relinker-only build uses, regardless of the author's previous contributions. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main` and it changes a file the relinker-only build uses; full builds never run automatically for pull requests. Every job that runs automatically for pull requests runs only when the pull request changes its files: a `paths` filter on the `pull_request` trigger, or `needs: eligibility` with an `if` on the files output it sets in `build.yml`. The Conventions check fails a workflow change that adds a job without one. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds.
|
||||
|
||||
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [TechnicalDebt](docs/dev/TechnicalDebt.md), and only UTF-8 text files. Run it locally before pushing:
|
||||
With `--field full=false`, the manual run skips the full builds and runs only the relinker matrix on Linux, Windows and macOS, if the pull request changes a file the relinker-only build uses, such as the Python tools and their tests in `tools/`.
|
||||
|
||||
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [technical debt](docs/dev/TechnicalDebt.md), the index or a file in `docs/dev/debt/`, and only UTF-8 text files. Run it locally before pushing:
|
||||
|
||||
```
|
||||
python3 tools/check_conventions.py --base origin/main
|
||||
@@ -50,13 +53,22 @@ python3 tools/check_conventions.py --base origin/main
|
||||
The [pull request template](.github/pull_request_template.md) is the checklist for these rules.
|
||||
|
||||
- One branch per topic, based on current `main`. Follow-up work goes in a new pull request, not into an open one.
|
||||
- Open pull requests per author are limited by how many of theirs are merged into `main`: 1 with none merged, 20 with at least 1, no limit from 20. A pull request opened or reopened over the limit is closed automatically; reopen it once another one is merged or closed.
|
||||
- Before starting, check that no open pull request already implements the same functions.
|
||||
- Keep the branch up to date with `main` and resolve conflicts yourself; rebasing and force-pushing is fine.
|
||||
- If a pull request needs another one first, say so in the description (`Depends on #N`).
|
||||
- A pull request that conflicts with `main` and has no activity for 7 days is labeled `stale` with a reminder. It is not closed.
|
||||
- If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise).
|
||||
- If a pull request fixes a reported issue, write `Fixes #N` on its checklist line (`none` otherwise). GitHub closes the issue on merge, and the `fix error` label is added within the hour.
|
||||
- If a pull request fixes a stop or a wrong result seen on a title, name it on the checklist line with the title ID and its [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussion (`Unblocks: PPSA01325 #3232`; `none` otherwise). Reviewers review these first. Such a pull request gets the `unblocks title` label.
|
||||
- For GPU changes, list each tested GPU's vendor, model, driver and OS on the checklist line. CI runs GPU tests only on lavapipe, so reviewers may ask for a run on another vendor before merging.
|
||||
- Fixes a title needs go to `main` as pull requests. A Game bring-up discussion lists the open pull requests and local changes the title still needs; update the list when one is merged, closed or replaced.
|
||||
- The PR overlap comment lists open pull requests that change the same lines or exports; agree with their authors which one goes first and declare it with `Depends on`.
|
||||
- Run the tests before opening the pull request and describe what was tested (OS, title or homebrew).
|
||||
- A pull request that claims a change in speed shows the summary lines of `main` and of the change, from the same machine and scene, and attaches the traces, captured as the [Performance report](.github/ISSUE_TEMPLATE/performance.yml) issue form describes. A slow scene is reported with the same form. A pull request titled `perf(...)` or `perf:` is labeled `perf`, and `perf numbers missing` until its description holds two valid summary lines with different commits.
|
||||
- Changes to the AGC driver, the shader recompiler or VideoOut run the same focused tests on `main` and the change with `VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation`, `ANYPS5_NO_SHADER_CACHE=1` and `ANYPS5_REQUIRE_VULKAN=1` using `ctest -V` from the same machine and GPU, confirm neither log reports `was not found but was requested by env var VK_INSTANCE_LAYERS`, confirm the change introduces no new `VUID-` validation errors compared with `main`, and name each GPU the GPU tests ran on (vendor, model, driver, OS) on the `GPUs tested` checklist line. CI runs these tests only on lavapipe, and hardware drivers fail where lavapipe and other vendors pass, so a reviewer can ask for a run on another vendor before merging.
|
||||
- Investigation notes, reports, screenshots and logs go in the pull request, not in the repository. Images for documentation go in the [gist](https://gist.github.com/boykopovar/0e53f2e1426f29ecd41e3b51540b8a90) comments.
|
||||
- Say whether the change was written with AI assistance. The author of the pull request is responsible for every line of it.
|
||||
|
||||
## Documentation
|
||||
|
||||
Text in the repository is dry, strict and concise. Record known gaps, unknown NIDs and signatures in [technical debt](docs/dev/TechnicalDebt.md) instead of new files.
|
||||
Text in the repository is dry, strict and concise. Record known gaps, unknown NIDs and signatures in [technical debt](docs/dev/TechnicalDebt.md) instead of new files. An entry states the gap first (what differs from the console, what is unverified, what throws) and describes the implemented behaviour only as far as the gap needs it. A pull request that closes a gap removes its entry, or cuts it to what is still open, instead of rewriting it to describe the corrected behaviour. An entry goes in `docs/dev/debt/<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.
|
||||
@@ -2,29 +2,33 @@
|
||||
|
||||
Tool for automatic executables porting to Linux and Windows.
|
||||
|
||||
The relinker-only build runs on macOS, including Apple silicon.
|
||||
|
||||
[**Web converter**](https://boykopovar.github.io/AnyPS5/) - files stay on your device.
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||
[](https://boykopovar.github.io/AnyPS5/) [](https://boykopovar.github.io/AnyPS5/)
|
||||
[](https://boykopovar.github.io/AnyPS5/progress.html) [](https://boykopovar.github.io/AnyPS5/progress.html)
|
||||
|
||||
[](https://boykopovar.github.io/AnyPS5/)
|
||||
[](https://boykopovar.github.io/AnyPS5/progress.html)
|
||||
|
||||
[Progress history](https://boykopovar.github.io/AnyPS5/history.html)
|
||||
|
||||
<sub>* System libraries: percentage of the functions known to the project so far (declared in [core/libs/prx](core/libs/prx)), not of every PS5 system function. The total grows as more functions are declared.</sub>
|
||||
|
||||
[List of verified games](docs/user/COMPATIBILITY.md)
|
||||
|
||||
Dreaming Sarah (2D platformer) runs at a stable 60 fps on a GTX 1050 Ti / i5-7500 3.4GHz.
|
||||
|
||||
Unsupported or unexpected states strictly throw `std::runtime_error`. `what()` is printed to stderr and the process terminates.
|
||||
|
||||
The [shader recompiler](core/shader/recompiler/Recompiler.cpp) successfully produces SPIR-V (validated via [Spirv-Tools](3rdparty/SPIRV-Tools) when built with `ANYPS5_ENABLE_SPIRV_TOOLS`).
|
||||
|
||||
## Compatibility
|
||||
|
||||
See the [game compatibility list](docs/user/COMPATIBILITY.md) for tested games and known issues.
|
||||
Per-game progress (stage reached, open blockers, libraries and GPU coverage) is tracked on the [blocker map](https://alxcbtw.github.io/anyps5-roadmap/), a community project built from public GitHub data ([source](https://github.com/AlxcBTW/anyps5-roadmap)). Test reports go in the [Game bring-up](https://github.com/boykopovar/AnyPS5/discussions/categories/game-bring-up) discussions, one thread per title.
|
||||
|
||||
## Input mapping
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
set(ANYPS5_VULKAN_HEADERS_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include")
|
||||
set(ANYPS5_SPIRV_HEADERS_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include")
|
||||
set(ANYPS5_SDL2_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include")
|
||||
set(ANYPS5_GLSLANG_INCLUDE_DIRS "${CMAKE_SOURCE_DIR}/3rdparty/glslang")
|
||||
set(ANYPS5_SPIRV_HEADERS_SOURCE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers")
|
||||
set(ANYPS5_GLSLANG_SOURCE_DIR "${CMAKE_SOURCE_DIR}/3rdparty/glslang")
|
||||
|
||||
if(NOT ANYPS5_USE_SYSTEM_DEPS)
|
||||
return()
|
||||
endif()
|
||||
|
||||
include(CheckCXXSourceCompiles)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
if(NOT TARGET SDL2::SDL2)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SDL2 was found but does not provide SDL2::SDL2")
|
||||
endif()
|
||||
add_library(SDL2-static INTERFACE)
|
||||
target_link_libraries(SDL2-static INTERFACE SDL2::SDL2)
|
||||
get_target_property(ANYPS5_SDL2_INCLUDE_DIR SDL2::SDL2 INTERFACE_INCLUDE_DIRECTORIES)
|
||||
if(NOT ANYPS5_SDL2_INCLUDE_DIR)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SDL2::SDL2 exposes no INTERFACE_INCLUDE_DIRECTORIES")
|
||||
endif()
|
||||
set(ANYPS5_SDL2_PROBE_INCLUDES ${ANYPS5_SDL2_INCLUDE_DIR})
|
||||
foreach(_dir IN LISTS ANYPS5_SDL2_INCLUDE_DIR)
|
||||
list(APPEND ANYPS5_SDL2_PROBE_INCLUDES "${_dir}/SDL2")
|
||||
endforeach()
|
||||
list(REMOVE_DUPLICATES ANYPS5_SDL2_PROBE_INCLUDES)
|
||||
set(CMAKE_REQUIRED_INCLUDES ${ANYPS5_SDL2_PROBE_INCLUDES})
|
||||
check_cxx_source_compiles("#include \"SDL_config.h\"\n#ifndef SDL_JOYSTICK_HIDAPI\n#error SDL_JOYSTICK_HIDAPI is not defined\n#endif\nint main() { return 0; }" ANYPS5_SDL2_HAS_HIDAPI)
|
||||
unset(CMAKE_REQUIRED_INCLUDES)
|
||||
if(NOT ANYPS5_SDL2_HAS_HIDAPI)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: the system SDL2 does not define SDL_JOYSTICK_HIDAPI, so a DualSense gets no light bar, touchpad or trigger effects and the disk audio driver does not preserve the requested format; sdl2-compat (SDL3 underneath) and SDL2 builds with HIDAPI disabled both fail the pad and audio tests; install a real SDL2 2.x built with HIDAPI")
|
||||
endif()
|
||||
|
||||
find_package(Freetype REQUIRED)
|
||||
if(NOT TARGET Freetype::Freetype)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: freetype was found but does not provide Freetype::Freetype")
|
||||
endif()
|
||||
add_library(freetype INTERFACE)
|
||||
target_link_libraries(freetype INTERFACE Freetype::Freetype)
|
||||
|
||||
find_package(VulkanHeaders CONFIG REQUIRED)
|
||||
if(NOT TARGET Vulkan::Headers)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: Vulkan-Headers was found but does not provide Vulkan::Headers")
|
||||
endif()
|
||||
get_target_property(ANYPS5_VULKAN_HEADERS_INCLUDE_DIR Vulkan::Headers INTERFACE_INCLUDE_DIRECTORIES)
|
||||
if(NOT ANYPS5_VULKAN_HEADERS_INCLUDE_DIR)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: Vulkan::Headers exposes no INTERFACE_INCLUDE_DIRECTORIES")
|
||||
endif()
|
||||
set(CMAKE_REQUIRED_INCLUDES ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
check_cxx_source_compiles("#include <vulkan/vulkan.h>\nint main() { VkPhysicalDeviceMaintenance8FeaturesKHR features{}; (void)features; return 0; }" ANYPS5_VULKAN_HAS_MAINTENANCE8)
|
||||
unset(CMAKE_REQUIRED_INCLUDES)
|
||||
if(NOT ANYPS5_VULKAN_HAS_MAINTENANCE8)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: the system Vulkan-Headers do not provide VkPhysicalDeviceMaintenance8FeaturesKHR (VK_KHR_maintenance8), which the agc driver uses; install Vulkan-Headers 1.4.310 or newer")
|
||||
endif()
|
||||
|
||||
find_package(SPIRV-Headers CONFIG REQUIRED)
|
||||
if(NOT TARGET SPIRV-Headers::SPIRV-Headers)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SPIRV-Headers was found but does not provide SPIRV-Headers::SPIRV-Headers")
|
||||
endif()
|
||||
get_target_property(ANYPS5_SPIRV_HEADERS_INCLUDE_DIR SPIRV-Headers::SPIRV-Headers INTERFACE_INCLUDE_DIRECTORIES)
|
||||
if(NOT ANYPS5_SPIRV_HEADERS_INCLUDE_DIR)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SPIRV-Headers::SPIRV-Headers exposes no INTERFACE_INCLUDE_DIRECTORIES")
|
||||
endif()
|
||||
|
||||
find_package(glslang CONFIG REQUIRED)
|
||||
if(NOT TARGET glslang::glslang OR NOT TARGET glslang::SPIRV)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: glslang was found but does not provide glslang::glslang and glslang::SPIRV")
|
||||
endif()
|
||||
add_library(glslang INTERFACE)
|
||||
target_link_libraries(glslang INTERFACE glslang::glslang)
|
||||
add_library(SPIRV INTERFACE)
|
||||
target_link_libraries(SPIRV INTERFACE glslang::SPIRV)
|
||||
find_path(ANYPS5_GLSLANG_PUBLIC_INCLUDE_DIR NAMES glslang/Public/ShaderLang.h)
|
||||
if(NOT ANYPS5_GLSLANG_PUBLIC_INCLUDE_DIR)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: glslang/Public/ShaderLang.h was not found; the glslang headers are incomplete")
|
||||
endif()
|
||||
set(ANYPS5_GLSLANG_SPIRV_INCLUDE_DIR "${ANYPS5_GLSLANG_PUBLIC_INCLUDE_DIR}/glslang")
|
||||
if(NOT EXISTS "${ANYPS5_GLSLANG_SPIRV_INCLUDE_DIR}/SPIRV/GlslangToSpv.h")
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: ${ANYPS5_GLSLANG_SPIRV_INCLUDE_DIR}/SPIRV/GlslangToSpv.h was not found")
|
||||
endif()
|
||||
set(ANYPS5_GLSLANG_INCLUDE_DIRS "${ANYPS5_GLSLANG_PUBLIC_INCLUDE_DIR};${ANYPS5_GLSLANG_SPIRV_INCLUDE_DIR}")
|
||||
|
||||
find_program(ANYPS5_GLSLANG_STANDALONE_EXECUTABLE NAMES glslang glslangValidator)
|
||||
if(NOT ANYPS5_GLSLANG_STANDALONE_EXECUTABLE)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: the glslang compiler (glslang or glslangValidator) was not found; install the glslang tools package")
|
||||
endif()
|
||||
add_executable(glslang-standalone IMPORTED GLOBAL)
|
||||
set_target_properties(glslang-standalone PROPERTIES IMPORTED_LOCATION "${ANYPS5_GLSLANG_STANDALONE_EXECUTABLE}")
|
||||
|
||||
if(ANYPS5_ENABLE_SPIRV_TOOLS)
|
||||
find_package(SPIRV-Tools-opt CONFIG REQUIRED)
|
||||
if(NOT TARGET SPIRV-Tools-opt)
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SPIRV-Tools was found but does not provide SPIRV-Tools-opt")
|
||||
endif()
|
||||
if(NOT TARGET SPIRV-Tools-static)
|
||||
if(TARGET SPIRV-Tools)
|
||||
add_library(SPIRV-Tools-static INTERFACE)
|
||||
target_link_libraries(SPIRV-Tools-static INTERFACE SPIRV-Tools)
|
||||
else()
|
||||
message(FATAL_ERROR "ANYPS5_USE_SYSTEM_DEPS: SPIRV-Tools was found but provides neither SPIRV-Tools-static nor SPIRV-Tools")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(ANYPS5_SYSTEM_DEPS_STAMP "")
|
||||
list(APPEND ANYPS5_SYSTEM_DEPS_PACKAGES VulkanHeaders SPIRV-Headers glslang)
|
||||
if(ANYPS5_ENABLE_SPIRV_TOOLS)
|
||||
list(APPEND ANYPS5_SYSTEM_DEPS_PACKAGES SPIRV-Tools-opt)
|
||||
endif()
|
||||
foreach(_package IN LISTS ANYPS5_SYSTEM_DEPS_PACKAGES)
|
||||
set(_version "unknown")
|
||||
if(DEFINED ${_package}_VERSION)
|
||||
set(_version "${${_package}_VERSION}")
|
||||
endif()
|
||||
string(APPEND ANYPS5_SYSTEM_DEPS_STAMP " ${_package}=${_version}")
|
||||
endforeach()
|
||||
@@ -0,0 +1,21 @@
|
||||
set(TRACY_ENABLE ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_STATIC OFF CACHE BOOL "" FORCE)
|
||||
set(TRACY_ON_DEMAND ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_ONLY_LOCALHOST ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_NO_CRASH_HANDLER ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_NO_CODE_TRANSFER ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_NO_SAMPLING ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_NO_SYSTEM_TRACING ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_NO_CALLSTACK ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_NO_FRAME_IMAGE ON CACHE BOOL "" FORCE)
|
||||
set(TRACY_NO_VERIFY ON CACHE BOOL "" FORCE)
|
||||
set(CMAKE_DISABLE_FIND_PACKAGE_rocprofiler-sdk ON)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/tracy ${CMAKE_BINARY_DIR}/3rdparty/tracy EXCLUDE_FROM_ALL)
|
||||
|
||||
if(MINGW)
|
||||
target_sources(TracyClient PRIVATE ${CMAKE_CURRENT_LIST_DIR}/TracyThreadExit.cpp)
|
||||
target_compile_definitions(TracyClient PRIVATE TRACY_DELAYED_INIT)
|
||||
get_target_property(tracyInterfaceDefinitions TracyClient INTERFACE_COMPILE_DEFINITIONS)
|
||||
list(FILTER tracyInterfaceDefinitions EXCLUDE REGEX "^(WINVER|_WIN32_WINNT)=")
|
||||
set_target_properties(TracyClient PROPERTIES INTERFACE_COMPILE_DEFINITIONS "${tracyInterfaceDefinitions}")
|
||||
endif()
|
||||
@@ -0,0 +1,3 @@
|
||||
extern "C" int __cxa_thread_atexit(void (*)(void*), void*, void*) {
|
||||
return 0;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -16,6 +16,12 @@ function(add_jpeg_decoder target runtimeTarget)
|
||||
set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a")
|
||||
endif()
|
||||
file(MAKE_DIRECTORY "${jpeg_turbo_prefix}/include")
|
||||
set(jpeg_turbo_platform_args)
|
||||
if(APPLE)
|
||||
list(APPEND jpeg_turbo_platform_args
|
||||
-DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}
|
||||
-DCMAKE_APPLE_SILICON_PROCESSOR=${CMAKE_APPLE_SILICON_PROCESSOR})
|
||||
endif()
|
||||
ExternalProject_Add(jpeg_turbo_external
|
||||
SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo"
|
||||
BINARY_DIR "${CMAKE_BINARY_DIR}/libjpeg-turbo-build"
|
||||
@@ -38,6 +44,7 @@ function(add_jpeg_decoder target runtimeTarget)
|
||||
-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER}
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER}
|
||||
${jpeg_turbo_platform_args}
|
||||
BUILD_BYPRODUCTS
|
||||
"${jpeg_turbo_library}"
|
||||
)
|
||||
|
||||
@@ -35,6 +35,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
|
||||
@@ -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;
|
||||
|
||||
@@ -97,12 +135,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -138,9 +138,9 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> png) {
|
||||
while (png.size() - offset >= CHUNK_OVERHEAD) {
|
||||
const std::uint32_t length = readBigEndian32(&png[offset]);
|
||||
const std::uint8_t* type = &png[offset + 4];
|
||||
if (length > png.size() - offset - CHUNK_OVERHEAD) break;
|
||||
if (isChunkType(type, "tRNS")) header.hasTransparency = true;
|
||||
if (isChunkType(type, "tRNS") || isChunkType(type, "IDAT") || isChunkType(type, "IEND")) break;
|
||||
if (length > png.size() - offset - CHUNK_OVERHEAD) break;
|
||||
offset += CHUNK_OVERHEAD + length;
|
||||
}
|
||||
return header;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,12 @@ int main() {
|
||||
Require(paletteHeader.has_value());
|
||||
Require(paletteHeader->width == 2 && paletteHeader->height == 2 && paletteHeader->bitDepth == 2);
|
||||
Require(paletteHeader->colorType == Decoder::Png::ColorType::Palette && paletteHeader->hasTransparency);
|
||||
for (const std::size_t size : {std::size_t{69}, std::size_t{71}}) {
|
||||
const auto truncated = Decoder::Png::ParseHeader(std::span<const std::uint8_t>(PALETTE_TRNS, size));
|
||||
Require(truncated.has_value() && !truncated->hasTransparency);
|
||||
}
|
||||
const auto completeTransparency = Decoder::Png::ParseHeader(std::span<const std::uint8_t>(PALETTE_TRNS, 73));
|
||||
Require(completeTransparency.has_value() && completeTransparency->hasTransparency);
|
||||
const auto palette = Decoder::Png::Decode(PALETTE_TRNS);
|
||||
Require(palette.has_value());
|
||||
Require(palette->pixels == std::vector<std::uint8_t>({255, 0, 0, 255, 0, 255, 0, 0, 0, 0, 255, 255, 255, 255, 0, 128}));
|
||||
|
||||
+462
-8
@@ -1,8 +1,22 @@
|
||||
if(NOT WIN32)
|
||||
string(REPLACE "-static " "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ")
|
||||
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
|
||||
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
|
||||
endforeach()
|
||||
string(STRIP "${CMAKE_EXE_LINKER_FLAGS}" CMAKE_EXE_LINKER_FLAGS)
|
||||
endif()
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
include(CheckPIESupported)
|
||||
check_pie_supported(LANGUAGES C CXX)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(ANYPS5_PRX_RPATH "@loader_path")
|
||||
else()
|
||||
set(ANYPS5_PRX_RPATH "$ORIGIN")
|
||||
endif()
|
||||
|
||||
add_executable(nid_patcher
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/Sha1.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/NidCompute.cpp
|
||||
@@ -10,6 +24,7 @@ add_executable(nid_patcher
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/ExportExclusions.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/ElfNidPatcher.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/PeNidPatcher.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/MachONidPatcher.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/BinaryPatcherFactory.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/src/main.cpp
|
||||
)
|
||||
@@ -18,7 +33,14 @@ target_include_directories(nid_patcher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/i
|
||||
target_compile_features(nid_patcher PRIVATE cxx_std_20)
|
||||
|
||||
if(MINGW)
|
||||
target_link_options(nid_patcher PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
target_link_options(nid_patcher PRIVATE -municode -static -static-libgcc -static-libstdc++)
|
||||
include(CheckCXXSourceCompiles)
|
||||
set(CMAKE_REQUIRED_FLAGS -fno-asynchronous-unwind-tables)
|
||||
check_cxx_source_compiles("#ifdef __USING_SJLJ_EXCEPTIONS__\n#error\n#endif\nint main() { return 0; }" ANYPS5_MINGW_DWARF_EXCEPTIONS)
|
||||
unset(CMAKE_REQUIRED_FLAGS)
|
||||
if(NOT ANYPS5_MINGW_DWARF_EXCEPTIONS)
|
||||
message(FATAL_ERROR "${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION} selects SjLj exceptions with -fno-asynchronous-unwind-tables; the libraries need DWARF unwinding. Use the MinGW-w64 toolchain listed in docs/dev/BUILD.md.")
|
||||
endif()
|
||||
add_compile_options(-fno-asynchronous-unwind-tables)
|
||||
endif()
|
||||
|
||||
@@ -38,6 +60,21 @@ if(MINGW)
|
||||
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
|
||||
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
|
||||
endforeach()
|
||||
|
||||
if(NOT EXISTS "${CMAKE_RANLIB}")
|
||||
message(FATAL_ERROR "Stable import libraries require ranlib")
|
||||
endif()
|
||||
list(LENGTH CMAKE_CXX_CREATE_SHARED_LIBRARY linkCommandCount)
|
||||
string(FIND "${CMAKE_CXX_CREATE_SHARED_LIBRARY}" "-Wl,--out-implib,<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)
|
||||
@@ -45,7 +82,14 @@ add_library(libc SHARED EXCLUDE_FROM_ALL)
|
||||
set(LIBS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
add_subdirectory(${LIBC_DIR} ${CMAKE_CURRENT_BINARY_DIR}/prx/libc)
|
||||
target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
if(WIN32)
|
||||
target_compile_options(libc PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp")
|
||||
endif()
|
||||
configure_windows_unwind(libc)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
target_link_options(libc PRIVATE LINKER:-z,now)
|
||||
target_link_options(libc INTERFACE LINKER:--no-as-needed)
|
||||
endif()
|
||||
|
||||
set(UNPATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/libs/unpatched)
|
||||
set(PATCHED_DIR ${CMAKE_CURRENT_BINARY_DIR}/libs)
|
||||
@@ -58,11 +102,19 @@ set_target_properties(libc PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR}
|
||||
BUILD_WITH_INSTALL_RPATH ON
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "${ANYPS5_PRX_RPATH}"
|
||||
)
|
||||
|
||||
set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx)
|
||||
set(LIBC_PATCHED ${PATCHED_DIR}/libc.prx)
|
||||
set(LIBC_EXPORT_LIST ${CMAKE_CURRENT_BINARY_DIR}/libc.exports)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${LIBC_EXPORT_LIST}
|
||||
COMMAND $<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}
|
||||
@@ -90,10 +142,16 @@ foreach(LIB_DIR ${LIBS_DIRS})
|
||||
configure_windows_unwind(${LIB_NAME})
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
target_compile_options(${LIB_NAME} PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp")
|
||||
endif()
|
||||
if(LIB_NAME STREQUAL "libkernel")
|
||||
target_link_libraries(${LIB_NAME} PRIVATE libc)
|
||||
target_link_libraries(${LIB_NAME} PUBLIC libc)
|
||||
else()
|
||||
target_link_libraries(${LIB_NAME} PRIVATE libkernel libc)
|
||||
target_link_libraries(${LIB_NAME} PRIVATE libkernel PUBLIC libc)
|
||||
endif()
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
target_link_options(${LIB_NAME} PRIVATE LINKER:-z,now)
|
||||
endif()
|
||||
if(WIN32 AND LIB_NAME STREQUAL "libSceNet")
|
||||
target_link_libraries(${LIB_NAME} PRIVATE ws2_32)
|
||||
@@ -110,7 +168,7 @@ foreach(LIB_DIR ${LIBS_DIRS})
|
||||
LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR}
|
||||
BUILD_WITH_INSTALL_RPATH ON
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH "${ANYPS5_PRX_RPATH}"
|
||||
)
|
||||
|
||||
set(UNPATCHED_FILE ${UNPATCHED_DIR}/${LIB_NAME}.prx)
|
||||
@@ -119,8 +177,8 @@ foreach(LIB_DIR ${LIBS_DIRS})
|
||||
add_custom_command(
|
||||
OUTPUT ${PATCHED_FILE}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${UNPATCHED_FILE} ${PATCHED_FILE}
|
||||
COMMAND $<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}"
|
||||
)
|
||||
|
||||
@@ -128,35 +186,344 @@ foreach(LIB_DIR ${LIBS_DIRS})
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(APS5_ENABLE_TRACY)
|
||||
set_target_properties(TracyClient PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
|
||||
LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR}
|
||||
)
|
||||
set(TRACY_CLIENT_COPIED ${CMAKE_CURRENT_BINARY_DIR}/TracyClient.copied)
|
||||
add_custom_command(
|
||||
OUTPUT ${TRACY_CLIENT_COPIED}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${PATCHED_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:TracyClient> ${PATCHED_DIR}
|
||||
COMMAND ${CMAKE_COMMAND} -E touch ${TRACY_CLIENT_COPIED}
|
||||
DEPENDS TracyClient
|
||||
COMMENT "Copying the Tracy client next to the patched libraries"
|
||||
)
|
||||
list(APPEND PATCHED_OUTPUTS ${TRACY_CLIENT_COPIED})
|
||||
endif()
|
||||
|
||||
if(TARGET mimalloc)
|
||||
add_executable(pe_import_first ${CMAKE_CURRENT_SOURCE_DIR}/allocator/PeImportFirstMain.cpp ${CMAKE_CURRENT_SOURCE_DIR}/allocator/PeImportFirst.cpp)
|
||||
target_compile_features(pe_import_first PRIVATE cxx_std_20)
|
||||
if(MINGW)
|
||||
target_compile_options(pe_import_first PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(pe_import_first PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
add_custom_command(
|
||||
OUTPUT ${PATCHED_DIR}/mimalloc.dll
|
||||
COMMAND ${CMAKE_COMMAND} -E copy $<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)
|
||||
add_test_executable(guest_posix_scalar_io_tests tests/GuestPosixScalarIo.cpp)
|
||||
target_include_directories(guest_posix_scalar_io_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_posix_scalar_io_tests PRIVATE libkernel libc)
|
||||
configure_windows_unwind(guest_posix_scalar_io_tests)
|
||||
add_test(NAME guest_posix_scalar_io COMMAND guest_posix_scalar_io_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_posix_scalar_io PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp)
|
||||
target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
|
||||
target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20)
|
||||
if(MINGW)
|
||||
target_compile_options(nid_buffer_validation_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME nid_buffer_validation COMMAND nid_buffer_validation_tests)
|
||||
set_tests_properties(nid_buffer_validation PROPERTIES TIMEOUT 10)
|
||||
|
||||
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}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_native_paths.py $<TARGET_FILE:nid_patcher>)
|
||||
set_tests_properties(nid_patcher_native_paths PROPERTIES TIMEOUT 20)
|
||||
endif()
|
||||
add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp)
|
||||
add_dependencies(patched_libraries_tests libs)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(MINGW)
|
||||
target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
|
||||
if(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>)
|
||||
endif()
|
||||
add_test_executable(guest_kernel_sys_tests tests/GuestKernelSys.cpp)
|
||||
target_include_directories(guest_kernel_sys_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_kernel_sys_tests PRIVATE libkernel_sys libkernel libc)
|
||||
configure_windows_unwind(guest_kernel_sys_tests)
|
||||
add_test(NAME guest_kernel_sys COMMAND guest_kernel_sys_tests)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_verneed COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_verneed.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_kernel_sys PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
if(APPLE)
|
||||
add_subdirectory(tests/write_watch)
|
||||
add_subdirectory(tests/guest_arena)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_test_executable(windows_write_watch_tests tests/WindowsWriteWatch.cpp)
|
||||
target_include_directories(windows_write_watch_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_definitions(windows_write_watch_tests PRIVATE _WIN32_WINNT=0x0A00)
|
||||
if(MINGW)
|
||||
target_compile_options(windows_write_watch_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME windows_write_watch COMMAND windows_write_watch_tests)
|
||||
set_tests_properties(windows_write_watch PROPERTIES TIMEOUT 20)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
add_test_executable(undeliverable_host_exception_tests tests/UndeliverableHostException.cpp)
|
||||
add_dependencies(undeliverable_host_exception_tests libc)
|
||||
add_test(NAME undeliverable_host_exception COMMAND undeliverable_host_exception_tests $<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_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)
|
||||
configure_windows_unwind(guest_offline_imports_tests)
|
||||
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_shared_strings COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_shared_strings.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_signin_dialog_tests tests/GuestSigninDialog.cpp)
|
||||
target_include_directories(guest_signin_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_signin_dialog_tests PRIVATE libSceSigninDialog libc)
|
||||
configure_windows_unwind(guest_signin_dialog_tests)
|
||||
add_test(NAME guest_signin_dialog COMMAND guest_signin_dialog_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_signin_dialog PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSigninDialog>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_usb_storage_dialog_tests tests/GuestUsbStorageDialog.cpp)
|
||||
target_include_directories(guest_usb_storage_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_usb_storage_dialog_tests PRIVATE libSceUsbStorageDialog libc)
|
||||
configure_windows_unwind(guest_usb_storage_dialog_tests)
|
||||
add_test(NAME guest_usb_storage_dialog COMMAND guest_usb_storage_dialog_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_usb_storage_dialog PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<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)
|
||||
configure_windows_unwind(guest_netctl_ap_dialog_tests)
|
||||
add_test(NAME guest_netctl_ap_dialog COMMAND guest_netctl_ap_dialog_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_netctl_ap_dialog PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNetCtlApDialog>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_np_auth_tests tests/GuestNpAuth.cpp)
|
||||
target_include_directories(guest_np_auth_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_np_auth_tests PRIVATE libSceNpAuth libc)
|
||||
configure_windows_unwind(guest_np_auth_tests)
|
||||
add_test(NAME guest_np_auth COMMAND guest_np_auth_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_np_auth PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpAuth>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_rudp_status_tests tests/GuestRudpStatus.cpp)
|
||||
target_include_directories(guest_rudp_status_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_rudp_status_tests PRIVATE libSceRudp libc)
|
||||
configure_windows_unwind(guest_rudp_status_tests)
|
||||
add_test(NAME guest_rudp_status COMMAND guest_rudp_status_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_rudp_status PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_rudp_context_queries_tests tests/GuestRudpContextQueries.cpp)
|
||||
target_include_directories(guest_rudp_context_queries_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_rudp_context_queries_tests PRIVATE libSceRudp libc)
|
||||
configure_windows_unwind(guest_rudp_context_queries_tests)
|
||||
add_test(NAME guest_rudp_context_queries COMMAND guest_rudp_context_queries_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_rudp_context_queries PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_rudp_pump_tests tests/GuestRudpPump.cpp)
|
||||
target_include_directories(guest_rudp_pump_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_rudp_pump_tests PRIVATE libSceRudp libc)
|
||||
configure_windows_unwind(guest_rudp_pump_tests)
|
||||
add_test(NAME guest_rudp_pump COMMAND guest_rudp_pump_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_rudp_pump PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_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)
|
||||
configure_windows_unwind(guest_memory_pool_stats_tests)
|
||||
add_test(NAME guest_memory_pool_stats COMMAND guest_memory_pool_stats_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_memory_pool_stats PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSsl>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_system_gesture_tests tests/GuestSystemGesture.cpp)
|
||||
target_include_directories(guest_system_gesture_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_system_gesture_tests PRIVATE libSceSystemGesture libc)
|
||||
configure_windows_unwind(guest_system_gesture_tests)
|
||||
add_test(NAME guest_system_gesture COMMAND guest_system_gesture_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_system_gesture PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSystemGesture>")
|
||||
endif()
|
||||
|
||||
if(UNIX AND (CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64)$" OR CMAKE_OSX_ARCHITECTURES STREQUAL "x86_64"))
|
||||
add_subdirectory(tests/register_context)
|
||||
endif()
|
||||
@@ -187,6 +554,18 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(savedata_memory_growth savedata_memory_growth_read_failure PROPERTIES
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<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})
|
||||
@@ -198,6 +577,21 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
add_test(NAME host_thread_local COMMAND host_thread_local_tests)
|
||||
set_tests_properties(host_thread_local PROPERTIES TIMEOUT 15)
|
||||
if(MINGW)
|
||||
add_test(NAME prx_thread_local_destructors COMMAND ${CMAKE_COMMAND} -DNM=${CMAKE_NM} -DOBJDUMP=${CMAKE_OBJDUMP}
|
||||
-DMODULES=$<TARGET_FILE_DIR:libc> -P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ThreadLocalDestructors.cmake)
|
||||
set_tests_properties(prx_thread_local_destructors PROPERTIES TIMEOUT 300)
|
||||
endif()
|
||||
|
||||
add_test_executable(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(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp)
|
||||
target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
@@ -217,6 +611,16 @@ if(BUILD_TESTING)
|
||||
configure_windows_unwind(savedata_replace_file_failure_tests)
|
||||
add_test(NAME savedata_replace_file_failure COMMAND savedata_replace_file_failure_tests)
|
||||
|
||||
add_test_executable(guest_crt_tests tests/GuestCrt.cpp)
|
||||
target_include_directories(guest_crt_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_crt_tests PRIVATE libSceAmpr libc)
|
||||
configure_windows_unwind(guest_crt_tests)
|
||||
add_test(NAME guest_crt COMMAND guest_crt_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_crt PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAmpr>")
|
||||
endif()
|
||||
|
||||
if(MINGW)
|
||||
target_compile_options(savedata_core_write_replacement_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_compile_options(savedata_native_write_replacement_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
@@ -228,4 +632,54 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(savedata_native_write_replacement PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSaveData.native>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_posix_random_tests tests/GuestPosixRandom.cpp)
|
||||
target_include_directories(guest_posix_random_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_posix_random_tests PRIVATE libScePosixForWebKit)
|
||||
configure_windows_unwind(guest_posix_random_tests)
|
||||
add_test(NAME guest_posix_random COMMAND guest_posix_random_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_posix_random PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePosixForWebKit>")
|
||||
endif()
|
||||
|
||||
add_test_executable(MemoryPoolTest
|
||||
tests/MemoryPoolTest.cpp
|
||||
prx/libkernel/DirectMemory/MemoryPool.cpp
|
||||
)
|
||||
target_include_directories(MemoryPoolTest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
if(MINGW)
|
||||
target_compile_options(MemoryPoolTest PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
add_test(NAME MemoryPoolTest COMMAND MemoryPoolTest)
|
||||
|
||||
add_test_executable(guest_acm_tests tests/GuestAcm.cpp)
|
||||
target_include_directories(guest_acm_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_acm_tests PRIVATE libSceAcm libc)
|
||||
configure_windows_unwind(guest_acm_tests)
|
||||
add_test(NAME guest_acm COMMAND guest_acm_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_acm PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceAcm>")
|
||||
endif()
|
||||
|
||||
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()
|
||||
endif()
|
||||
+281
-25
@@ -16,6 +16,23 @@ struct alignas(16) __m128 { float v[4]; };
|
||||
|
||||
using Bool = std::uint8_t;
|
||||
|
||||
struct SonyLconv {
|
||||
char* monetary[7];
|
||||
char chars[14];
|
||||
char pad[2];
|
||||
char* decimal_point;
|
||||
char* grouping;
|
||||
char* thousands_sep;
|
||||
char* frac_grouping;
|
||||
char* frac_sep;
|
||||
char* false_text;
|
||||
char* true_text;
|
||||
char* no_text;
|
||||
char* yes_text;
|
||||
};
|
||||
static_assert(offsetof(SonyLconv, decimal_point) == 0x48);
|
||||
static_assert(sizeof(SonyLconv) == 0x90);
|
||||
|
||||
using KernelModule = std::int32_t;
|
||||
using KernelCpumask = std::uint64_t;
|
||||
using KernelUseconds = unsigned int;
|
||||
@@ -78,6 +95,14 @@ struct KernelAioRwRequest {
|
||||
std::int32_t fd;
|
||||
};
|
||||
|
||||
struct KernelAioSchedulingParam {
|
||||
std::int32_t schedulingWindowSize;
|
||||
std::int32_t delayedCountLimit;
|
||||
std::uint32_t enableSplit;
|
||||
std::uint32_t splitSize;
|
||||
std::uint32_t splitChunkSize;
|
||||
};
|
||||
|
||||
struct KernelBatchMapEntry {
|
||||
void* start;
|
||||
std::uint64_t offset;
|
||||
@@ -135,18 +160,23 @@ struct VirtualQueryInfo {
|
||||
std::uint64_t offset;
|
||||
std::int32_t protection;
|
||||
std::int32_t memory_type;
|
||||
std::uint32_t is_flexible : 1;
|
||||
std::uint32_t is_direct : 1;
|
||||
std::uint32_t is_stack : 1;
|
||||
std::uint32_t is_pooled : 1;
|
||||
std::uint32_t is_committed : 1;
|
||||
std::uint32_t is_gpu_prt : 1;
|
||||
std::uint32_t amm_usage : 1;
|
||||
std::uint32_t reserved : 1;
|
||||
std::uint8_t is_flexible : 1;
|
||||
std::uint8_t is_direct : 1;
|
||||
std::uint8_t is_stack : 1;
|
||||
std::uint8_t is_pooled : 1;
|
||||
std::uint8_t is_committed : 1;
|
||||
std::uint8_t is_gpu_prt : 1;
|
||||
std::uint8_t amm_usage : 1;
|
||||
std::uint8_t reserved : 1;
|
||||
char name[32];
|
||||
std::uint8_t gpu_mask_id;
|
||||
std::uint8_t reserved2;
|
||||
};
|
||||
static_assert(sizeof(VirtualQueryInfo) == 72);
|
||||
static_assert(alignof(VirtualQueryInfo) == 8);
|
||||
static_assert(offsetof(VirtualQueryInfo, name) == 33);
|
||||
static_assert(offsetof(VirtualQueryInfo, gpu_mask_id) == 65);
|
||||
static_assert(offsetof(VirtualQueryInfo, reserved2) == 66);
|
||||
|
||||
struct KernelSemaPrivate;
|
||||
struct KernelEventFlagPrivate;
|
||||
@@ -255,6 +285,12 @@ struct ModuleInfoForUnwind {
|
||||
std::uint64_t seg0_size;
|
||||
};
|
||||
|
||||
struct InternalSegmentInfo {
|
||||
std::uint64_t address;
|
||||
std::uint64_t size;
|
||||
};
|
||||
static_assert(sizeof(InternalSegmentInfo) == 0x10);
|
||||
|
||||
struct MallocReplace {
|
||||
std::uint64_t size = sizeof(MallocReplace);
|
||||
void* malloc_initialize = nullptr;
|
||||
@@ -521,6 +557,47 @@ struct AudioOut2MasteringStatesHeader {
|
||||
std::uint32_t states_id;
|
||||
};
|
||||
|
||||
struct AudioOut2MasteringStatesDescriptor {
|
||||
std::uint32_t id;
|
||||
std::uint32_t size;
|
||||
};
|
||||
|
||||
struct AudioOut2MasteringCompressorStates {
|
||||
AudioOut2MasteringStatesDescriptor descriptor;
|
||||
std::uint32_t reserved[2];
|
||||
float input_rms[3][8];
|
||||
float compression_coeff[3][8];
|
||||
};
|
||||
|
||||
struct AudioOut2MasteringCompressorStatesV2 {
|
||||
AudioOut2MasteringStatesDescriptor descriptor;
|
||||
std::uint32_t reserved[2];
|
||||
float input_rms[4][8];
|
||||
float compression_coeff[4][8];
|
||||
};
|
||||
|
||||
struct AudioOut2MasteringLimiterStates {
|
||||
AudioOut2MasteringStatesDescriptor descriptor;
|
||||
std::uint32_t reserved[2];
|
||||
float input_peak[8];
|
||||
float output_peak[8];
|
||||
float gain_peak[8];
|
||||
};
|
||||
|
||||
struct AudioOut2MasteringStates {
|
||||
AudioOut2MasteringStatesHeader states_header;
|
||||
std::uint32_t reserved[3];
|
||||
AudioOut2MasteringCompressorStates compressor_states;
|
||||
AudioOut2MasteringLimiterStates limiter_states;
|
||||
};
|
||||
|
||||
struct AudioOut2MasteringStatesV2 {
|
||||
AudioOut2MasteringStatesHeader states_header;
|
||||
std::uint32_t reserved[3];
|
||||
AudioOut2MasteringCompressorStatesV2 compressor_states;
|
||||
AudioOut2MasteringLimiterStates limiter_states;
|
||||
};
|
||||
|
||||
struct AudioOutOutputParam {
|
||||
int handle;
|
||||
const void* ptr;
|
||||
@@ -644,10 +721,13 @@ struct AvPlayerVideoEx {
|
||||
std::uint8_t luma_bit_depth;
|
||||
std::uint8_t chroma_bit_depth;
|
||||
bool video_full_range_flag;
|
||||
std::uint8_t reserved1[37];
|
||||
std::uint8_t reserved1[5];
|
||||
double frame_rate;
|
||||
std::uint8_t reserved2[24];
|
||||
};
|
||||
static_assert(sizeof(AvPlayerVideoEx) == 80);
|
||||
static_assert(offsetof(AvPlayerVideoEx, crop_left_offset) == 20 && offsetof(AvPlayerVideoEx, pitch) == 36);
|
||||
static_assert(offsetof(AvPlayerVideoEx, frame_rate) == 48);
|
||||
|
||||
struct AvPlayerTimedTextEx {
|
||||
std::uint8_t language_code[4];
|
||||
@@ -882,6 +962,14 @@ struct VoicePortInfo {
|
||||
std::uint16_t reserved;
|
||||
};
|
||||
|
||||
struct VoiceResourceInfo {
|
||||
std::uint16_t max_voice_input_ports;
|
||||
std::uint16_t max_voice_output_ports;
|
||||
std::uint16_t max_device_input_ports;
|
||||
std::uint16_t max_device_output_ports;
|
||||
std::uint16_t max_ports;
|
||||
};
|
||||
|
||||
struct VoiceStartParam {
|
||||
void* container;
|
||||
std::uint32_t mem_size;
|
||||
@@ -1067,8 +1155,8 @@ struct ImeKeycode {
|
||||
std::uint64_t timestamp;
|
||||
};
|
||||
|
||||
using ImeTextFilter = std::int32_t (*)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
|
||||
using ImeExtKeyboardFilter = int (*)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
|
||||
using ImeTextFilter = std::int32_t (APS5_VABI *)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
|
||||
using ImeExtKeyboardFilter = int (APS5_VABI *)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
|
||||
|
||||
struct ImeExtendedParam {
|
||||
std::uint32_t option;
|
||||
@@ -1150,7 +1238,7 @@ struct ImeEvent {
|
||||
ImeEventParam param;
|
||||
};
|
||||
|
||||
using EventHandler = void (*)(void* arg, const ImeEvent* event);
|
||||
using EventHandler = void (APS5_VABI *)(void* arg, const ImeEvent* event);
|
||||
|
||||
struct Param {
|
||||
std::int32_t user_id;
|
||||
@@ -1234,9 +1322,16 @@ struct NetCtlNatInfo { std::uint8_t opaque[128]; };
|
||||
|
||||
union NetCtlInfo { std::uint8_t opaque[256]; };
|
||||
|
||||
using NetCtlCallback = void (*)(int, void*);
|
||||
struct NetCtlIfStat {
|
||||
std::uint32_t device;
|
||||
std::uint64_t tx_bytes;
|
||||
std::uint64_t rx_bytes;
|
||||
std::uint32_t reserved[8];
|
||||
};
|
||||
|
||||
struct HttpEpoll {};
|
||||
using NetCtlCallback = void (APS5_VABI *)(int, void*);
|
||||
|
||||
struct HttpEpoll;
|
||||
using HttpEpollHandle = HttpEpoll*;
|
||||
using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*);
|
||||
using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*);
|
||||
@@ -1278,6 +1373,18 @@ struct NpTitleId { char data[13]; char pad[3]; };
|
||||
struct NpTitleSecret { std::uint8_t data[128]; };
|
||||
struct NpContentRestriction { std::uint8_t opaque[128]; };
|
||||
struct NpOnlineId { char data[17]; char pad[3]; };
|
||||
|
||||
struct SharePlayConnectionInfoA {
|
||||
std::int32_t status;
|
||||
std::int32_t mode;
|
||||
NpOnlineId hostOnlineId;
|
||||
NpOnlineId visitorOnlineId;
|
||||
std::uint64_t hostAccountId;
|
||||
std::uint64_t visitorAccountId;
|
||||
std::int32_t hostUserId;
|
||||
std::int32_t visitorUserId;
|
||||
};
|
||||
static_assert(sizeof(SharePlayConnectionInfoA) == 72 && offsetof(SharePlayConnectionInfoA, hostAccountId) == 48 && offsetof(SharePlayConnectionInfoA, hostUserId) == 64);
|
||||
struct NpId { NpOnlineId online_id; std::uint8_t opaque[4]; };
|
||||
struct NpCreateAsyncRequestParameter { std::uint8_t opaque[64]; };
|
||||
struct NpCheckPremiumParameter { std::uint8_t opaque[64]; };
|
||||
@@ -1314,10 +1421,6 @@ struct NpUniversalDataSystemMemoryStat {
|
||||
std::size_t current_inuse_size;
|
||||
};
|
||||
|
||||
struct NpUniversalDataSystemEvent {};
|
||||
struct NpUniversalDataSystemEventPropertyObject {};
|
||||
struct NpUniversalDataSystemEventPropertyArray {};
|
||||
|
||||
struct NpUniversalDataSystemStorageStat {
|
||||
std::size_t in_events;
|
||||
std::size_t out_events;
|
||||
@@ -1336,6 +1439,11 @@ struct NpGameIntentData {
|
||||
std::uint8_t padding[7];
|
||||
};
|
||||
|
||||
struct NpGameIntentInitParam {
|
||||
std::size_t size;
|
||||
std::uint8_t reserved[32];
|
||||
};
|
||||
|
||||
struct NpGameIntentInfo {
|
||||
std::size_t size;
|
||||
std::int32_t user_id;
|
||||
@@ -1352,6 +1460,26 @@ struct NpWebApi2ResponseInformationOption {
|
||||
std::size_t response_data_size;
|
||||
};
|
||||
|
||||
struct NpWebApi2IntInitializeArgs {
|
||||
std::int32_t lib_http_ctx_id;
|
||||
std::int32_t reserved;
|
||||
std::uint64_t pool_size;
|
||||
const char* name;
|
||||
std::uint64_t struct_size;
|
||||
};
|
||||
static_assert(sizeof(NpWebApi2IntInitializeArgs) == 32);
|
||||
|
||||
struct NpWebApi2IntInitialize2Args {
|
||||
std::int32_t lib_http_ctx_id;
|
||||
std::int32_t reserved;
|
||||
std::uint64_t pool_size;
|
||||
const char* name;
|
||||
std::uint32_t push_config_group;
|
||||
std::int32_t reserved2;
|
||||
std::uint64_t struct_size;
|
||||
};
|
||||
static_assert(sizeof(NpWebApi2IntInitialize2Args) == 40);
|
||||
|
||||
struct GameUpdateCheckParam {
|
||||
std::size_t size;
|
||||
std::uint32_t option;
|
||||
@@ -1411,8 +1539,19 @@ struct SaveDataMountInfo {
|
||||
|
||||
struct SceSaveDataTitleId { char data[10]; char pad[2]; };
|
||||
struct SceSaveDataDirName { char data[32]; };
|
||||
struct SaveDataSearchInfo { std::uint8_t opaque[128]; };
|
||||
struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; };
|
||||
struct SaveDataSearchInfo {
|
||||
std::uint64_t blocks;
|
||||
std::uint64_t free_blocks;
|
||||
std::uint8_t reserved[32];
|
||||
};
|
||||
static_assert(sizeof(SaveDataSearchInfo) == 48);
|
||||
struct SaveDataMemoryData {
|
||||
void* buf;
|
||||
std::size_t buf_size;
|
||||
std::size_t offset;
|
||||
std::uint8_t reserved[40];
|
||||
};
|
||||
static_assert(sizeof(SaveDataMemoryData) == 64);
|
||||
|
||||
struct SaveDataMount3 {
|
||||
int user_id;
|
||||
@@ -1534,6 +1673,13 @@ struct SaveDataBackup {
|
||||
std::uint8_t reserved[32];
|
||||
};
|
||||
|
||||
struct SaveDataConvert {
|
||||
std::int32_t user_id;
|
||||
std::int32_t pad;
|
||||
const SceSaveDataDirName* dir_name;
|
||||
std::uint8_t unknown[48];
|
||||
};
|
||||
|
||||
struct AppContentInitParam { char reserved[32]; };
|
||||
|
||||
struct AppContentBootParam {
|
||||
@@ -1553,8 +1699,50 @@ struct ContentExportInitParam2 {
|
||||
std::int64_t reserved1;
|
||||
};
|
||||
|
||||
struct ContentExportParam {
|
||||
char title[257];
|
||||
char comment[257];
|
||||
char content_type[65];
|
||||
};
|
||||
static_assert(sizeof(ContentExportParam) == 579);
|
||||
|
||||
struct ContentExportCallbackParam {
|
||||
std::size_t content_length;
|
||||
void* func;
|
||||
void* user_data;
|
||||
};
|
||||
|
||||
struct ContentSearchInitParam { std::size_t memory_size; };
|
||||
|
||||
struct ContentSearchColumnValue {
|
||||
std::uint32_t size;
|
||||
std::int64_t value;
|
||||
};
|
||||
|
||||
struct ContentSearchContentColumn {
|
||||
std::int32_t column_type;
|
||||
std::int32_t column_operator;
|
||||
const ContentSearchColumnValue* value;
|
||||
std::int32_t logical_connector;
|
||||
};
|
||||
|
||||
struct ContentSearchContentColumnSet {
|
||||
const ContentSearchContentColumn* columns;
|
||||
std::int32_t columns_length;
|
||||
std::int32_t logical_connector;
|
||||
};
|
||||
|
||||
struct ContentSearchContentOrderByCondition {
|
||||
std::int32_t column_type;
|
||||
std::int32_t sort_type;
|
||||
};
|
||||
|
||||
static_assert(sizeof(ContentSearchColumnValue) == 16);
|
||||
static_assert(sizeof(ContentSearchContentColumn) == 24);
|
||||
static_assert(offsetof(ContentSearchContentColumn, logical_connector) == 16);
|
||||
static_assert(sizeof(ContentSearchContentColumnSet) == 16);
|
||||
static_assert(sizeof(ContentSearchContentOrderByCondition) == 8);
|
||||
|
||||
struct PngEncCreateParam {
|
||||
std::uint32_t this_size;
|
||||
std::uint32_t attribute;
|
||||
@@ -1646,23 +1834,29 @@ struct JpegEncOutputInfo {
|
||||
};
|
||||
|
||||
struct JpegDecCreateParam {
|
||||
std::uint32_t size;
|
||||
std::uint32_t this_size;
|
||||
std::uint32_t attribute;
|
||||
std::uint32_t max_image_width;
|
||||
};
|
||||
|
||||
struct JpegDecParseParam {
|
||||
const void* jpeg_mem_addr;
|
||||
std::uint32_t jpeg_mem_size;
|
||||
std::uint32_t reserved0;
|
||||
std::uint16_t decode_mode;
|
||||
std::uint16_t down_scale;
|
||||
};
|
||||
|
||||
struct JpegDecDecodeParam {
|
||||
const void* jpeg_mem_addr;
|
||||
void* image_mem_addr;
|
||||
void* coefficient_mem_addr;
|
||||
std::uint32_t jpeg_mem_size;
|
||||
std::uint32_t image_mem_size;
|
||||
std::uint32_t coefficient_mem_size;
|
||||
std::uint16_t decode_mode;
|
||||
std::uint16_t down_scale;
|
||||
std::uint16_t pixel_format;
|
||||
std::uint16_t reserved0;
|
||||
std::uint16_t alpha_value;
|
||||
std::uint32_t image_pitch;
|
||||
};
|
||||
|
||||
@@ -1670,8 +1864,13 @@ struct JpegDecImageInfo {
|
||||
std::uint32_t image_width;
|
||||
std::uint32_t image_height;
|
||||
std::uint16_t color_space;
|
||||
std::uint16_t bit_depth;
|
||||
std::uint32_t image_flag;
|
||||
std::uint16_t component_number;
|
||||
std::uint8_t component_id[4];
|
||||
std::uint8_t sampling_factor[4];
|
||||
std::uint32_t coefficient_mem_size;
|
||||
std::uint32_t suitable_csc_attribute;
|
||||
std::uint32_t output_image_width;
|
||||
std::uint32_t output_image_height;
|
||||
};
|
||||
|
||||
struct PlayGoInitParams {
|
||||
@@ -1722,6 +1921,15 @@ struct SystemServiceDisplaySafeAreaInfo {
|
||||
std::uint8_t reserved[128];
|
||||
};
|
||||
|
||||
struct SystemServicePlayerDialogParam {
|
||||
std::uint64_t size;
|
||||
std::int32_t mode;
|
||||
std::int32_t user_id;
|
||||
std::uint64_t target_account_id;
|
||||
std::uint8_t reserved[40];
|
||||
};
|
||||
static_assert(sizeof(SystemServicePlayerDialogParam) == 0x40);
|
||||
|
||||
struct SystemServiceHdrToneMapLuminance {
|
||||
float max_full_frame_tone_map_luminance;
|
||||
float max_tone_map_luminance;
|
||||
@@ -1762,6 +1970,13 @@ struct SystemGestureTouchRecognizerInformation {
|
||||
|
||||
struct SystemGestureTouchEvent { std::uint8_t reserve[168]; };
|
||||
|
||||
struct SystemGestureTouchPadData {
|
||||
std::int32_t pad_handle;
|
||||
std::int32_t report_number;
|
||||
const PadData* pad_data_buffer;
|
||||
std::uint8_t reserve[8];
|
||||
};
|
||||
|
||||
struct UserServiceLoginUserIdList { int user_id[4]; };
|
||||
|
||||
struct UserServiceRegisteredUserIdList { int user_id[16]; };
|
||||
@@ -1839,6 +2054,42 @@ struct VideoOutVblankStatus {
|
||||
std::uint8_t pad1[6] = {};
|
||||
};
|
||||
|
||||
struct VideoOutBufferAttribute {
|
||||
std::uint32_t pixel_format;
|
||||
std::uint32_t tiling_mode;
|
||||
std::uint32_t aspect_ratio;
|
||||
std::uint32_t width;
|
||||
std::uint32_t height;
|
||||
std::uint32_t pitch_in_pixel;
|
||||
std::uint32_t option;
|
||||
std::uint32_t reserved0;
|
||||
std::uint64_t reserved1;
|
||||
};
|
||||
static_assert(sizeof(VideoOutBufferAttribute) == 40);
|
||||
|
||||
struct VideoOutFlipStatusPs4 {
|
||||
std::uint64_t count = 0;
|
||||
std::uint64_t processTime = 0;
|
||||
std::uint64_t tsc = 0;
|
||||
std::int64_t flipArg = 0;
|
||||
std::uint64_t submitTsc = 0;
|
||||
std::uint64_t reserved0 = 0;
|
||||
std::int32_t gcQueueNum = 0;
|
||||
std::int32_t flipPendingNum = 0;
|
||||
std::int32_t currentBuffer = 0;
|
||||
std::uint32_t reserved1 = 0;
|
||||
};
|
||||
static_assert(sizeof(VideoOutFlipStatusPs4) == 64);
|
||||
|
||||
struct VideoOutVblankStatusPs4 {
|
||||
std::uint64_t count = 0;
|
||||
std::uint64_t processTime = 0;
|
||||
std::uint64_t tsc = 0;
|
||||
std::uint8_t flags = 0;
|
||||
std::uint8_t pad[7] = {};
|
||||
};
|
||||
static_assert(sizeof(VideoOutVblankStatusPs4) == 32);
|
||||
|
||||
struct VideoOutOutputStatus {
|
||||
std::uint32_t resolution = 0;
|
||||
std::uint32_t dynamicRange = 0;
|
||||
@@ -1867,6 +2118,11 @@ struct VideoOutColorSettings {
|
||||
std::uint32_t reserved[3] = {};
|
||||
};
|
||||
|
||||
struct VideoOutColorSpaceConversionSettings {
|
||||
std::uint32_t internalData[8] = {};
|
||||
};
|
||||
static_assert(sizeof(VideoOutColorSpaceConversionSettings) == 32);
|
||||
|
||||
|
||||
using atexit_func_t = void (*)();
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#include "PeImportFirst.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#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::string& 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);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#ifndef CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP
|
||||
#define CORE_LIBS_ALLOCATOR_PEIMPORTFIRST_HPP
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Allocator {
|
||||
|
||||
void MoveImportFirst(std::vector<unsigned char>& image, const std::string& dll);
|
||||
void MoveImportFirstInFile(const std::string& path, const std::string& dll);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,18 @@
|
||||
#include "PeImportFirst.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 3) {
|
||||
std::fprintf(stderr, "usage: pe_import_first <file> <dll>\n");
|
||||
return 2;
|
||||
}
|
||||
try {
|
||||
Allocator::MoveImportFirstInFile(argv[1], argv[2]);
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "pe_import_first: %s: %s\n", argv[1], error.what());
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -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,156 @@
|
||||
#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.string(), "mimalloc-redirect.dll");
|
||||
const auto moved = readFile(path);
|
||||
require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll", "the file's import did not move first");
|
||||
}
|
||||
|
||||
void failsWithoutTouchingTheFile(const std::filesystem::path& directory, const std::string& what, const std::vector<unsigned char>& bytes, const std::string& expected) {
|
||||
const auto path = directory / (what + ".dll");
|
||||
writeFile(path, bytes);
|
||||
try {
|
||||
Allocator::MoveImportFirstInFile(path.string(), "mimalloc-redirect.dll");
|
||||
} catch (const std::exception& error) {
|
||||
require(std::string(error.what()).find(expected) != std::string::npos, what + ": failed with another error: " + error.what());
|
||||
require(readFile(path) == bytes, what + ": the file changed although the move failed");
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error(what + ": the move did not fail");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto directory = std::filesystem::temp_directory_path() / ("aps5-pe-import-first-" + std::to_string(std::random_device{}()));
|
||||
try {
|
||||
std::filesystem::create_directories(directory);
|
||||
movesTheNamedImportFirst();
|
||||
movesTheImportInAFile(directory);
|
||||
failsWithoutTouchingTheFile(directory, "missing-import", image({"kernel32.dll", "ucrtbase.dll"}), "does not import mimalloc-redirect.dll");
|
||||
auto truncated = image({"kernel32.dll", "mimalloc-redirect.dll", "ucrtbase.dll"});
|
||||
truncated.resize(SectionOffset + DescriptorBytes + DescriptorBytes / 2);
|
||||
failsWithoutTouchingTheFile(directory, "truncated-directory", truncated, "truncated import directory");
|
||||
failsWithoutTouchingTheFile(directory, "pe32", image({"kernel32.dll", "mimalloc-redirect.dll"}, 0x10b), "not a PE32+ image");
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove_all(directory, ignored);
|
||||
return 1;
|
||||
}
|
||||
std::filesystem::remove_all(directory);
|
||||
std::cout << "pe import first tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef CORE_LIBS_NID_MACHONIDPATCHER_HPP
|
||||
#define CORE_LIBS_NID_MACHONIDPATCHER_HPP
|
||||
|
||||
#include <nid/IBinaryPatcher.hpp>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Nid {
|
||||
|
||||
constexpr std::uint32_t kMachOMagic64 = 0xfeedfacfu;
|
||||
|
||||
struct MachOExport {
|
||||
std::string Name;
|
||||
std::uint64_t Flags = 0;
|
||||
std::uint64_t Address = 0;
|
||||
std::uint64_t Other = 0;
|
||||
std::string ImportName;
|
||||
};
|
||||
|
||||
std::vector<MachOExport> ReadMachOExports(const std::vector<std::uint8_t>& binary);
|
||||
|
||||
class MachONidPatcher final : public IBinaryPatcher {
|
||||
public:
|
||||
void PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef NID_NATIVEPATH_HPP
|
||||
#define NID_NATIVEPATH_HPP
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace Nid::Internal {
|
||||
|
||||
inline std::filesystem::path NativePath(const std::string& path) {
|
||||
return std::filesystem::path(std::u8string(path.begin(), path.end()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -61,7 +61,7 @@ inline std::string StripNidPostfix(const std::string& name) {
|
||||
|
||||
template<typename T>
|
||||
T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
|
||||
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("read out of bounds");
|
||||
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("read out of bounds");
|
||||
T v;
|
||||
std::memcpy(&v, buf.data() + offset, sizeof(T));
|
||||
return v;
|
||||
@@ -69,7 +69,7 @@ T Read(const std::vector<std::uint8_t>& buf, std::size_t offset) {
|
||||
|
||||
template<typename T>
|
||||
void Write(std::vector<std::uint8_t>& buf, std::size_t offset, const T& v) {
|
||||
if (offset + sizeof(T) > buf.size()) throw std::runtime_error("write out of bounds");
|
||||
if (offset > buf.size() || sizeof(T) > buf.size() - offset) throw std::runtime_error("write out of bounds");
|
||||
std::memcpy(buf.data() + offset, &v, sizeof(T));
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ inline std::string ReadCStr(const std::vector<std::uint8_t>& buf, std::size_t of
|
||||
std::string s;
|
||||
while (offset < buf.size() && buf[offset] != 0u)
|
||||
s.push_back(static_cast<char>(buf[offset++]));
|
||||
if (offset == buf.size()) throw std::runtime_error("unterminated cstr");
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <nid/BinaryPatcherFactory.hpp>
|
||||
#include <nid/ElfPatcher.hpp>
|
||||
#include <nid/MachONidPatcher.hpp>
|
||||
#include <nid/PeNidPatcher.hpp>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -10,6 +11,8 @@ std::unique_ptr<IBinaryPatcher> MakePatcher(const std::vector<std::uint8_t>& bin
|
||||
return std::make_unique<ElfNidPatcher>();
|
||||
if (binary.size() >= 2 && binary[0] == 'M' && binary[1] == 'Z')
|
||||
return std::make_unique<PeNidPatcher>();
|
||||
if (binary.size() >= 4 && binary[0] == 0xcf && binary[1] == 0xfa && binary[2] == 0xed && binary[3] == 0xfe)
|
||||
return std::make_unique<MachONidPatcher>();
|
||||
throw std::runtime_error("unrecognized binary format");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <nid/NidCompute.hpp>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
@@ -15,6 +16,11 @@ namespace Nid {
|
||||
namespace {
|
||||
|
||||
|
||||
void RequireElfRange(const std::vector<std::uint8_t>& elf, std::uint64_t offset, std::uint64_t size, const char* table) {
|
||||
if (offset > elf.size() || size > elf.size() - offset)
|
||||
throw std::runtime_error(std::string("invalid ELF ") + table + " range");
|
||||
}
|
||||
|
||||
std::uint32_t GnuHash(const std::string& name) {
|
||||
std::uint32_t h = 5381u;
|
||||
for (const unsigned char c : name)
|
||||
@@ -22,6 +28,44 @@ std::uint32_t GnuHash(const std::string& name) {
|
||||
return h;
|
||||
}
|
||||
|
||||
std::uint32_t SysvHash(const std::string& name) {
|
||||
std::uint32_t hash = 0u;
|
||||
for (const unsigned char byte : name) {
|
||||
hash = (hash << 4u) + byte;
|
||||
const auto high = hash & 0xf0000000u;
|
||||
hash ^= high >> 24u;
|
||||
hash &= ~high;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
void ValidateSysvHash(const std::vector<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;
|
||||
@@ -36,6 +80,18 @@ GnuHashLayout ReadGnuHashLayout(const std::vector<std::uint8_t>& elf, std::size_
|
||||
return layout;
|
||||
}
|
||||
|
||||
void ValidateGnuHash(const std::vector<std::uint8_t>& elf, std::size_t offset, std::size_t size, std::size_t symCount) {
|
||||
if (offset > elf.size() || size > elf.size() - offset || size < 16u)
|
||||
throw std::runtime_error("invalid gnu.hash section range");
|
||||
const auto layout = ReadGnuHashLayout(elf, offset);
|
||||
if (layout.SymOffset > symCount || !layout.BloomSize || (layout.BloomSize & (layout.BloomSize - 1u)) ||
|
||||
layout.BloomShift >= 32u || (!layout.NBuckets && layout.SymOffset != symCount))
|
||||
throw std::runtime_error("invalid gnu.hash geometry");
|
||||
const std::uint64_t required = 16u + std::uint64_t{layout.BloomSize} * 8u +
|
||||
std::uint64_t{layout.NBuckets} * 4u + (symCount - layout.SymOffset) * 4u;
|
||||
if (required > size) throw std::runtime_error("gnu.hash section too small to rebuild in place");
|
||||
}
|
||||
|
||||
void ReorderDynSymForGnuHash(
|
||||
std::vector<std::uint8_t>& elf,
|
||||
std::size_t dynSymOffset,
|
||||
@@ -204,49 +260,65 @@ void RemapDynamicNeededOffsets(
|
||||
}
|
||||
}
|
||||
|
||||
struct VersionNameField {
|
||||
std::size_t Offset;
|
||||
std::uint32_t Name;
|
||||
};
|
||||
|
||||
std::vector<VersionNameField> ReadVersionNameFields(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
|
||||
using namespace Internal;
|
||||
std::vector<VersionNameField> fields;
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const auto section = Read<Elf64_Shdr>(elf, static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr));
|
||||
if (section.sh_type != kShtGnuVerneed || section.sh_size == 0u) continue;
|
||||
if (section.sh_offset > elf.size() || section.sh_size > elf.size() - section.sh_offset)
|
||||
throw std::runtime_error("invalid .gnu.version_r section range");
|
||||
const auto location = [&](std::uint64_t relative, std::size_t size) {
|
||||
if (relative > section.sh_size || size > section.sh_size - relative)
|
||||
throw std::runtime_error(".gnu.version_r record outside section");
|
||||
return static_cast<std::size_t>(section.sh_offset + relative);
|
||||
};
|
||||
const auto advance = [&](std::uint64_t current, std::uint32_t step, std::size_t size) {
|
||||
if (step < size || step > section.sh_size - current)
|
||||
throw std::runtime_error("invalid .gnu.version_r record link");
|
||||
return current + step;
|
||||
};
|
||||
std::uint64_t current = 0u;
|
||||
while (true) {
|
||||
const auto offset = location(current, sizeof(Elf64_Verneed));
|
||||
const auto need = Read<Elf64_Verneed>(elf, offset);
|
||||
if (need.vn_version != 1u) throw std::runtime_error("unsupported .gnu.version_r structure version");
|
||||
fields.push_back({offset + offsetof(Elf64_Verneed, vn_file), need.vn_file});
|
||||
if (need.vn_cnt != 0u) {
|
||||
auto auxiliary = advance(current, need.vn_aux, sizeof(Elf64_Verneed));
|
||||
for (std::uint16_t index = 0u; index < need.vn_cnt; ++index) {
|
||||
const auto auxOffset = location(auxiliary, sizeof(Elf64_Vernaux));
|
||||
const auto aux = Read<Elf64_Vernaux>(elf, auxOffset);
|
||||
fields.push_back({auxOffset + offsetof(Elf64_Vernaux, vna_name), aux.vna_name});
|
||||
if (index + 1u == need.vn_cnt) {
|
||||
if (aux.vna_next != 0u) throw std::runtime_error(".gnu.version_r auxiliary count mismatch");
|
||||
} else {
|
||||
auxiliary = advance(auxiliary, aux.vna_next, sizeof(Elf64_Vernaux));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (need.vn_next == 0u) break;
|
||||
current = advance(current, need.vn_next, sizeof(Elf64_Verneed));
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
void RemapVerneedOffsets(
|
||||
std::vector<std::uint8_t>& elf,
|
||||
const Elf64_Ehdr& ehdr,
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>>& oldToNewOffset
|
||||
) {
|
||||
using namespace Internal;
|
||||
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type != kShtGnuVerneed) continue;
|
||||
|
||||
const std::size_t verneedOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
std::size_t entryOffset = verneedOffset;
|
||||
|
||||
while (true) {
|
||||
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
|
||||
|
||||
const auto mappedFile = std::find_if(
|
||||
oldToNewOffset.begin(), oldToNewOffset.end(),
|
||||
[&](const auto& entry) { return entry.first == verneed.vn_file; }
|
||||
);
|
||||
if (mappedFile != oldToNewOffset.end())
|
||||
Write(elf, entryOffset + offsetof(Elf64_Verneed, vn_file), mappedFile->second);
|
||||
|
||||
std::size_t auxOffset = entryOffset + verneed.vn_aux;
|
||||
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
|
||||
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
|
||||
|
||||
const auto mappedName = std::find_if(
|
||||
oldToNewOffset.begin(), oldToNewOffset.end(),
|
||||
[&](const auto& entry) { return entry.first == vernaux.vna_name; }
|
||||
);
|
||||
if (mappedName != oldToNewOffset.end())
|
||||
Write(elf, auxOffset + offsetof(Elf64_Vernaux, vna_name), mappedName->second);
|
||||
|
||||
if (vernaux.vna_next == 0u) break;
|
||||
auxOffset += vernaux.vna_next;
|
||||
}
|
||||
|
||||
if (verneed.vn_next == 0u) break;
|
||||
entryOffset += verneed.vn_next;
|
||||
}
|
||||
for (const auto& field : ReadVersionNameFields(elf, ehdr)) {
|
||||
const auto mapped = std::find_if(oldToNewOffset.begin(), oldToNewOffset.end(),
|
||||
[&](const auto& entry) { return entry.first == field.Name; });
|
||||
if (mapped != oldToNewOffset.end()) Write(elf, field.Offset, mapped->second);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,35 +347,8 @@ std::vector<std::uint32_t> CollectDynamicNameOffsets(const std::vector<std::uint
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> CollectVerneedNameOffsets(const std::vector<std::uint8_t>& elf, const Elf64_Ehdr& ehdr) {
|
||||
using namespace Internal;
|
||||
|
||||
std::vector<std::uint32_t> offsets;
|
||||
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type != kShtGnuVerneed) continue;
|
||||
|
||||
std::size_t entryOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
|
||||
while (true) {
|
||||
const auto verneed = Read<Elf64_Verneed>(elf, entryOffset);
|
||||
offsets.push_back(verneed.vn_file);
|
||||
|
||||
std::size_t auxOffset = entryOffset + verneed.vn_aux;
|
||||
for (std::uint16_t a = 0u; a < verneed.vn_cnt; ++a) {
|
||||
const auto vernaux = Read<Elf64_Vernaux>(elf, auxOffset);
|
||||
offsets.push_back(vernaux.vna_name);
|
||||
|
||||
if (vernaux.vna_next == 0u) break;
|
||||
auxOffset += vernaux.vna_next;
|
||||
}
|
||||
|
||||
if (verneed.vn_next == 0u) break;
|
||||
entryOffset += verneed.vn_next;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& field : ReadVersionNameFields(elf, ehdr)) offsets.push_back(field.Name);
|
||||
return offsets;
|
||||
}
|
||||
|
||||
@@ -316,9 +361,14 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
if (elf[4] != 2) throw std::runtime_error("not ELF64");
|
||||
|
||||
const auto ehdr = Read<Elf64_Ehdr>(elf, 0u);
|
||||
if (ehdr.e_ident[5] != 1 || ehdr.e_shentsize != sizeof(Elf64_Shdr))
|
||||
throw std::runtime_error("unsupported ELF byte order or section header size");
|
||||
RequireElfRange(elf, ehdr.e_shoff, std::uint64_t{ehdr.e_shnum} * sizeof(Elf64_Shdr), "section table");
|
||||
|
||||
std::size_t dynSymOffset = 0u, dynSymSize = 0u;
|
||||
std::size_t gnuHashOffset = 0u, gnuHashSize = 0u;
|
||||
std::size_t sysvHashOffset = 0u, sysvHashSize = 0u;
|
||||
std::uint32_t sysvHashLink = 0u;
|
||||
std::size_t versymOffset = 0u;
|
||||
std::uint32_t dynSymSectionIndex = 0u;
|
||||
std::uint32_t gnuHashSymOffset = 0u;
|
||||
@@ -326,6 +376,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + i * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type == kShtDynsym) {
|
||||
if (shdr.sh_entsize != sizeof(Elf64_Sym) || shdr.sh_size % sizeof(Elf64_Sym) || shdr.sh_link >= ehdr.e_shnum)
|
||||
throw std::runtime_error("invalid ELF dynsym layout");
|
||||
RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynsym");
|
||||
dynSymOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
dynSymSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
dynSymSectionIndex = i;
|
||||
@@ -333,7 +386,11 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
if (shdr.sh_type == kShtGnuHash) {
|
||||
gnuHashOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
gnuHashSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
gnuHashSymOffset = Read<std::uint32_t>(elf, gnuHashOffset + 4u);
|
||||
}
|
||||
if (shdr.sh_type == 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);
|
||||
@@ -345,6 +402,14 @@ 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);
|
||||
}
|
||||
|
||||
const std::size_t dynStrSectionLink = [&]() -> std::size_t {
|
||||
for (std::uint16_t i = 0u; i < ehdr.e_shnum; ++i) {
|
||||
@@ -359,6 +424,8 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
{
|
||||
const std::size_t shOffset = static_cast<std::size_t>(ehdr.e_shoff) + dynStrSectionLink * sizeof(Elf64_Shdr);
|
||||
const auto shdr = Read<Elf64_Shdr>(elf, shOffset);
|
||||
if (shdr.sh_type != 3) throw std::runtime_error("invalid ELF dynstr section type");
|
||||
RequireElfRange(elf, shdr.sh_offset, shdr.sh_size, "dynstr");
|
||||
dynStrOffset = static_cast<std::size_t>(shdr.sh_offset);
|
||||
dynStrSize = static_cast<std::size_t>(shdr.sh_size);
|
||||
}
|
||||
@@ -391,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);
|
||||
@@ -407,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);
|
||||
@@ -427,7 +498,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
const auto addName = [&](std::uint32_t oldOffset, std::string value) {
|
||||
const auto existing = std::find_if(
|
||||
names.begin(), names.end(),
|
||||
[&](const auto& entry) { return entry.first == oldOffset; }
|
||||
[&](const auto& entry) { return entry.first == oldOffset && entry.second == value; }
|
||||
);
|
||||
if (existing == names.end()) names.emplace_back(oldOffset, std::move(value));
|
||||
};
|
||||
@@ -463,8 +534,9 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
newDynStr.push_back(0u);
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> oldToNewOffset;
|
||||
for (const auto& name : names) oldToNewOffset.emplace_back(name.first, valueOffsets.at(name.second));
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> preservedToNewOffset;
|
||||
for (const auto offset : preservedOffsets)
|
||||
preservedToNewOffset.emplace_back(offset, valueOffsets.at(ReadCStr(origDynStr, offset)));
|
||||
|
||||
if (newDynStr.size() > dynStrSize) {
|
||||
throw std::runtime_error(
|
||||
@@ -475,25 +547,15 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
|
||||
newDynStr.resize(dynStrSize, 0u);
|
||||
|
||||
std::vector<std::string> newSymbolNames(symCount);
|
||||
for (std::size_t i = 1u; i < symCount; ++i) {
|
||||
const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym);
|
||||
const auto sym = Read<Elf64_Sym>(elf, symOffset);
|
||||
if (sym.st_name == 0u) continue;
|
||||
|
||||
const auto mapped = std::find_if(
|
||||
oldToNewOffset.begin(), oldToNewOffset.end(),
|
||||
[&](const auto& entry) { return entry.first == sym.st_name; }
|
||||
);
|
||||
if (mapped == oldToNewOffset.end()) continue;
|
||||
|
||||
newSymbolNames[i] = ReadCStr(newDynStr, mapped->second);
|
||||
Write(elf, symOffset + offsetof(Elf64_Sym, st_name), mapped->second);
|
||||
for (const auto& use : uses) {
|
||||
newSymbolNames[use.symIndex] = use.newValue;
|
||||
Write(elf, dynSymOffset + use.symIndex * sizeof(Elf64_Sym) + offsetof(Elf64_Sym, st_name), valueOffsets.at(use.newValue));
|
||||
}
|
||||
|
||||
std::memcpy(elf.data() + dynStrOffset, newDynStr.data(), newDynStr.size());
|
||||
|
||||
RemapDynamicNeededOffsets(elf, ehdr, oldToNewOffset);
|
||||
RemapVerneedOffsets(elf, ehdr, oldToNewOffset);
|
||||
RemapDynamicNeededOffsets(elf, ehdr, preservedToNewOffset);
|
||||
RemapVerneedOffsets(elf, ehdr, preservedToNewOffset);
|
||||
|
||||
if (gnuHashOffset != 0u) {
|
||||
const auto bucketCount = Read<std::uint32_t>(elf, gnuHashOffset);
|
||||
@@ -502,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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
#include <nid/ExportExclusions.hpp>
|
||||
#include <nid/ElfPatcher.hpp>
|
||||
#include <nid/PeNidPatcher.hpp>
|
||||
#include <nid/MachONidPatcher.hpp>
|
||||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <nid/NativePath.hpp>
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace Nid {
|
||||
|
||||
@@ -48,7 +53,12 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
|
||||
if (!directory.VirtualAddress || directory.Size < sizeof(PeExportDirectory)) throw std::runtime_error("missing reference PE exports");
|
||||
const auto sections = optional + optionalSize;
|
||||
RequireRange(binary, sections, std::uint64_t{sectionCount} * sizeof(PeSectionHeader));
|
||||
const auto headersSize = ReadValue<std::uint32_t>(binary, optional + 60);
|
||||
const auto mapRva = [&](std::uint32_t rva, std::uint64_t size) -> std::uint64_t {
|
||||
if (rva < headersSize && size <= headersSize - rva) {
|
||||
RequireRange(binary, rva, size);
|
||||
return rva;
|
||||
}
|
||||
for (std::uint16_t index = 0; index < sectionCount; ++index) {
|
||||
const auto section = ReadValue<PeSectionHeader>(binary, sections + std::uint64_t{index} * sizeof(PeSectionHeader));
|
||||
if (rva < section.VirtualAddress) continue;
|
||||
@@ -63,14 +73,20 @@ std::unordered_set<std::string> ReadPeExports(const std::vector<std::uint8_t>& b
|
||||
const auto table = ReadValue<PeExportDirectory>(binary, mapRva(directory.VirtualAddress, sizeof(PeExportDirectory)));
|
||||
if (!table.NumberOfNames) throw std::runtime_error("reference PE has no named exports");
|
||||
const auto names = mapRva(table.AddressOfNames, std::uint64_t{table.NumberOfNames} * 4);
|
||||
const auto ordinals = mapRva(table.AddressOfNameOrdinals, std::uint64_t{table.NumberOfNames} * 2);
|
||||
const auto functions = mapRva(table.AddressOfFunctions, std::uint64_t{table.NumberOfFunctions} * 4);
|
||||
std::unordered_set<std::string> result;
|
||||
for (std::uint32_t index = 0; index < table.NumberOfNames; ++index) {
|
||||
const auto ordinal = ReadValue<std::uint16_t>(binary, ordinals + std::uint64_t{index} * 2);
|
||||
if (ordinal >= table.NumberOfFunctions) throw std::runtime_error("invalid reference PE export ordinal");
|
||||
if (!ReadValue<std::uint32_t>(binary, functions + std::uint64_t{ordinal} * 4)) continue;
|
||||
const auto rva = ReadValue<std::uint32_t>(binary, names + std::uint64_t{index} * 4);
|
||||
const auto offset = mapRva(rva, 1);
|
||||
auto name = ReadName(binary, offset, binary.size() - offset);
|
||||
mapRva(rva, name.size() + 1);
|
||||
if (!result.insert(name).second) throw std::runtime_error("duplicate reference export: " + name);
|
||||
}
|
||||
if (result.empty()) throw std::runtime_error("reference PE has no named exports");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -100,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) {
|
||||
@@ -109,7 +143,7 @@ std::string NormalizeExportName(const std::string& name) {
|
||||
|
||||
std::unordered_set<std::string> ReadExportExclusions(const std::string& path) {
|
||||
if constexpr (std::endian::native != std::endian::little) throw std::runtime_error("export references require a little-endian host");
|
||||
std::ifstream input(path, std::ios::binary | std::ios::ate);
|
||||
std::ifstream input(Internal::NativePath(path), std::ios::binary | std::ios::ate);
|
||||
if (!input) throw std::runtime_error("cannot open export reference: " + path);
|
||||
const auto size = input.tellg();
|
||||
if (size <= 0) throw std::runtime_error("empty or unreadable export reference: " + path);
|
||||
@@ -121,6 +155,12 @@ std::unordered_set<std::string> ReadExportExclusions(const std::string& path) {
|
||||
exports = ReadPeExports(binary);
|
||||
} else if (binary.size() >= 4 && binary[0] == 0x7f && binary[1] == 'E' && binary[2] == 'L' && binary[3] == 'F') {
|
||||
exports = ReadElfExports(binary);
|
||||
} else if (binary.size() >= 4 && binary[0] == 0xcf && binary[1] == 0xfa && binary[2] == 0xed && binary[3] == 0xfe) {
|
||||
for (const auto& entry : ReadMachOExports(binary)) {
|
||||
if (entry.Name.size() < 2 || entry.Name[0] != '_') throw std::runtime_error("invalid reference Mach-O export: " + entry.Name);
|
||||
if (!exports.insert(entry.Name.substr(1)).second) throw std::runtime_error("duplicate reference export: " + entry.Name);
|
||||
}
|
||||
if (exports.empty()) throw std::runtime_error("reference Mach-O has no exports");
|
||||
} else {
|
||||
throw std::runtime_error("unrecognized export reference format: " + path);
|
||||
}
|
||||
@@ -134,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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
#include <nid/MachONidPatcher.hpp>
|
||||
#include <nid/NidResolver.hpp>
|
||||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Nid {
|
||||
|
||||
namespace {
|
||||
|
||||
using Internal::Read;
|
||||
using Internal::Write;
|
||||
|
||||
constexpr std::uint32_t kLcSegment64 = 0x19;
|
||||
constexpr std::uint32_t kLcCodeSignature = 0x1d;
|
||||
constexpr std::uint32_t kLcDyldInfo = 0x22;
|
||||
constexpr std::uint32_t kLcDyldInfoOnly = 0x80000022u;
|
||||
constexpr std::uint32_t kLcDyldExportsTrie = 0x80000033u;
|
||||
constexpr std::uint32_t kLcDyldChainedFixups = 0x80000034u;
|
||||
constexpr std::uint32_t kLcLoadDylib = 0x0c;
|
||||
constexpr std::uint32_t kLcLoadWeakDylib = 0x80000018u;
|
||||
constexpr std::uint32_t kLcReexportDylib = 0x8000001fu;
|
||||
constexpr std::uint32_t kLcLazyLoadDylib = 0x20;
|
||||
constexpr std::uint32_t kLcLoadUpwardDylib = 0x80000023u;
|
||||
|
||||
constexpr std::uint64_t kExportWeakDefinition = 0x04;
|
||||
constexpr std::uint64_t kExportReexport = 0x08;
|
||||
constexpr std::uint64_t kExportStubAndResolver = 0x10;
|
||||
|
||||
constexpr std::uint32_t kImportFormatPlain = 1;
|
||||
constexpr std::uint32_t kImportFormatAddend = 2;
|
||||
constexpr std::uint32_t kImportFormatAddend64 = 3;
|
||||
|
||||
constexpr std::uint64_t kPageSize = 0x1000;
|
||||
|
||||
struct MachHeader64 {
|
||||
std::uint32_t Magic;
|
||||
std::uint32_t CpuType;
|
||||
std::uint32_t CpuSubtype;
|
||||
std::uint32_t FileType;
|
||||
std::uint32_t CommandCount;
|
||||
std::uint32_t CommandBytes;
|
||||
std::uint32_t Flags;
|
||||
std::uint32_t Reserved;
|
||||
};
|
||||
|
||||
struct LoadCommand {
|
||||
std::uint32_t Cmd;
|
||||
std::uint32_t Size;
|
||||
};
|
||||
|
||||
struct SegmentCommand64 {
|
||||
std::uint32_t Cmd;
|
||||
std::uint32_t Size;
|
||||
char Name[16];
|
||||
std::uint64_t VmAddress;
|
||||
std::uint64_t VmSize;
|
||||
std::uint64_t FileOffset;
|
||||
std::uint64_t FileSize;
|
||||
std::uint32_t MaxProtection;
|
||||
std::uint32_t InitProtection;
|
||||
std::uint32_t SectionCount;
|
||||
std::uint32_t Flags;
|
||||
};
|
||||
|
||||
struct LinkeditDataCommand {
|
||||
std::uint32_t Cmd;
|
||||
std::uint32_t Size;
|
||||
std::uint32_t DataOffset;
|
||||
std::uint32_t DataSize;
|
||||
};
|
||||
|
||||
struct DyldInfoCommand {
|
||||
std::uint32_t Cmd;
|
||||
std::uint32_t Size;
|
||||
std::uint32_t RebaseOffset;
|
||||
std::uint32_t RebaseSize;
|
||||
std::uint32_t BindOffset;
|
||||
std::uint32_t BindSize;
|
||||
std::uint32_t WeakBindOffset;
|
||||
std::uint32_t WeakBindSize;
|
||||
std::uint32_t LazyBindOffset;
|
||||
std::uint32_t LazyBindSize;
|
||||
std::uint32_t ExportOffset;
|
||||
std::uint32_t ExportSize;
|
||||
};
|
||||
|
||||
struct ChainedFixupsHeader {
|
||||
std::uint32_t Version;
|
||||
std::uint32_t StartsOffset;
|
||||
std::uint32_t ImportsOffset;
|
||||
std::uint32_t SymbolsOffset;
|
||||
std::uint32_t ImportsCount;
|
||||
std::uint32_t ImportsFormat;
|
||||
std::uint32_t SymbolsFormat;
|
||||
};
|
||||
|
||||
void RequireRange(const std::vector<std::uint8_t>& binary, std::uint64_t offset, std::uint64_t size) {
|
||||
if (offset > binary.size() || size > binary.size() - offset) throw std::runtime_error("Mach-O range out of bounds");
|
||||
}
|
||||
|
||||
std::uint64_t ReadUleb(const std::vector<std::uint8_t>& data, std::size_t& cursor, std::size_t end) {
|
||||
std::uint64_t value = 0;
|
||||
unsigned shift = 0;
|
||||
for (;;) {
|
||||
if (cursor >= end) throw std::runtime_error("truncated uleb128 in export trie");
|
||||
const auto byte = data[cursor++];
|
||||
if (shift >= 64 || (shift == 63 && (byte & 0x7e) != 0)) throw std::runtime_error("uleb128 overflow in export trie");
|
||||
value |= std::uint64_t{byte & 0x7fu} << shift;
|
||||
if ((byte & 0x80) == 0) return value;
|
||||
shift += 7;
|
||||
}
|
||||
}
|
||||
|
||||
void WriteUleb(std::vector<std::uint8_t>& out, std::uint64_t value) {
|
||||
do {
|
||||
std::uint8_t byte = value & 0x7f;
|
||||
value >>= 7;
|
||||
if (value != 0) byte |= 0x80;
|
||||
out.push_back(byte);
|
||||
} while (value != 0);
|
||||
}
|
||||
|
||||
std::size_t UlebSize(std::uint64_t value) {
|
||||
std::size_t size = 1;
|
||||
while (value >>= 7) ++size;
|
||||
return size;
|
||||
}
|
||||
|
||||
std::string ReadTrieString(const std::vector<std::uint8_t>& data, std::size_t& cursor, std::size_t end) {
|
||||
const auto start = cursor;
|
||||
while (cursor < end && data[cursor] != 0) ++cursor;
|
||||
if (cursor >= end) throw std::runtime_error("unterminated string in export trie");
|
||||
std::string text(reinterpret_cast<const char*>(data.data() + start), cursor - start);
|
||||
++cursor;
|
||||
return text;
|
||||
}
|
||||
|
||||
struct Image {
|
||||
MachHeader64 Header {};
|
||||
std::vector<std::size_t> Commands;
|
||||
};
|
||||
|
||||
Image ParseImage(const std::vector<std::uint8_t>& binary) {
|
||||
Image image;
|
||||
image.Header = Read<MachHeader64>(binary, 0);
|
||||
if (image.Header.Magic != kMachOMagic64) throw std::runtime_error("not a 64-bit little-endian Mach-O image");
|
||||
RequireRange(binary, sizeof(MachHeader64), image.Header.CommandBytes);
|
||||
std::size_t cursor = sizeof(MachHeader64);
|
||||
const auto end = cursor + image.Header.CommandBytes;
|
||||
for (std::uint32_t index = 0; index < image.Header.CommandCount; ++index) {
|
||||
if (cursor + sizeof(LoadCommand) > end) throw std::runtime_error("load command out of bounds");
|
||||
const auto command = Read<LoadCommand>(binary, cursor);
|
||||
if (command.Size < sizeof(LoadCommand) || command.Size % 8 != 0 || command.Size > end - cursor) throw std::runtime_error("invalid load command size");
|
||||
image.Commands.push_back(cursor);
|
||||
cursor += command.Size;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
|
||||
struct TrieLocation {
|
||||
std::size_t OffsetField;
|
||||
std::size_t SizeField;
|
||||
};
|
||||
|
||||
std::optional<TrieLocation> FindTrie(const std::vector<std::uint8_t>& binary, const Image& image) {
|
||||
std::optional<TrieLocation> found;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(binary, offset).Cmd;
|
||||
std::optional<TrieLocation> here;
|
||||
if (cmd == kLcDyldExportsTrie) here = TrieLocation{offset + offsetof(LinkeditDataCommand, DataOffset), offset + offsetof(LinkeditDataCommand, DataSize)};
|
||||
else if (cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly) {
|
||||
if (Read<DyldInfoCommand>(binary, offset).ExportSize != 0)
|
||||
here = TrieLocation{offset + offsetof(DyldInfoCommand, ExportOffset), offset + offsetof(DyldInfoCommand, ExportSize)};
|
||||
}
|
||||
if (!here) continue;
|
||||
if (found) throw std::runtime_error("Mach-O image has more than one export trie");
|
||||
found = here;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
void ParseTrieNode(const std::vector<std::uint8_t>& data, std::size_t start, std::size_t end, std::size_t node, std::string& prefix, std::set<std::size_t>& visited, std::vector<MachOExport>& out) {
|
||||
if (node >= end - start) throw std::runtime_error("export trie node out of bounds");
|
||||
if (!visited.insert(node).second) throw std::runtime_error("export trie has a cycle");
|
||||
std::size_t cursor = start + node;
|
||||
const auto terminalSize = ReadUleb(data, cursor, end);
|
||||
if (terminalSize > end - cursor) throw std::runtime_error("export trie terminal out of bounds");
|
||||
const auto children = cursor + terminalSize;
|
||||
if (terminalSize != 0) {
|
||||
MachOExport entry;
|
||||
entry.Name = prefix;
|
||||
entry.Flags = ReadUleb(data, cursor, children);
|
||||
if (entry.Flags & kExportReexport) {
|
||||
entry.Other = ReadUleb(data, cursor, children);
|
||||
entry.ImportName = ReadTrieString(data, cursor, children);
|
||||
} else {
|
||||
entry.Address = ReadUleb(data, cursor, children);
|
||||
if (entry.Flags & kExportStubAndResolver) entry.Other = ReadUleb(data, cursor, children);
|
||||
}
|
||||
out.push_back(std::move(entry));
|
||||
}
|
||||
cursor = children;
|
||||
if (cursor >= end) throw std::runtime_error("export trie child count out of bounds");
|
||||
const auto childCount = data[cursor++];
|
||||
for (unsigned index = 0; index < childCount; ++index) {
|
||||
const auto label = ReadTrieString(data, cursor, end);
|
||||
const auto child = ReadUleb(data, cursor, end);
|
||||
const auto length = prefix.size();
|
||||
prefix += label;
|
||||
ParseTrieNode(data, start, end, static_cast<std::size_t>(child), prefix, visited, out);
|
||||
prefix.resize(length);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<MachOExport> ParseTrie(const std::vector<std::uint8_t>& binary, std::uint32_t offset, std::uint32_t size) {
|
||||
RequireRange(binary, offset, size);
|
||||
std::vector<MachOExport> exports;
|
||||
if (size == 0) return exports;
|
||||
std::string prefix;
|
||||
std::set<std::size_t> visited;
|
||||
ParseTrieNode(binary, offset, std::size_t{offset} + size, 0, prefix, visited, exports);
|
||||
return exports;
|
||||
}
|
||||
|
||||
class TrieBuilder {
|
||||
public:
|
||||
explicit TrieBuilder(const std::vector<MachOExport>& exports) {
|
||||
for (const auto& entry : exports) Insert(entry);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Build() {
|
||||
std::vector<Node*> order;
|
||||
Collect(&root, order);
|
||||
for (bool changed = true; changed;) {
|
||||
changed = false;
|
||||
std::uint64_t offset = 0;
|
||||
for (auto* node : order) {
|
||||
if (node->Offset != offset) {
|
||||
node->Offset = offset;
|
||||
changed = true;
|
||||
}
|
||||
offset += NodeSize(*node);
|
||||
}
|
||||
}
|
||||
std::vector<std::uint8_t> out;
|
||||
for (auto* node : order) {
|
||||
if (out.size() != node->Offset) throw std::runtime_error("export trie layout mismatch");
|
||||
const auto terminal = TerminalBytes(*node);
|
||||
WriteUleb(out, terminal.size());
|
||||
out.insert(out.end(), terminal.begin(), terminal.end());
|
||||
if (node->Children.size() > 255) throw std::runtime_error("export trie node has too many children");
|
||||
out.push_back(static_cast<std::uint8_t>(node->Children.size()));
|
||||
for (const auto& [label, child] : node->Children) {
|
||||
out.insert(out.end(), label.begin(), label.end());
|
||||
out.push_back(0);
|
||||
WriteUleb(out, child->Offset);
|
||||
}
|
||||
}
|
||||
while (out.size() % 8 != 0) out.push_back(0);
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
struct Node {
|
||||
std::vector<std::pair<std::string, std::unique_ptr<Node>>> Children;
|
||||
std::optional<MachOExport> Entry;
|
||||
std::uint64_t Offset = 0;
|
||||
};
|
||||
|
||||
Node root;
|
||||
|
||||
void Insert(const MachOExport& entry) {
|
||||
Node* node = &root;
|
||||
std::string_view rest = entry.Name;
|
||||
while (!rest.empty()) {
|
||||
bool descended = false;
|
||||
for (auto& [label, child] : node->Children) {
|
||||
std::size_t common = 0;
|
||||
while (common < label.size() && common < rest.size() && label[common] == rest[common]) ++common;
|
||||
if (common == 0) continue;
|
||||
if (common < label.size()) {
|
||||
auto middle = std::make_unique<Node>();
|
||||
middle->Children.emplace_back(label.substr(common), std::move(child));
|
||||
label.resize(common);
|
||||
child = std::move(middle);
|
||||
}
|
||||
node = child.get();
|
||||
rest.remove_prefix(common);
|
||||
descended = true;
|
||||
break;
|
||||
}
|
||||
if (!descended) {
|
||||
node->Children.emplace_back(std::string(rest), std::make_unique<Node>());
|
||||
node = node->Children.back().second.get();
|
||||
rest = {};
|
||||
}
|
||||
}
|
||||
if (node->Entry) throw std::runtime_error("duplicate patched export name: " + entry.Name);
|
||||
node->Entry = entry;
|
||||
}
|
||||
|
||||
static void Collect(Node* node, std::vector<Node*>& order) {
|
||||
order.push_back(node);
|
||||
for (auto& [label, child] : node->Children) Collect(child.get(), order);
|
||||
}
|
||||
|
||||
static std::vector<std::uint8_t> TerminalBytes(const Node& node) {
|
||||
std::vector<std::uint8_t> bytes;
|
||||
if (!node.Entry) return bytes;
|
||||
const auto& entry = *node.Entry;
|
||||
WriteUleb(bytes, entry.Flags);
|
||||
if (entry.Flags & kExportReexport) {
|
||||
WriteUleb(bytes, entry.Other);
|
||||
bytes.insert(bytes.end(), entry.ImportName.begin(), entry.ImportName.end());
|
||||
bytes.push_back(0);
|
||||
} else {
|
||||
WriteUleb(bytes, entry.Address);
|
||||
if (entry.Flags & kExportStubAndResolver) WriteUleb(bytes, entry.Other);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static std::uint64_t NodeSize(const Node& node) {
|
||||
const auto terminal = TerminalBytes(node).size();
|
||||
std::uint64_t size = UlebSize(terminal) + terminal + 1;
|
||||
for (const auto& [label, child] : node.Children) size += label.size() + 1 + UlebSize(child->Offset);
|
||||
return size;
|
||||
}
|
||||
};
|
||||
|
||||
std::string StripUnderscore(const std::string& name) {
|
||||
return !name.empty() && name[0] == '_' ? name.substr(1) : name;
|
||||
}
|
||||
|
||||
bool IsGuestFacingImport(const std::string& name) {
|
||||
using namespace Internal;
|
||||
const bool hasNidPostfix = name.size() >= kNidPostfixLen && name.compare(name.size() - kNidPostfixLen, kNidPostfixLen, kNidPostfix) == 0;
|
||||
const bool hasScePrefix = name.size() >= 3u &&
|
||||
std::tolower(static_cast<unsigned char>(name[0])) == 's' &&
|
||||
std::tolower(static_cast<unsigned char>(name[1])) == 'c' &&
|
||||
std::tolower(static_cast<unsigned char>(name[2])) == 'e';
|
||||
return hasNidPostfix || hasScePrefix;
|
||||
}
|
||||
|
||||
std::vector<bool> DylibOrdinalsArePrx(const std::vector<std::uint8_t>& binary, const Image& image) {
|
||||
std::vector<bool> result;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto command = Read<LoadCommand>(binary, offset);
|
||||
if (command.Cmd != kLcLoadDylib && command.Cmd != kLcLoadWeakDylib && command.Cmd != kLcReexportDylib && command.Cmd != kLcLazyLoadDylib && command.Cmd != kLcLoadUpwardDylib) continue;
|
||||
const auto nameOffset = Read<std::uint32_t>(binary, offset + 8);
|
||||
if (nameOffset >= command.Size) throw std::runtime_error("dylib name out of bounds");
|
||||
const auto name = Internal::ReadCStr(binary, offset + nameOffset);
|
||||
result.push_back(name.size() >= 4 && name.compare(name.size() - 4, 4, ".prx") == 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PatchChainedImports(std::vector<std::uint8_t>& binary, const Image& image) {
|
||||
std::optional<LinkeditDataCommand> fixups;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(binary, offset).Cmd;
|
||||
if (cmd == kLcDyldChainedFixups) fixups = Read<LinkeditDataCommand>(binary, offset);
|
||||
if ((cmd == kLcDyldInfo || cmd == kLcDyldInfoOnly)) {
|
||||
const auto info = Read<DyldInfoCommand>(binary, offset);
|
||||
if (info.BindSize != 0 || info.LazyBindSize != 0 || info.WeakBindSize != 0) throw std::runtime_error("Mach-O bind opcodes are not supported; link with chained fixups");
|
||||
}
|
||||
}
|
||||
if (!fixups || fixups->DataSize == 0) return;
|
||||
RequireRange(binary, fixups->DataOffset, fixups->DataSize);
|
||||
const auto base = std::size_t{fixups->DataOffset};
|
||||
const auto header = Read<ChainedFixupsHeader>(binary, base);
|
||||
if (header.Version != 0) throw std::runtime_error("unsupported chained fixups version");
|
||||
if (header.SymbolsFormat != 0) throw std::runtime_error("compressed chained fixup symbols are not supported");
|
||||
const std::size_t entrySize = header.ImportsFormat == kImportFormatPlain ? 4 : header.ImportsFormat == kImportFormatAddend ? 8 : header.ImportsFormat == kImportFormatAddend64 ? 16 : 0;
|
||||
if (entrySize == 0) throw std::runtime_error("unsupported chained import format");
|
||||
if (header.ImportsOffset > fixups->DataSize || std::uint64_t{header.ImportsCount} * entrySize > fixups->DataSize - header.ImportsOffset || header.SymbolsOffset > fixups->DataSize)
|
||||
throw std::runtime_error("chained imports out of bounds");
|
||||
const auto symbols = base + header.SymbolsOffset;
|
||||
const auto symbolsEnd = base + fixups->DataSize;
|
||||
const auto prx = DylibOrdinalsArePrx(binary, image);
|
||||
|
||||
struct Import { std::uint32_t NameOffset; std::uint32_t Ordinal; };
|
||||
std::vector<Import> imports;
|
||||
for (std::uint32_t index = 0; index < header.ImportsCount; ++index) {
|
||||
const auto at = base + header.ImportsOffset + index * entrySize;
|
||||
if (header.ImportsFormat == kImportFormatAddend64) {
|
||||
const auto raw = Read<std::uint64_t>(binary, at);
|
||||
imports.push_back({static_cast<std::uint32_t>(raw >> 32), static_cast<std::uint32_t>(raw & 0xffff)});
|
||||
} else {
|
||||
const auto raw = Read<std::uint32_t>(binary, at);
|
||||
imports.push_back({raw >> 9, raw & 0xff});
|
||||
}
|
||||
}
|
||||
std::set<std::uint32_t> nameOffsets;
|
||||
for (const auto& item : imports) nameOffsets.insert(item.NameOffset);
|
||||
std::set<std::uint32_t> rewritten;
|
||||
for (const auto& item : imports) {
|
||||
const bool special = header.ImportsFormat == kImportFormatAddend64 ? item.Ordinal >= 0xfff0 : item.Ordinal >= 0xf0;
|
||||
if (item.Ordinal == 0 || special || item.Ordinal > prx.size() || !prx[item.Ordinal - 1]) continue;
|
||||
if (rewritten.contains(item.NameOffset)) continue;
|
||||
const auto at = symbols + item.NameOffset;
|
||||
if (at >= symbolsEnd) throw std::runtime_error("chained import name out of bounds");
|
||||
const auto name = Internal::ReadCStr(binary, at);
|
||||
if (at + name.size() >= symbolsEnd) throw std::runtime_error("unterminated chained import name");
|
||||
const auto bare = StripUnderscore(name);
|
||||
if (bare == name || !IsGuestFacingImport(bare)) continue;
|
||||
const auto patched = "_" + ResolveOneName(bare);
|
||||
if (patched.size() > name.size()) throw std::runtime_error("import NID longer than original name: " + name);
|
||||
const auto inside = nameOffsets.upper_bound(item.NameOffset);
|
||||
if (inside != nameOffsets.end() && *inside <= item.NameOffset + name.size()) throw std::runtime_error("chained import names share storage: " + name);
|
||||
std::memcpy(binary.data() + at, patched.data(), patched.size());
|
||||
std::fill(binary.begin() + static_cast<std::ptrdiff_t>(at + patched.size()), binary.begin() + static_cast<std::ptrdiff_t>(at + name.size() + 1), 0);
|
||||
rewritten.insert(item.NameOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteTrie(std::vector<std::uint8_t>& binary, const Image& image, const TrieLocation& location, const std::vector<std::uint8_t>& trie) {
|
||||
const auto oldOffset = Read<std::uint32_t>(binary, location.OffsetField);
|
||||
const auto oldSize = Read<std::uint32_t>(binary, location.SizeField);
|
||||
if (trie.size() <= oldSize) {
|
||||
std::copy(trie.begin(), trie.end(), binary.begin() + oldOffset);
|
||||
std::fill(binary.begin() + oldOffset + static_cast<std::ptrdiff_t>(trie.size()), binary.begin() + oldOffset + oldSize, 0);
|
||||
Write(binary, location.SizeField, static_cast<std::uint32_t>(trie.size()));
|
||||
return;
|
||||
}
|
||||
std::optional<std::size_t> linkedit;
|
||||
for (const auto offset : image.Commands) {
|
||||
const auto cmd = Read<LoadCommand>(binary, offset).Cmd;
|
||||
if (cmd == kLcCodeSignature) throw std::runtime_error("signed Mach-O images cannot grow their export trie");
|
||||
if (cmd != kLcSegment64) continue;
|
||||
const auto segment = Read<SegmentCommand64>(binary, offset);
|
||||
if (std::strncmp(segment.Name, "__LINKEDIT", sizeof(segment.Name)) == 0) linkedit = offset;
|
||||
}
|
||||
if (!linkedit) throw std::runtime_error("Mach-O image has no __LINKEDIT segment");
|
||||
auto segment = Read<SegmentCommand64>(binary, *linkedit);
|
||||
if (segment.FileOffset + segment.FileSize != binary.size()) throw std::runtime_error("__LINKEDIT does not end the file");
|
||||
const auto newOffset = (binary.size() + 7) & ~std::size_t{7};
|
||||
if (newOffset + trie.size() > 0xffffffffu) throw std::runtime_error("export trie offset overflow");
|
||||
binary.resize(newOffset, 0);
|
||||
binary.insert(binary.end(), trie.begin(), trie.end());
|
||||
std::fill(binary.begin() + oldOffset, binary.begin() + oldOffset + oldSize, 0);
|
||||
segment.FileSize = binary.size() - segment.FileOffset;
|
||||
segment.VmSize = std::max(segment.VmSize, (segment.FileSize + kPageSize - 1) & ~(kPageSize - 1));
|
||||
Write(binary, *linkedit, segment);
|
||||
Write(binary, location.OffsetField, static_cast<std::uint32_t>(newOffset));
|
||||
Write(binary, location.SizeField, static_cast<std::uint32_t>(trie.size()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::vector<MachOExport> ReadMachOExports(const std::vector<std::uint8_t>& binary) {
|
||||
const auto image = ParseImage(binary);
|
||||
const auto location = FindTrie(binary, image);
|
||||
if (!location) return {};
|
||||
return ParseTrie(binary, Read<std::uint32_t>(binary, location->OffsetField), Read<std::uint32_t>(binary, location->SizeField));
|
||||
}
|
||||
|
||||
void MachONidPatcher::PatchNids(std::vector<std::uint8_t>& binary, const std::string& libraryName, const std::unordered_set<std::string>& excludedExports) const {
|
||||
const auto image = ParseImage(binary);
|
||||
const auto location = FindTrie(binary, image);
|
||||
if (!location) throw std::runtime_error("no export trie");
|
||||
auto exports = ParseTrie(binary, Read<std::uint32_t>(binary, location->OffsetField), Read<std::uint32_t>(binary, location->SizeField));
|
||||
if (exports.empty()) throw std::runtime_error("no exported names");
|
||||
|
||||
std::vector<std::string> names;
|
||||
names.reserve(exports.size());
|
||||
for (const auto& entry : exports) {
|
||||
if (entry.Name.empty() || entry.Name[0] != '_') throw std::runtime_error("exported name without a C underscore: " + entry.Name);
|
||||
names.push_back(entry.Name.substr(1));
|
||||
}
|
||||
const auto nidMap = ResolveNids(names, libraryName, excludedExports);
|
||||
for (std::size_t index = 0; index < exports.size(); ++index) {
|
||||
const auto& name = names[index];
|
||||
const bool marked = name.ends_with(Internal::kNidPostfix) || Internal::IsNidNoPatch(name) || Internal::IsNidNoPatchCut(name);
|
||||
if ((exports[index].Flags & kExportWeakDefinition) != 0 && !marked) continue;
|
||||
const auto found = nidMap.find(name);
|
||||
if (found == nidMap.end()) throw std::runtime_error("symbol not in nid map: " + name);
|
||||
exports[index].Name = "_" + found->second;
|
||||
}
|
||||
WriteTrie(binary, image, *location, TrieBuilder(exports).Build());
|
||||
PatchChainedImports(binary, ParseImage(binary));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <nid/BinaryPatcherFactory.hpp>
|
||||
#include <nid/ExportExclusions.hpp>
|
||||
#include <nid/NativePath.hpp>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
@@ -9,25 +10,42 @@
|
||||
namespace {
|
||||
|
||||
std::vector<std::uint8_t> _readFile(const std::string& path) {
|
||||
std::ifstream f(path, std::ios::binary);
|
||||
std::ifstream f(Nid::Internal::NativePath(path), std::ios::binary);
|
||||
if (!f) throw std::runtime_error("cannot open: " + path);
|
||||
return {std::istreambuf_iterator<char>(f), {}};
|
||||
}
|
||||
|
||||
void _writeFile(const std::string& path, const std::vector<std::uint8_t>& data) {
|
||||
std::ofstream f(path, std::ios::binary);
|
||||
std::ofstream f(Nid::Internal::NativePath(path), std::ios::binary);
|
||||
if (!f) throw std::runtime_error("cannot write: " + path);
|
||||
f.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
|
||||
f.close();
|
||||
if (!f) throw std::runtime_error("cannot write: " + path);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
int Run(int argc, char* argv[]) {
|
||||
if (argc < 3) {
|
||||
std::cerr << "Usage: nid_patcher <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;
|
||||
@@ -35,10 +53,10 @@ int main(int argc, char* argv[]) {
|
||||
bool hasReference = false;
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
const std::string argument = argv[i];
|
||||
if (argument == "--preserve-exports") {
|
||||
if (hasReference || i + 1 == argc) throw std::runtime_error("--preserve-exports requires exactly one reference library");
|
||||
if (argument == "--preserve-exports" || argument == "--preserve-export-list") {
|
||||
if (hasReference || i + 1 == argc) throw std::runtime_error(argument + " requires exactly one reference");
|
||||
hasReference = true;
|
||||
excludedExports = Nid::ReadExportExclusions(argv[++i]);
|
||||
excludedExports = argument == "--preserve-exports" ? Nid::ReadExportExclusions(argv[++i]) : Nid::ReadExportList(argv[++i]);
|
||||
} else {
|
||||
if (argument.starts_with("--")) throw std::runtime_error("unknown option: " + argument);
|
||||
paths.push_back(argument);
|
||||
@@ -65,3 +83,27 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t* argv[]) {
|
||||
try {
|
||||
std::vector<std::string> values;
|
||||
values.reserve(static_cast<std::size_t>(argc));
|
||||
for (int index = 0; index < argc; ++index) {
|
||||
const auto utf8 = std::filesystem::path(argv[index]).u8string();
|
||||
values.emplace_back(utf8.begin(), utf8.end());
|
||||
}
|
||||
std::vector<char*> arguments;
|
||||
arguments.reserve(values.size());
|
||||
for (auto& value : values) arguments.push_back(value.data());
|
||||
return Run(argc, arguments.data());
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << "FAIL: " << error.what() << '\n';
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
#else
|
||||
int main(int argc, char* argv[]) {
|
||||
return Run(argc, argv);
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,60 @@
|
||||
#include <nid/NidPatcherUtils.hpp>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition) {
|
||||
if (!condition) throw std::runtime_error("NID buffer validation failed");
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
void RequireRejected(TAction action) {
|
||||
try { action(); }
|
||||
catch (const std::runtime_error&) { return; }
|
||||
throw std::runtime_error("Malformed NID buffer was accepted");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
using namespace Nid::Internal;
|
||||
const std::vector<std::uint8_t> unterminated{'n', 'a', 'm', 'e'};
|
||||
RequireRejected([&] { ReadCStr(unterminated, 0); });
|
||||
RequireRejected([&] { ReadCStr(unterminated, 3); });
|
||||
const std::vector<std::uint8_t> strings{'a', 0, 'b', 'c', 0};
|
||||
Require(ReadCStr(strings, 0) == "a");
|
||||
Require(ReadCStr(strings, 1).empty());
|
||||
Require(ReadCStr(strings, 2) == "bc");
|
||||
Require(ReadCStr(strings, 4).empty());
|
||||
RequireRejected([&] { ReadCStr(strings, strings.size()); });
|
||||
RequireRejected([&] { ReadCStr({}, 0); });
|
||||
|
||||
std::vector<std::uint8_t> bytes(8, 0xa5);
|
||||
const std::uint64_t value = 0x1020304050607080;
|
||||
Write(bytes, 0, value);
|
||||
Require(Read<std::uint64_t>(bytes, 0) == value);
|
||||
Write<std::uint32_t>(bytes, 4, 0x12345678);
|
||||
Require(Read<std::uint32_t>(bytes, 4) == 0x12345678);
|
||||
const auto before = bytes;
|
||||
for (const auto offset : {std::size_t{5}, std::size_t{8}, std::size_t{9},
|
||||
std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::size_t>::max() - 1,
|
||||
std::numeric_limits<std::size_t>::max() - 3}) {
|
||||
RequireRejected([&] { Read<std::uint32_t>(bytes, offset); });
|
||||
RequireRejected([&] { Write<std::uint32_t>(bytes, offset, 0); });
|
||||
Require(bytes == before);
|
||||
}
|
||||
RequireRejected([&] { Read<std::uint64_t>(bytes, 1); });
|
||||
std::vector<std::uint8_t> empty;
|
||||
RequireRejected([&] { Read<std::uint8_t>(empty, 0); });
|
||||
RequireRejected([&] { Write<std::uint8_t>(empty, 0, 0); });
|
||||
std::cout << "NID buffer validation passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,48 @@
|
||||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, SHOFF
|
||||
|
||||
|
||||
class ElfLayoutTests(unittest.TestCase):
|
||||
def test_declared_layout_must_match_supported_tables(self):
|
||||
changes = ((5, "B", 2), (0x3a, "H", 32),
|
||||
(SHOFF + 64 + 56, "Q", 8),
|
||||
(SHOFF + 64 + 32, "Q", len(symbols(False)) * 24 + 1),
|
||||
(SHOFF + 128 + 4, "I", 1))
|
||||
for offset, kind, value in changes:
|
||||
with self.subTest(offset=offset, value=value):
|
||||
image = build_image(symbols(False), False)
|
||||
struct.pack_into("<" + kind, image, offset, value)
|
||||
self.reject(image)
|
||||
|
||||
def test_table_ranges_and_links_are_checked(self):
|
||||
changes = ((0x28, "Q", 0x10000),
|
||||
(SHOFF + 64 + 24, "Q", 0xff0),
|
||||
(SHOFF + 64 + 40, "I", 100),
|
||||
(SHOFF + 128 + 24, "Q", 0xff0),
|
||||
(SHOFF + 128 + 32, "Q", 0xffffffffffffffff))
|
||||
for offset, kind, value in changes:
|
||||
with self.subTest(offset=offset, value=value):
|
||||
image = build_image(symbols(False), False)
|
||||
struct.pack_into("<" + kind, image, offset, value)
|
||||
self.reject(image)
|
||||
|
||||
def reject(self, image):
|
||||
result, after = patch(PATCHER, bytes(image), "layout.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertRegex(result.stderr.decode(), "ELF|dynsym|dynstr")
|
||||
self.assertEqual(after, image)
|
||||
|
||||
def test_valid_supported_layout_still_patches(self):
|
||||
image = build_image(symbols(False), False)
|
||||
result, after = patch(PATCHER, bytes(image), "valid-layout.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertNotEqual(after, image)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
@@ -0,0 +1,157 @@
|
||||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, HASH_OFFSET, SHOFF, read_dynsym, gnu_hash, sections
|
||||
|
||||
|
||||
def sysv_hash(name):
|
||||
value = 0
|
||||
for byte in name.encode("utf-8"):
|
||||
value = ((value << 4) + byte) & 0xFFFFFFFF
|
||||
high = value & 0xF0000000
|
||||
value = (value ^ (high >> 24)) & ~high
|
||||
return value
|
||||
|
||||
|
||||
def sysv_lookup(image, name):
|
||||
table = next(section for section in sections(image) if section[1] == 5)
|
||||
buckets, chains = struct.unpack_from("<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):
|
||||
def image(self, buckets=4, bloom=1, shift=6, symoffset=3):
|
||||
image = build_image(symbols(False), False)
|
||||
count = len(symbols(False))
|
||||
struct.pack_into("<IIII", image, HASH_OFFSET, buckets, symoffset, bloom, shift)
|
||||
size = 16 + 8 * bloom + 4 * buckets + 4 * max(0, count - symoffset)
|
||||
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
|
||||
return image
|
||||
|
||||
def test_non_power_of_two_bloom_is_rejected(self):
|
||||
self.reject(self.image(bloom=3))
|
||||
|
||||
def test_hash_shift_outside_word_is_rejected(self):
|
||||
self.reject(self.image(shift=32))
|
||||
|
||||
def test_unsafe_geometry_is_rejected(self):
|
||||
for fields in ({"bloom": 0}, {"buckets": 0}, {"symoffset": 100}):
|
||||
with self.subTest(fields=fields):
|
||||
self.reject(self.image(**fields))
|
||||
|
||||
def test_declared_section_extent_is_checked(self):
|
||||
for size in (12, 16, 0x10000):
|
||||
with self.subTest(size=size):
|
||||
image = self.image()
|
||||
struct.pack_into("<Q", image, SHOFF + 3 * 64 + 32, size)
|
||||
self.reject(image)
|
||||
|
||||
def reject(self, image):
|
||||
result, after = patch(PATCHER, bytes(image), "geometry.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertIn(b"gnu.hash", result.stderr)
|
||||
self.assertEqual(after, image)
|
||||
|
||||
def test_rebuilt_hash_finds_every_export(self):
|
||||
image = self.image(bloom=2)
|
||||
result, after = patch(PATCHER, image, "valid.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
entries = read_dynsym(after)
|
||||
buckets, first, bloom, shift = struct.unpack_from("<4I", after, HASH_OFFSET)
|
||||
bucket_offset = HASH_OFFSET + 16 + bloom * 8
|
||||
chain_offset = bucket_offset + buckets * 4
|
||||
for index in range(first, len(entries)):
|
||||
name = entries[index][0]
|
||||
hashed = gnu_hash(name)
|
||||
word = struct.unpack_from("<Q", after, HASH_OFFSET + 16 + ((hashed // 64) % bloom) * 8)[0]
|
||||
self.assertTrue(word & (1 << (hashed % 64)))
|
||||
self.assertTrue(word & (1 << ((hashed >> shift) % 64)))
|
||||
start = struct.unpack_from("<I", after, bucket_offset + (hashed % buckets) * 4)[0]
|
||||
found = False
|
||||
for candidate in range(start, len(entries)):
|
||||
value = struct.unpack_from("<I", after, chain_offset + (candidate - first) * 4)[0]
|
||||
if (value | 1) == (hashed | 1) and entries[candidate][0] == name:
|
||||
found = True
|
||||
if value & 1:
|
||||
break
|
||||
self.assertTrue(found, name)
|
||||
|
||||
def test_empty_hash_with_no_buckets_is_supported(self):
|
||||
image = self.image(buckets=0, symoffset=len(symbols(False)))
|
||||
result, after = patch(PATCHER, image, "empty.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(struct.unpack_from("<I", after, HASH_OFFSET)[0], 0)
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,284 @@
|
||||
"""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
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1, 0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30])
|
||||
BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-"
|
||||
|
||||
LC_SEGMENT_64 = 0x19
|
||||
LC_LOAD_DYLIB = 0x0C
|
||||
LC_CODE_SIGNATURE = 0x1D
|
||||
LC_DYLD_INFO_ONLY = 0x80000022
|
||||
LC_DYLD_EXPORTS_TRIE = 0x80000033
|
||||
LC_DYLD_CHAINED_FIXUPS = 0x80000034
|
||||
EXPORT_WEAK = 0x04
|
||||
PAGE = 0x1000
|
||||
|
||||
|
||||
def nid(name):
|
||||
digest = hashlib.sha1(name.encode() + SUFFIX).digest()
|
||||
reversed_bytes = digest[:8][::-1]
|
||||
text = ""
|
||||
for index in (0, 3):
|
||||
triple = (reversed_bytes[index] << 16) | (reversed_bytes[index + 1] << 8) | reversed_bytes[index + 2]
|
||||
text += BASE64[(triple >> 18) & 63] + BASE64[(triple >> 12) & 63] + BASE64[(triple >> 6) & 63] + BASE64[triple & 63]
|
||||
tail = (reversed_bytes[6] << 16) | (reversed_bytes[7] << 8)
|
||||
return text + BASE64[(tail >> 18) & 63] + BASE64[(tail >> 12) & 63] + BASE64[(tail >> 6) & 63]
|
||||
|
||||
|
||||
def uleb(value):
|
||||
out = bytearray()
|
||||
while True:
|
||||
byte = value & 0x7F
|
||||
value >>= 7
|
||||
out.append(byte | (0x80 if value else 0))
|
||||
if not value:
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def read_uleb(data, position):
|
||||
value = shift = 0
|
||||
while True:
|
||||
byte = data[position]
|
||||
position += 1
|
||||
value |= (byte & 0x7F) << shift
|
||||
if not byte & 0x80:
|
||||
return value, position
|
||||
shift += 7
|
||||
|
||||
|
||||
def build_trie(entries):
|
||||
offsets = [0] * len(entries)
|
||||
for _ in range(8):
|
||||
root = bytearray(b"\0") + bytes([len(entries)])
|
||||
for (name, _, _), offset in zip(entries, offsets):
|
||||
root += name.encode() + b"\0" + uleb(offset)
|
||||
nodes = []
|
||||
for name, flags, address in entries:
|
||||
terminal = uleb(flags) + uleb(address)
|
||||
nodes.append(uleb(len(terminal)) + terminal + b"\0")
|
||||
position, expected = len(root), []
|
||||
for node in nodes:
|
||||
expected.append(position)
|
||||
position += len(node)
|
||||
if expected == offsets:
|
||||
break
|
||||
offsets = expected
|
||||
trie = bytes(root) + b"".join(nodes)
|
||||
return trie + bytes(-len(trie) % 8)
|
||||
|
||||
|
||||
def parse_trie(data, start, size):
|
||||
found = {}
|
||||
|
||||
def walk(node, prefix):
|
||||
position = start + node
|
||||
terminal, position = read_uleb(data, position)
|
||||
children = position + terminal
|
||||
if terminal:
|
||||
flags, position = read_uleb(data, position)
|
||||
address, position = read_uleb(data, position)
|
||||
found[prefix] = (flags, address)
|
||||
count = data[children]
|
||||
position = children + 1
|
||||
for _ in range(count):
|
||||
end = data.index(0, position)
|
||||
label = data[position:end].decode()
|
||||
child, position = read_uleb(data, end + 1)
|
||||
walk(child, prefix + label)
|
||||
|
||||
if size:
|
||||
walk(0, "")
|
||||
return found
|
||||
|
||||
|
||||
def build_fixups(imports):
|
||||
symbols = bytearray(b"\0")
|
||||
entries = []
|
||||
for name, ordinal in imports:
|
||||
entries.append(ordinal | (len(symbols) << 9))
|
||||
symbols += name.encode() + b"\0"
|
||||
imports_offset = 32
|
||||
header = struct.pack("<7I", 0, 28, imports_offset, imports_offset + 4 * len(imports), len(imports), 1, 0)
|
||||
blob = header + struct.pack("<I", 0) + b"".join(struct.pack("<I", entry) for entry in entries) + bytes(symbols)
|
||||
return blob + bytes(-len(blob) % 8)
|
||||
|
||||
|
||||
def dylib_command(path):
|
||||
name = path.encode() + b"\0"
|
||||
size = (24 + len(name) + 7) & ~7
|
||||
return struct.pack("<IIIIII", LC_LOAD_DYLIB, size, 24, 2, 0x10000, 0x10000) + name.ljust(size - 24, b"\0")
|
||||
|
||||
|
||||
def build_dylib(exports, imports, signed=False, bind_opcodes=False):
|
||||
trie = build_trie(exports)
|
||||
fixups = build_fixups(imports)
|
||||
linkedit = bytearray(trie + fixups)
|
||||
trie_command = (struct.pack("<IIII", LC_DYLD_EXPORTS_TRIE, 16, PAGE, len(trie)))
|
||||
fixups_command = struct.pack("<IIII", LC_DYLD_CHAINED_FIXUPS, 16, PAGE + len(trie), len(fixups))
|
||||
if bind_opcodes:
|
||||
trie_command = struct.pack("<IIIIIIIIIIII", LC_DYLD_INFO_ONLY, 48, 0, 0, PAGE, 8, 0, 0, 0, 0, PAGE, len(trie))
|
||||
extra = [trie_command, fixups_command, dylib_command("@rpath/libkernel.prx"), dylib_command("/usr/lib/libSystem.B.dylib")]
|
||||
if signed:
|
||||
extra.append(struct.pack("<IIII", LC_CODE_SIGNATURE, 16, PAGE + len(linkedit), 0))
|
||||
text = struct.pack("<II16sQQQQiiII", LC_SEGMENT_64, 72, b"__TEXT", 0, PAGE, 0, PAGE, 5, 5, 0, 0)
|
||||
link = struct.pack("<II16sQQQQiiII", LC_SEGMENT_64, 72, b"__LINKEDIT", PAGE, PAGE, PAGE, len(linkedit), 1, 1, 0, 0)
|
||||
body = text + link + b"".join(extra)
|
||||
header = struct.pack("<IIIIIIII", 0xFEEDFACF, 0x01000007, 3, 6, 2 + len(extra), len(body), 0, 0)
|
||||
image = bytearray(header + body)
|
||||
image += bytes(PAGE - len(image))
|
||||
image += linkedit
|
||||
return bytes(image)
|
||||
|
||||
|
||||
def command_fields(data):
|
||||
offset, found = 32, {}
|
||||
for _ in range(struct.unpack_from("<I", data, 16)[0]):
|
||||
cmd, size = struct.unpack_from("<II", data, offset)
|
||||
found.setdefault(cmd, []).append(offset)
|
||||
offset += size
|
||||
return found
|
||||
|
||||
|
||||
def segment(data, name):
|
||||
for offset in command_fields(data)[LC_SEGMENT_64]:
|
||||
fields = struct.unpack_from("<16sQQQQ", data, offset + 8)
|
||||
if fields[0].rstrip(b"\0").decode() == name:
|
||||
return offset, fields[1:]
|
||||
raise AssertionError(name)
|
||||
|
||||
|
||||
def chained_imports(data):
|
||||
offset = command_fields(data)[LC_DYLD_CHAINED_FIXUPS][0]
|
||||
base, size = struct.unpack_from("<II", data, offset + 8)
|
||||
_, _, imports_offset, symbols_offset, count, _, _ = struct.unpack_from("<7I", data, base)
|
||||
result = []
|
||||
for index in range(count):
|
||||
raw, = struct.unpack_from("<I", data, base + imports_offset + 4 * index)
|
||||
start = base + symbols_offset + (raw >> 9)
|
||||
result.append((data[start:data.index(0, start)].decode(), raw & 0xFF))
|
||||
return result
|
||||
|
||||
|
||||
def patch(patcher, directory, name, image):
|
||||
path = Path(directory) / name
|
||||
path.write_bytes(image)
|
||||
result = subprocess.run([str(patcher), "libtest", str(path)], capture_output=True, text=True, timeout=30)
|
||||
return result, path
|
||||
|
||||
|
||||
def exports_of(data):
|
||||
offset = command_fields(data)[LC_DYLD_EXPORTS_TRIE][0]
|
||||
start, size = struct.unpack_from("<II", data, offset + 8)
|
||||
return parse_trie(data, start, size)
|
||||
|
||||
|
||||
def check_shrinking(patcher, directory):
|
||||
exports = [("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108), ("_weak_inline", EXPORT_WEAK, 0x110)]
|
||||
imports = [("_malloc", 2), ("_puts_nid_postfix", 1), ("_sceKernelFoo", 1), ("_helper", 1), ("_exit_nid_postfix", 2)]
|
||||
result, path = patch(patcher, directory, "shrink.dylib", build_dylib(exports, imports))
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
data = path.read_bytes()
|
||||
assert exports_of(data) == {"_" + nid("puts"): (0, 0x100), "_" + nid("exit"): (0, 0x108), "_weak_inline": (EXPORT_WEAK, 0x110)}, exports_of(data)
|
||||
assert chained_imports(data) == [("_malloc", 2), ("_" + nid("puts"), 1), ("_" + nid("sceKernelFoo"), 1), ("_helper", 1), ("_exit_nid_postfix", 2)], chained_imports(data)
|
||||
assert len(data) == len(build_dylib(exports, imports)), "the image changed size"
|
||||
|
||||
|
||||
def check_growing(patcher, directory):
|
||||
original = build_dylib([("_ab", 0, 0x100), ("_cd", 0, 0x108)], [])
|
||||
result, path = patch(patcher, directory, "grow.dylib", original)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
data = path.read_bytes()
|
||||
assert exports_of(data) == {"_" + nid("ab"): (0, 0x100), "_" + nid("cd"): (0, 0x108)}, exports_of(data)
|
||||
offset, (vmaddr, vmsize, fileoff, filesize) = segment(data, "__LINKEDIT")
|
||||
assert fileoff + filesize == len(data) and vmsize >= filesize and len(data) > len(original), (fileoff, filesize, len(data))
|
||||
trie_offset, trie_size = struct.unpack_from("<II", data, command_fields(data)[LC_DYLD_EXPORTS_TRIE][0] + 8)
|
||||
assert trie_offset >= len(original) and trie_offset + trie_size <= len(data) and trie_offset % 8 == 0, (trie_offset, trie_size)
|
||||
|
||||
|
||||
def check_preserved(patcher, directory):
|
||||
reference = Path(directory) / "reference.dylib"
|
||||
reference.write_bytes(build_dylib([("_puts", 0, 0x100)], []))
|
||||
path = Path(directory) / "preserve.dylib"
|
||||
path.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], []))
|
||||
result = subprocess.run([str(patcher), "libtest", "--preserve-exports", str(reference), str(path)], capture_output=True, text=True, timeout=30)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes())
|
||||
|
||||
|
||||
def write_export_list(patcher, reference, listing):
|
||||
return subprocess.run([str(patcher), "--write-export-list", str(reference), str(listing)], capture_output=True, text=True, timeout=30)
|
||||
|
||||
|
||||
def check_export_list(patcher, directory):
|
||||
reference = Path(directory) / "listed.dylib"
|
||||
listing = Path(directory) / "listed.exports"
|
||||
reference.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit", 0, 0x108)], []))
|
||||
result = write_export_list(patcher, reference, listing)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
assert listing.read_bytes() == b"exit\nputs\n", listing.read_bytes()
|
||||
|
||||
os.utime(listing, ns=(1_000_000_000_000_000_000, 1_000_000_000_000_000_000))
|
||||
reference.write_bytes(build_dylib([("_exit", 0, 0x110), ("_puts_nid_postfix", 0, 0x118)], []))
|
||||
result = write_export_list(patcher, reference, listing)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
assert listing.stat().st_mtime_ns == 1_000_000_000_000_000_000, "an unchanged export list was rewritten"
|
||||
|
||||
reference.write_bytes(build_dylib([("_puts", 0, 0x100)], []))
|
||||
result = write_export_list(patcher, reference, listing)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
assert listing.read_bytes() == b"puts\n", listing.read_bytes()
|
||||
assert listing.stat().st_mtime_ns != 1_000_000_000_000_000_000, "a changed export list kept its time"
|
||||
|
||||
path = Path(directory) / "listed-preserve.dylib"
|
||||
path.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], []))
|
||||
result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30)
|
||||
assert result.returncode == 0, (result.stdout, result.stderr)
|
||||
assert exports_of(path.read_bytes()) == {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)}, exports_of(path.read_bytes())
|
||||
|
||||
malformed_lists = [
|
||||
("unterminated", b"puts", "malformed export list"),
|
||||
("blank", b"puts\n\nexit\n", "empty name in export list"),
|
||||
("twice", b"puts\nputs\n", "duplicate name in export list"),
|
||||
("empty", b"", "malformed export list"),
|
||||
]
|
||||
for name, content, message in malformed_lists:
|
||||
malformed = Path(directory) / (name + ".exports")
|
||||
malformed.write_bytes(content)
|
||||
target = Path(directory) / (name + ".dylib")
|
||||
target.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], []))
|
||||
result = subprocess.run([str(patcher), "libtest", "--preserve-export-list", str(malformed), str(target)], capture_output=True, text=True, timeout=30)
|
||||
assert result.returncode != 0 and message in result.stdout + result.stderr, (name, result.stdout, result.stderr)
|
||||
result = subprocess.run([str(patcher), "libtest", "--preserve-exports", str(reference), "--preserve-export-list", str(listing), str(path)], capture_output=True, text=True, timeout=30)
|
||||
assert result.returncode != 0 and "exactly one reference" in result.stdout + result.stderr, (result.stdout, result.stderr)
|
||||
|
||||
|
||||
def check_refusals(patcher, directory):
|
||||
growing = [("_ab", 0, 0x100), ("_cd", 0, 0x108)]
|
||||
result, _ = patch(patcher, directory, "signed.dylib", build_dylib(growing, [], signed=True))
|
||||
assert result.returncode != 0 and "signed Mach-O images cannot grow their export trie" in result.stdout + result.stderr, (result.stdout, result.stderr)
|
||||
result, _ = patch(patcher, directory, "opcodes.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], [], bind_opcodes=True))
|
||||
assert result.returncode != 0 and "bind opcodes are not supported" in result.stdout + result.stderr, (result.stdout, result.stderr)
|
||||
result, _ = patch(patcher, directory, "long.dylib", build_dylib([("_puts_nid_postfix", 0, 0x100)], [("_sceXy", 1)]))
|
||||
assert result.returncode != 0 and "import NID longer than original name" in result.stdout + result.stderr, (result.stdout, result.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
patcher = Path(sys.argv[1]).resolve()
|
||||
with tempfile.TemporaryDirectory(prefix="anyps5-macho-nid-") as directory:
|
||||
check_shrinking(patcher, directory)
|
||||
check_growing(patcher, directory)
|
||||
check_preserved(patcher, directory)
|
||||
check_export_list(patcher, directory)
|
||||
check_refusals(patcher, directory)
|
||||
print("NID patcher Mach-O tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,83 @@
|
||||
import subprocess
|
||||
import signal
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_macho_nids import build_dylib, exports_of, nid
|
||||
|
||||
try:
|
||||
import resource
|
||||
except ImportError:
|
||||
resource = None
|
||||
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
|
||||
|
||||
class NativePathTests(unittest.TestCase):
|
||||
def invoke(self, *arguments):
|
||||
return subprocess.run([str(PATCHER), "libtest", *map(str, arguments)],
|
||||
capture_output=True, encoding="utf-8", errors="replace", timeout=10)
|
||||
|
||||
def test_non_ascii_directory_and_filename_patch_the_intended_file(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary) / "\u6e2c\u8a66"
|
||||
root.mkdir()
|
||||
file = root / "\u043c\u043e\u0434\u0443\u043b\u044c.prx"
|
||||
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100)], []))
|
||||
result = self.invoke(file)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(exports_of(file.read_bytes()), {"_" + nid("puts"): (0, 0x100)})
|
||||
self.assertIn(str(file), result.stdout)
|
||||
self.assertEqual(list(root.iterdir()), [file])
|
||||
|
||||
def test_non_ascii_export_reference_preserves_the_named_export(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
reference = root / "\u6e2c\u8a66-reference.prx"
|
||||
original = build_dylib([("_puts", 0, 0x100)], [])
|
||||
reference.write_bytes(original)
|
||||
file = root / "target.prx"
|
||||
file.write_bytes(build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], []))
|
||||
result = self.invoke("--preserve-exports", reference, file)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(exports_of(file.read_bytes()), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
|
||||
self.assertEqual(reference.read_bytes(), original)
|
||||
|
||||
def test_missing_unicode_input_does_not_modify_another_file(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
existing = root / "existing.prx"
|
||||
existing.write_bytes(b"unchanged")
|
||||
missing = root / "\u6e2c\u8a66-missing.prx"
|
||||
result = self.invoke(missing)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
self.assertIn(str(missing), result.stderr)
|
||||
self.assertFalse(missing.exists())
|
||||
self.assertEqual(existing.read_bytes(), b"unchanged")
|
||||
|
||||
@unittest.skipUnless(resource is not None and hasattr(signal, "SIGXFSZ"), "requires POSIX file size limits")
|
||||
def test_failed_file_write_is_reported_without_success(self):
|
||||
for limit in (0, 1024):
|
||||
with self.subTest(limit=limit), tempfile.TemporaryDirectory() as temporary:
|
||||
file = Path(temporary) / "library.prx"
|
||||
original = build_dylib([("_puts_nid_postfix", 0, 0x100)], [])
|
||||
file.write_bytes(original)
|
||||
|
||||
def restrict_output():
|
||||
signal.signal(signal.SIGXFSZ, signal.SIG_IGN)
|
||||
resource.setrlimit(resource.RLIMIT_FSIZE, (limit, limit))
|
||||
|
||||
result = subprocess.run([str(PATCHER), "libtest", str(file)], capture_output=True,
|
||||
encoding="utf-8", errors="replace", timeout=10,
|
||||
preexec_fn=restrict_output)
|
||||
self.assertEqual(file.stat().st_size, limit)
|
||||
self.assertEqual(result.returncode, 2, result.stdout + result.stderr)
|
||||
self.assertIn("cannot write", result.stderr)
|
||||
self.assertIn(str(file), result.stderr)
|
||||
self.assertNotIn("OK:", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,246 @@
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_macho_nids import build_dylib, exports_of, nid
|
||||
|
||||
|
||||
def reference(*, headers=False, address=0x2000, ordinal=0):
|
||||
image = bytearray(1280)
|
||||
image[:2] = b"MZ"
|
||||
struct.pack_into("<I", image, 0x3c, 0x40)
|
||||
struct.pack_into("<IHHIIIHH", image, 0x40, 0x4550, 0x8664, 2, 0, 0, 0, 240, 0x2022)
|
||||
optional = 0x58
|
||||
struct.pack_into("<H", image, optional, 0x20b)
|
||||
struct.pack_into("<I", image, optional + 60, 512)
|
||||
struct.pack_into("<I", image, optional + 108, 16)
|
||||
start, rva = (416, 416) if headers else (512, 0x1000)
|
||||
struct.pack_into("<II", image, optional + 112, rva, 80)
|
||||
for index, (name, va, offset, size) in enumerate(((b".edata", 0x1000, 512, 256), (b".text", 0x2000, 768, 512))):
|
||||
struct.pack_into("<8sIIIIIIHHI", image, optional + 240 + index * 40,
|
||||
name, size, va, size, offset, 0, 0, 0, 0, 0x40000040)
|
||||
struct.pack_into("<IIHHIIIIIII", image, start, 0, 0, 0, 0, rva + 50, 1, 1, 1, rva + 40, rva + 44, rva + 48)
|
||||
struct.pack_into("<IIH", image, start + 40, address, rva + 50, ordinal)
|
||||
name = b"puts_nid_postfix\0"
|
||||
image[start + 50:start + 50 + len(name)] = name
|
||||
return image
|
||||
|
||||
|
||||
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:
|
||||
root = Path(tmp)
|
||||
source, ref = root / "target.dylib", root / "reference.bin"
|
||||
original = build_dylib([("_puts_nid_postfix", 0, 0x100), ("_exit_nid_postfix", 0, 0x108)], [])
|
||||
source.write_bytes(original)
|
||||
ref.write_bytes(data)
|
||||
result = subprocess.run([str(PATCHER), "libc", str(source), "--preserve-exports", str(ref)], capture_output=True, text=True)
|
||||
self.assertEqual(ref.read_bytes(), data)
|
||||
return result, source.read_bytes(), original
|
||||
|
||||
def test_header_resident_export_tables(self):
|
||||
result, image, _ = self.patch_with_reference(reference(headers=True))
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(exports_of(image), {"_puts_nid_postfix": (0, 0x100), "_" + nid("exit"): (0, 0x108)})
|
||||
|
||||
def test_section_resident_export_tables(self):
|
||||
result, image, _ = self.patch_with_reference(reference())
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("_puts_nid_postfix", exports_of(image))
|
||||
|
||||
def test_null_function_slot_does_not_preserve_a_name(self):
|
||||
result, image, original = self.patch_with_reference(reference(address=0))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("no named exports", result.stdout + result.stderr)
|
||||
self.assertEqual(image, original)
|
||||
|
||||
def test_invalid_name_ordinal_is_rejected_without_changing_target(self):
|
||||
result, image, original = self.patch_with_reference(reference(ordinal=1))
|
||||
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("ordinal", result.stdout + result.stderr)
|
||||
self.assertEqual(image, original)
|
||||
|
||||
|
||||
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,58 @@
|
||||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSYM_OFFSET, read_dynsym, sections, DEFINITIONS
|
||||
|
||||
|
||||
class SharedStringTests(unittest.TestCase):
|
||||
def test_dynamic_names_keep_original_string_when_export_shares_offset(self):
|
||||
for tag in (1, 14, 15, 29):
|
||||
with self.subTest(tag=tag):
|
||||
data = build_image(symbols(False), False)
|
||||
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 6, 2, 0, 0x580, 32, 2, 0, 8, 16)
|
||||
struct.pack_into("<qQqQ", data, 0x580, tag, name_offset, 0, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "shared-dynamic.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
new_offset = struct.unpack_from("<Q", after, 0x588)[0]
|
||||
start = strings[4] + new_offset
|
||||
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
|
||||
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
|
||||
self.assertNotEqual(renamed, DEFINITIONS[0][0])
|
||||
|
||||
def test_version_requirement_strings_keep_original_names(self):
|
||||
data = build_image(symbols(False), False)
|
||||
name_offset = struct.unpack_from("<I", data, DYNSYM_OFFSET + 3 * 24)[0]
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, 0x580, 32, 2, 1, 8, 0)
|
||||
struct.pack_into("<HHIII", data, 0x580, 1, 1, name_offset, 16, 0)
|
||||
struct.pack_into("<IHHII", data, 0x590, 0, 0, 2, name_offset, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "shared-version.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for field in (0x584, 0x598):
|
||||
offset = struct.unpack_from("<I", after, field)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)].decode(), DEFINITIONS[0][0])
|
||||
renamed = next(entry[0] for entry in read_dynsym(after) if entry[2] == DEFINITIONS[0][2])
|
||||
self.assertNotEqual(renamed, DEFINITIONS[0][0])
|
||||
|
||||
def test_local_symbol_keeps_name_shared_with_renamed_global(self):
|
||||
entries = symbols(False)
|
||||
entries[1] = (DEFINITIONS[0][0], 1, 9, 2, 0x1500)
|
||||
data = build_image(entries, False)
|
||||
data[DYNSYM_OFFSET + 24 + 4] = 2
|
||||
result, after = patch(PATCHER, bytes(data), "shared-local.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
names = {value: name for name, _, value in read_dynsym(after)}
|
||||
self.assertEqual(names[0x1500], DEFINITIONS[0][0])
|
||||
self.assertNotEqual(names[DEFINITIONS[0][2]], DEFINITIONS[0][0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
@@ -0,0 +1,76 @@
|
||||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from test_symbol_versions import build_image, symbols, patch, SHOFF, DYNSTR_OFFSET, sections
|
||||
|
||||
VERNEED = 0x580
|
||||
|
||||
|
||||
def image(*, auxiliary=16, count=1, size=32, version=1, auxnext=0, nextrecord=0):
|
||||
data = build_image(symbols(False), False)
|
||||
strings = sections(data)[2]
|
||||
filename = strings[5]
|
||||
names = b"libfixture.so\0FIXTURE_1\0"
|
||||
data[DYNSTR_OFFSET + filename:DYNSTR_OFFSET + filename + len(names)] = names
|
||||
struct.pack_into("<Q", data, SHOFF + 2 * 64 + 32, filename + len(names))
|
||||
struct.pack_into("<H", data, 0x3c, 5)
|
||||
struct.pack_into("<IIQQQQIIQQ", data, SHOFF + 4 * 64, 0, 0x6ffffffe, 2, 0, VERNEED, size, 2, 1, 8, 0)
|
||||
struct.pack_into("<HHIII", data, VERNEED, version, count, filename, auxiliary, nextrecord)
|
||||
struct.pack_into("<IHHII", data, VERNEED + auxiliary, 0, 0, 2, filename + len(b"libfixture.so\0"), auxnext)
|
||||
return data
|
||||
|
||||
|
||||
class VersionRequirementsTests(unittest.TestCase):
|
||||
def test_auxiliary_record_cannot_leave_declared_section(self):
|
||||
self.reject(image(auxiliary=32))
|
||||
|
||||
def test_auxiliary_count_must_match_chain(self):
|
||||
self.reject(image(count=2))
|
||||
|
||||
def test_unsupported_structure_version_is_rejected(self):
|
||||
self.reject(image(version=2))
|
||||
|
||||
def test_invalid_forward_links_and_section_ranges_are_rejected(self):
|
||||
for fields in ({"auxnext": 16}, {"nextrecord": 64}, {"size": 15},
|
||||
{"size": 0x10000}, {"auxiliary": 8}, {"count": 2, "auxnext": 8}):
|
||||
with self.subTest(fields=fields):
|
||||
self.reject(image(**fields))
|
||||
|
||||
def test_valid_multiple_auxiliaries_and_requirements(self):
|
||||
data = image(count=2, size=80, auxnext=16, nextrecord=48)
|
||||
original_name = struct.unpack_from("<I", data, VERNEED + 24)[0]
|
||||
original_file = struct.unpack_from("<I", data, VERNEED + 4)[0]
|
||||
struct.pack_into("<IHHII", data, VERNEED + 32, 0, 0, 3, original_name, 0)
|
||||
struct.pack_into("<HHIII", data, VERNEED + 48, 1, 1, original_file, 16, 0)
|
||||
struct.pack_into("<IHHII", data, VERNEED + 64, 0, 0, 4, original_name, 0)
|
||||
result, after = patch(PATCHER, bytes(data), "multiple-versions.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for position, expected in ((4, b"libfixture.so"), (24, b"FIXTURE_1"),
|
||||
(40, b"FIXTURE_1"), (52, b"libfixture.so"), (72, b"FIXTURE_1")):
|
||||
offset = struct.unpack_from("<I", after, VERNEED + position)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)], expected)
|
||||
|
||||
def reject(self, data):
|
||||
result, after = patch(PATCHER, bytes(data), "versions.so")
|
||||
self.assertEqual(result.returncode, 2, (result.returncode, result.stderr))
|
||||
self.assertIn(b"version_r", result.stderr)
|
||||
self.assertEqual(after, data)
|
||||
|
||||
def test_valid_names_are_preserved_when_strings_are_rebuilt(self):
|
||||
data = image()
|
||||
result, after = patch(PATCHER, bytes(data), "valid-versions.so")
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
strings = sections(after)[2]
|
||||
for position, expected in ((VERNEED + 4, b"libfixture.so"), (VERNEED + 16 + 8, b"FIXTURE_1")):
|
||||
offset = struct.unpack_from("<I", after, position)[0]
|
||||
start = strings[4] + offset
|
||||
self.assertEqual(after[start:after.index(0, start)], expected)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
PATCHER = Path(sys.argv.pop(1)).resolve()
|
||||
unittest.main()
|
||||
@@ -1,54 +0,0 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI ErrorDialogClose(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI ErrorDialogOpen(const void* param) {
|
||||
(void)param;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI ShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) {
|
||||
(void)feature_flag;
|
||||
(void)status;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI ShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) {
|
||||
(void)heap_size;
|
||||
(void)thread_priority;
|
||||
(void)affinity_mask;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI ShareTerminate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI SystemServiceParamGetInt(int param_id, int* value) {
|
||||
(void)param_id;
|
||||
(void)value;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI SystemServiceParamGetString(int param_id, char* buf, size_t buf_size) {
|
||||
(void)param_id;
|
||||
(void)buf;
|
||||
(void)buf_size;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI RemotePlayGetConnectionStatus(int user_id, int* status) {
|
||||
(void)user_id;
|
||||
(void)status;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI ShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) {
|
||||
(void)feature_flag;
|
||||
(void)status;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI ShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) {
|
||||
(void)heap_size;
|
||||
(void)thread_priority;
|
||||
(void)affinity_mask;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI ShareTerminate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
|
||||
APS5_DUMMY_FUN
|
||||
|
||||
}
|
||||
@@ -1,64 +1,113 @@
|
||||
#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) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcm_FFT() {
|
||||
int APS5_VABI sceAcm_FFT(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcm_Panner(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcmBatchJobPriority(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcm_Panner() {
|
||||
int APS5_VABI sceAcm_ConvReverb_SharedIr() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcm_IFFT() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int APS5_VABI sceAcmBatchJobNotification() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -4,17 +4,34 @@
|
||||
#include "prx/libSceAgc/Command/include/Memory.hpp"
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
#include "prx/libSceAgc/DcbState/include/Marker.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VideoOutput.hpp"
|
||||
#include "prx/libSceAgcDriver/Resource/include/Registration.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) {
|
||||
if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
|
||||
NotImplemented_nid_no_patch(function);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
// unknown signature
|
||||
void* APS5_VABI sceAgcAcbAtomicGds_0900(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
std::uint32_t* APS5_VABI sceAgcAcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) {
|
||||
const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) |
|
||||
((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) |
|
||||
((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u);
|
||||
return Agc::Command::Emit(buf, 0x1du, {
|
||||
operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index,
|
||||
mask & 0x00ff00ffu, data, static_cast<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 sceAgcAcbJump(CommandBuffer* buf, std::uint8_t cachePolicy, const std::uint32_t* target, std::uint32_t sizeInDwords) {
|
||||
@@ -46,21 +63,38 @@ std::uint32_t APS5_VABI sceAgcAcbRewindGetSize() {
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t videoOutHandle, std::uint32_t displayBufferIndex) {
|
||||
(void)buf;
|
||||
(void)videoOutHandle;
|
||||
(void)displayBufferIndex;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
auto* packet = Agc::Command::Allocate(buf, AgcDriver::RenderingWaitPacketWords, __func__);
|
||||
packet[0] = AgcDriver::RenderingWaitPacketHeader;
|
||||
packet[1] = videoOutHandle;
|
||||
packet[2] = displayBufferIndex;
|
||||
packet[3] = 0;
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t videoOutHandle, std::int32_t displayBufferIndex, std::uint32_t flipMode, std::int64_t flipArg) {
|
||||
(void)buf;
|
||||
(void)videoOutHandle;
|
||||
(void)displayBufferIndex;
|
||||
(void)flipMode;
|
||||
(void)flipArg;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
auto* packet = Agc::Command::Allocate(buf, AgcDriver::FlipPacketWords, __func__);
|
||||
packet[0] = AgcDriver::FlipPacketHeader;
|
||||
packet[1] = videoOutHandle;
|
||||
packet[2] = static_cast<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) {
|
||||
(void)workloadIds;
|
||||
(void)workloadCount;
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) {
|
||||
(void)workloadId;
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) {
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -29,9 +29,11 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DcbState/src/Predication.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DcbState/src/Registers.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/DcbState/src/Workload.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Debug.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/PacketInfo.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Platform.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/RegisterDefaults.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Semaphore.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/ShaderFusion.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Misc/src/Suspend.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Patch/src/CondExec.cpp
|
||||
@@ -46,6 +48,7 @@ add_library(libSceAgc SHARED EXCLUDE_FROM_ALL
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Predication/src/Predication.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/CreateShader.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/GsOversubscription.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/GsPrimPayload.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/InterpolantMapping.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/LinkShaders.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Shader/src/PrimState.cpp
|
||||
@@ -67,7 +70,7 @@ endforeach()
|
||||
|
||||
add_test_executable(agc_command_tests EXCLUDE_FROM_ALL tests/Commands.cpp)
|
||||
target_include_directories(agc_command_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_command_tests PRIVATE libSceAgc libc)
|
||||
target_link_libraries(agc_command_tests PRIVATE libSceAgc libSceAgcDriver libc)
|
||||
configure_windows_unwind(agc_command_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_command_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
@@ -78,6 +81,19 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<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_platform_tests EXCLUDE_FROM_ALL tests/Platform.cpp)
|
||||
@@ -93,6 +109,19 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_semaphore_tests EXCLUDE_FROM_ALL tests/Semaphore.cpp)
|
||||
target_include_directories(agc_semaphore_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_semaphore_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_semaphore_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_semaphore_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_semaphore_tests COMMAND agc_semaphore_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_semaphore_tests PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp)
|
||||
target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc)
|
||||
@@ -209,7 +238,7 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_shader_fusion_tests EXCLUDE_FROM_ALL tests/ShaderFusion.cpp)
|
||||
target_include_directories(agc_shader_fusion_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include)
|
||||
target_include_directories(agc_shader_fusion_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR} ${ANYPS5_SPIRV_HEADERS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_shader_fusion_tests PRIVATE libSceAgc libSceAgcDriver libc)
|
||||
set_target_properties(agc_shader_fusion_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_shader_fusion_tests)
|
||||
@@ -290,6 +319,22 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_gs_prim_payload_tests EXCLUDE_FROM_ALL
|
||||
tests/GsPrimPayload.cpp
|
||||
Shader/src/GsPrimPayload.cpp
|
||||
)
|
||||
target_include_directories(agc_gs_prim_payload_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_gs_prim_payload_tests PRIVATE libc)
|
||||
configure_windows_unwind(agc_gs_prim_payload_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_gs_prim_payload_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_gs_prim_payload_tests COMMAND agc_gs_prim_payload_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_gs_prim_payload_tests PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_dma_data_tests EXCLUDE_FROM_ALL
|
||||
tests/DmaData.cpp
|
||||
Command/src/Packet.cpp
|
||||
@@ -383,6 +428,7 @@ configure_windows_unwind(agc_shader_helpers_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests)
|
||||
set_tests_properties(agc_shader_helpers PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<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
|
||||
@@ -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>
|
||||
@@ -85,7 +87,7 @@ void ValidatePacket(const std::uint32_t* packet, std::uint32_t opcode, std::uint
|
||||
}
|
||||
|
||||
std::uint32_t* WriteNop(CommandBuffer* buffer, std::uint32_t count, const char* function) {
|
||||
const auto header = Header(0x10u, count);
|
||||
const auto header = count == 1 ? 0xffff1000u : Header(0x10u, count);
|
||||
auto* packet = Allocate(buffer, count, function);
|
||||
packet[0] = header;
|
||||
std::fill_n(packet + 1, count - 1u, 0u);
|
||||
@@ -96,8 +98,8 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s
|
||||
Require(count != 0 && count <= 0x3fffu && offset <= 0xffffu && count <= 0x10000u - offset, function, "invalid register range");
|
||||
std::vector<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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,7 +20,7 @@ uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, uint32_t sh
|
||||
// SET_BASE with base index 1; header bit 1 selects the dispatch (compute) base over the draw base.
|
||||
Agc::Command::CheckBits(shader_type, 1, __func__);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(indirect_base_addr);
|
||||
Agc::Command::CheckAddress(address, 8, __func__);
|
||||
if (address != 0) Agc::Command::CheckAddress(address, 8, __func__);
|
||||
Agc::Command::CheckBits(address, 0xffffffffffffull, __func__);
|
||||
auto* packet = Agc::Command::Allocate(buf, 4, __func__);
|
||||
packet[0] = Agc::Command::Header(0x11u, 4, shader_type != 0 ? 2u : 0u);
|
||||
|
||||
@@ -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,11 +12,19 @@
|
||||
|
||||
extern "C" {
|
||||
|
||||
// unknown signature
|
||||
APS5_EXPORT("zARR5aCmkoY", sceAgcDcbA_zARR5aCmkoY);
|
||||
void* APS5_VABI sceAgcDcbA_zARR5aCmkoY(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
std::uint32_t* APS5_VABI sceAgcDcbAtomicGds_0900(CommandBuffer* buf, std::uint32_t control, std::uint32_t mode, std::uint32_t gdsOp, std::uint32_t src, std::uint16_t offset, std::uint16_t index, std::uint32_t mask, std::uint32_t data, std::uint64_t compareData, std::uint64_t extraData) {
|
||||
const auto operation = (control & 0xffu) | ((control & 0x200u) << 7u) |
|
||||
((control & 0x20u) << 13u) | ((control & 0x40u) << 14u) |
|
||||
((control & 0x20u) == 0 && mode == 1 ? 0x20000u : 0u);
|
||||
return Agc::Command::Emit(buf, 0x1du, {
|
||||
operation, ((gdsOp & 1u) << 8u) | (src & 0x3fu), offset, index,
|
||||
mask & 0x00ff00ffu, data, static_cast<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);
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +43,7 @@ std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t o
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command) {
|
||||
Agc::Command::CheckBits(command, 0xfu, __func__);
|
||||
return Agc::Command::Emit(buf, 0x12u, {command}, __func__);
|
||||
return Agc::Command::Emit(buf, 0x12u, {command & 0xfu}, __func__);
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbRewind(CommandBuffer* buf, uint32_t initial_state) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -36,7 +36,6 @@ std::uint32_t* APS5_VABI sceAgcDcbEventWrite(CommandBuffer* buf, std::uint8_t ev
|
||||
Agc::Command::CheckAddress(guestAddress, 8, __func__);
|
||||
return Agc::Command::Emit(buf, 0x46u, {0x100u | eventType, static_cast<std::uint32_t>(guestAddress), static_cast<std::uint32_t>(guestAddress >> 32u)}, __func__);
|
||||
}
|
||||
Agc::Command::Require(address == nullptr, __func__, "this event does not use an address");
|
||||
const auto eventIndex = eventType == 7 || eventType == 15 || eventType == 16 ? 0x400u : 0u;
|
||||
return Agc::Command::Emit(buf, 0x46u, {eventIndex | eventType}, __func__);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,7 +29,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetCxRegistersIndirect(CommandBuffer* buf, con
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcDcbSetCxRegistersIndirectGetSize(std::uint32_t numRegs) {
|
||||
Agc::Command::CheckBits(numRegs, 0x3fffu, __func__);
|
||||
static_cast<void>(numRegs);
|
||||
return 20;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetShRegistersIndirect(CommandBuffer* buf, con
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcDcbSetShRegistersIndirectGetSize(std::uint32_t numRegs) {
|
||||
Agc::Command::CheckBits(numRegs, 0x3fffu, __func__);
|
||||
static_cast<void>(numRegs);
|
||||
return 20;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ std::uint32_t* APS5_VABI sceAgcDcbSetUcRegistersIndirect(CommandBuffer* buf, con
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcDcbSetUcRegistersIndirectGetSize(std::uint32_t numRegs) {
|
||||
Agc::Command::CheckBits(numRegs, 0x3fffu, __func__);
|
||||
static_cast<void>(numRegs);
|
||||
return 20;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,26 +1,62 @@
|
||||
#include "prx/libSceAgc/DcbState/include/Workload.hpp"
|
||||
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
#include "prx/libSceAgcDriver/Resource/include/Registration.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t OpcodeNop = 0x10;
|
||||
constexpr std::uint32_t ActivePacketWords = 18;
|
||||
constexpr std::uint32_t CompletePacketWords = 12;
|
||||
constexpr std::uint32_t MaxWorkloadId = 63;
|
||||
constexpr std::uint32_t MaxActiveCount = 63;
|
||||
|
||||
uint32_t* NewPacket(CommandBuffer* buf, std::uint32_t words, const char* function) {
|
||||
auto* packet = Agc::Command::Allocate(buf, words, function);
|
||||
std::memset(packet, 0, words * sizeof(std::uint32_t));
|
||||
packet[0] = Agc::Command::Header(OpcodeNop, words, 0);
|
||||
return packet;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t stream_id, uint32_t workload_id) {
|
||||
(void)buf;
|
||||
(void)stream_id;
|
||||
(void)workload_id;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
if (buf == nullptr || workload_id > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr;
|
||||
const std::uint64_t clearMask = ~(std::uint64_t{1} << workload_id);
|
||||
auto* packet = NewPacket(buf, CompletePacketWords, __func__);
|
||||
packet[1] = stream_id;
|
||||
packet[2] = workload_id;
|
||||
packet[3] = static_cast<std::uint32_t>(clearMask);
|
||||
packet[4] = static_cast<std::uint32_t>(clearMask >> 32);
|
||||
return packet;
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t stream_id, const uint32_t* workload_ids, uint32_t workload_count) {
|
||||
(void)buf;
|
||||
(void)stream_id;
|
||||
(void)workload_ids;
|
||||
(void)workload_count;
|
||||
if (buf == nullptr || workload_ids == nullptr || workload_count == 0 || workload_count > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(stream_id)) return nullptr;
|
||||
std::uint64_t mask = 0;
|
||||
for (std::uint32_t index = 0; index < workload_count; ++index) {
|
||||
const auto id = workload_ids[index];
|
||||
if (id > MaxWorkloadId) return nullptr;
|
||||
const std::uint64_t bit = std::uint64_t{1} << id;
|
||||
if ((mask & bit) != 0) return nullptr;
|
||||
mask |= bit;
|
||||
}
|
||||
auto* packet = NewPacket(buf, ActivePacketWords, __func__);
|
||||
packet[1] = stream_id;
|
||||
packet[2] = static_cast<std::uint32_t>(mask);
|
||||
packet[3] = static_cast<std::uint32_t>(mask >> 32);
|
||||
return packet;
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetWorkloadStreamInactive(CommandBuffer* buf, uint32_t streamId) {
|
||||
if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_DEBUG_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_DEBUG_HPP
|
||||
|
||||
#endif
|
||||
@@ -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
|
||||
Loaded 100 of 2883 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user