Merge branch 'main' into 6492

This commit is contained in:
Dean Galvin committed 2026-10-18 19:07:59 +13:00
commit e886b91fab
861 files changed
+52393 -3349

No files matched your search

+1
View File
@@ -13,6 +13,7 @@
- [ ] New third-party code is a submodule built from source
- [ ] Generic behaviour, not specific to one title
- [ ] Depends on: <!-- #PR, or none -->
- [ ] Fixes: <!-- issue it closes as Fixes #N, or none -->
- [ ] Unblocks: <!-- title ID and Game bring-up discussion, e.g. PPSA01325 #3232, or none -->
- [ ] GPUs tested: <!-- for AGC, shader recompiler or VideoOut changes: vendor, model, driver and OS of each GPU the GPU tests ran on; otherwise n/a -->
- [ ] AI-assisted: <!-- yes / no -->
+295 -22
View File
@@ -22,13 +22,18 @@ on:
required: false
default: true
type: boolean
select:
description: Build and run only the tests whose build inputs the pull requests change (push builds of main always run every test)
required: false
default: false
type: boolean
permissions:
contents: read
concurrency:
group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }}
cancel-in-progress: ${{ github.event_name != 'push' }}
cancel-in-progress: true
env:
CONFIGURE: >-
@@ -65,23 +70,77 @@ jobs:
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: touches(
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', workflow
'tools/', 'cmake/', 'CMakeLists.txt'
),
registerContext: touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow),
writeWatch: touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow),
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') {
@@ -97,6 +156,7 @@ jobs:
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}`);
}
@@ -162,6 +222,7 @@ jobs:
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 });
truncated ||= files.length >= 3000;
paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)));
await readWorkflow(files, pr.head.sha);
pulls.push({ number: pull_number, head: pr.head.sha });
}
const run = select(paths, truncated);
@@ -254,9 +315,40 @@ jobs:
7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null
- if: runner.os == 'Windows'
run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON
- if: ${{ matrix.architectures == 'x86_64' }}
run: sudo softwareupdate --install-rosetta --agree-to-license
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON -DCMAKE_OSX_ARCHITECTURES=${{ matrix.architectures }}
- run: cmake --build build-relinker --parallel 3
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4
relinker-asan:
needs: eligibility
if: ${{ needs.eligibility.outputs.relinker == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
env:
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
PR_HEAD: ${{ needs.eligibility.outputs.head }}
run: |
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
- run: cmake -S . -B build-relinker-asan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ -DANYPS5_RELINKER_ONLY=ON -DANYPS5_ENABLE_ASAN=ON -DBUILD_TESTING=ON
- run: cmake --build build-relinker-asan --parallel 4
- run: ctest --test-dir build-relinker-asan --output-on-failure --timeout 120 --label-regex '^tls_function_coverage$' --no-tests=error
register-context:
needs: eligibility
@@ -270,6 +362,25 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
shell: bash
env:
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
PR_HEAD: ${{ needs.eligibility.outputs.head }}
run: |
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
- run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }}
- run: cmake --build build/register-context --parallel
- run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error
@@ -285,6 +396,25 @@ jobs:
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
shell: bash
env:
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
PR_HEAD: ${{ needs.eligibility.outputs.head }}
run: |
read -ra numbers <<< "$PR_NUMBER"
read -ra heads <<< "$PR_HEAD"
refspecs=()
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
git fetch --no-tags --unshallow origin "${refspecs[@]}"
for i in "${!numbers[@]}"; do
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
- run: cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release
- run: cmake --build build/write-watch --parallel
- run: ctest --test-dir build/write-watch --output-on-failure --repeat until-fail:3 --no-tests=error
@@ -300,8 +430,6 @@ jobs:
CCACHE_COMPILERCHECK: content
MESA_SHADER_CACHE_DIR: /var/cache/anyps5/mesa
MESA_SHADER_CACHE_MAX_SIZE: 4G
CMAKE_BUILD_PARALLEL_LEVEL: 1
CTEST_PARALLEL_LEVEL: 1
steps:
- uses: actions/checkout@v7
with:
@@ -335,16 +463,87 @@ jobs:
ccache --show-config
dpkg-query -W mesa-vulkan-drivers
grep -m1 '^model name' /proc/cpuinfo
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON
- name: Install the wild linker
shell: bash
env:
WILD_VERSION: 0.10.0
WILD_SHA256: 641265506a7c06cfb03181b8916ab663ec8407855db6d4db7f8450667d105283
run: |
tools=$RUNNER_TOOL_CACHE/wild
mkdir -p "$tools" 2>/dev/null && test -w "$tools" || { tools=$RUNNER_TEMP/wild; mkdir -p "$tools"; }
dir=$tools/$WILD_VERSION
tmp=
probe=$(mktemp -d "$RUNNER_TEMP/wild-probe.XXXXXX")
trap 'rm -rf "$tmp" "$probe"' EXIT
if [ ! -x "$dir/bin/ld" ]; then
tmp=$(mktemp -d "$tools/.wild.XXXXXX")
name=wild-linker-$WILD_VERSION-x86_64-unknown-linux-gnu
curl -fsSL --retry 3 -o "$tmp/$name.tar.gz" "https://github.com/davidlattimore/wild/releases/download/$WILD_VERSION/$name.tar.gz"
echo "$WILD_SHA256 $tmp/$name.tar.gz" | sha256sum -c -
tar -xzf "$tmp/$name.tar.gz" -C "$tmp"
mkdir -p "$tmp/out/bin"
mv "$tmp/$name/wild" "$tmp/out/bin/wild"
ln -s wild "$tmp/out/bin/ld"
mv -T "$tmp/out" "$dir" 2>/dev/null || test -x "$dir/bin/ld"
fi
echo 'int main(void) { return 0; }' > "$probe/main.c"
gcc "-B$dir/bin" "$probe/main.c" -o "$probe/main"
readelf -p .comment "$probe/main" | grep -F "Linker: Wild $WILD_VERSION"
echo "LINKER_FLAGS=-B$dir/bin" >> "$GITHUB_ENV"
- name: Size parallelism to what the shared host has free
run: |
cpus=$(nproc)
busy=$(awk '{ printf "%d", $1 + 0.5 }' /proc/loadavg)
mem_mb=$(awk '/^MemTotal:/ { print int($2 / 1024) }' /proc/meminfo)
slots=$(( (mem_mb - 2048) / 1024 ))
slots=$(( slots < 1 ? 1 : slots ))
tests=$(( cpus - busy ))
tests=$(( tests < 1 ? 1 : tests ))
slot_dir=$CCACHE_DIR/compile-slots
mkdir -p -m 1777 "$slot_dir"
test -w "$slot_dir"
echo "cpus=$cpus load=$busy mem_total_mb=$mem_mb -> build jobs $cpus, cold compiles $slots per host, tests $tests"
echo "CMAKE_BUILD_PARALLEL_LEVEL=$cpus" >> "$GITHUB_ENV"
echo "CTEST_PARALLEL_LEVEL=$tests" >> "$GITHUB_ENV"
echo "CCACHE_PREFIX=$GITHUB_WORKSPACE/tools/compile_slot.sh" >> "$GITHUB_ENV"
echo "ANYPS5_COMPILE_SLOT_DIR=$slot_dir" >> "$GITHUB_ENV"
echo "ANYPS5_COMPILE_SLOTS=$slots" >> "$GITHUB_ENV"
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON "-DCMAKE_EXE_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_SHARED_LINKER_FLAGS=$LINKER_FLAGS" "-DCMAKE_MODULE_LINKER_FLAGS=$LINKER_FLAGS"
- name: Check that every test executable is registered with ctest
run: python3 tools/check_test_registration.py build
- run: cmake --build build
- run: cmake --build build --target libs
- run: ctest --test-dir build --output-on-failure --timeout 120 --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
@@ -372,7 +571,6 @@ jobs:
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
submodules: recursive
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
shell: bash
@@ -389,7 +587,28 @@ jobs:
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
done
git submodule update --init --recursive
- id: submodules
name: Key the submodules on their commits
run: |
echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT"
- id: submodule-cache
uses: actions/cache/restore@v6
with:
path: ${{ runner.temp }}/submodule-gitdirs.tar
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
- name: Check out the submodules
run: |
if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar --force-local -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi
git submodule update --init --recursive --depth 1 --jobs 8
- if: steps.submodule-cache.outputs.cache-hit != 'true'
run: tar --force-local -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules
- if: steps.submodule-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: ${{ runner.temp }}/submodule-gitdirs.tar
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
- name: Point the WinLibs CMake at the CA bundle of Git for Windows
run: |
for prefix in ucrt64 mingw64; do
@@ -409,21 +628,74 @@ jobs:
- run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
- uses: hendrikmuhs/ccache-action@v1.2.24
with:
key: windows
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
key: ${{ github.event_name == 'workflow_dispatch' && 'dispatch-windows' || 'windows' }}
restore-keys: ${{ github.event_name == 'workflow_dispatch' && 'windows' || '' }}
save: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
evict-old-files: job
- id: ffmpeg-cache
uses: actions/cache/restore@v6
with:
path: build/externals
key: ffmpeg-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }}
- id: seed-cache
uses: actions/cache/restore@v6
with:
path: configure-seed.cmake
key: configure-seed-${{ env.WINLIBS_ZIP }}-${{ steps.submodules.outputs.key }}-${{ hashFiles('CMakeLists.txt', 'cmake/**', 'core/libs/CMakeLists.txt', '.github/workflows/build.yml') }}
- shell: cmd
run: cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
run: if exist configure-seed.cmake (cmake -C configure-seed.cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown") else (cmake -S . -B build %CONFIGURE% "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown")
- if: steps.seed-cache.outputs.cache-hit != 'true'
shell: cmd
run: python tools/configure_seed.py build/CMakeCache.txt configure-seed.cmake
- if: steps.seed-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: configure-seed.cmake
key: ${{ steps.seed-cache.outputs.cache-primary-key }}
- if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
continue-on-error: true
with:
path: build/externals
key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }}
- name: Check that every test executable is registered with ctest
shell: cmd
run: python tools/check_test_registration.py build
- shell: cmd
run: cmake --build build
- shell: cmd
- if: ${{ inputs.select }}
uses: actions/cache/restore@v6
with:
path: test-graph.json
key: test-graph-${{ runner.os }}-${{ needs.eligibility.outputs.sha }}
restore-keys: test-graph-${{ runner.os }}-
- name: Select the tests the change reaches
if: ${{ inputs.select }}
env:
BASE: ${{ needs.eligibility.outputs.sha }}
shell: cmd
run: |
git diff --name-only %BASE% HEAD > changed.txt
python tools/select_tests.py select build test-graph.json changed.txt
- id: build
shell: cmd
run: if exist build\select.ninja (ninja -C build -f select.ninja selected) else (cmake --build build)
- id: libs
shell: cmd
run: cmake --build build --target libs
- shell: cmd
run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
run: python tools/select_tests.py ctest build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log --output-junit ctest.xml
- run: python tools/check_skips.py build/ctest.xml --job windows
- name: Write the test graph of main
if: ${{ !cancelled() && github.event_name == 'push' && steps.build.outcome == 'success' && steps.libs.outcome == 'success' }}
continue-on-error: true
shell: cmd
run: python tools/select_tests.py dump build test-graph.json
- if: ${{ !cancelled() && github.event_name == 'push' && hashFiles('test-graph.json') != '' }}
continue-on-error: true
uses: actions/cache/save@v6
with:
path: test-graph.json
key: test-graph-${{ runner.os }}-${{ github.sha }}
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
shell: bash
@@ -431,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
+44
View File
@@ -0,0 +1,44 @@
name: Fix label
on:
schedule:
- cron: '45 * * * *'
workflow_dispatch:
permissions: {}
concurrency:
group: fix-label
cancel-in-progress: true
jobs:
label:
runs-on: ubuntu-latest
permissions:
issues: read
pull-requests: write
env:
GH_TOKEN: ${{ github.token }}
LABEL: fix error
steps:
- run: |
gh api graphql --paginate -f owner="${GITHUB_REPOSITORY%/*}" -f name="${GITHUB_REPOSITORY#*/}" -f query='
query($owner: String!, $name: String!, $endCursor: String) {
repository(owner: $owner, name: $name) {
pullRequests(states: OPEN, first: 100, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes { number closingIssuesReferences { totalCount } labels(first: 100) { nodes { name } } }
}
}
}' --jq '.data.repository.pullRequests.nodes[]
| (.closingIssuesReferences.totalCount > 0) as $fix
| ([.labels.nodes[].name] | index(env.LABEL) != null) as $has
| if $fix and ($has | not) then "add \(.number)" elif $has and ($fix | not) then "remove \(.number)" else empty end' > "$RUNNER_TEMP/fix-todo"
label=$(jq -rn 'env.LABEL | @uri')
while read -r action number; do
if [ "$action" = add ]; then
gh api "repos/$GITHUB_REPOSITORY/issues/$number/labels" -f "labels[]=$LABEL" --silent
else
gh api -X DELETE "repos/$GITHUB_REPOSITORY/issues/$number/labels/$label" --silent
fi
done < "$RUNNER_TEMP/fix-todo"
+1
View File
@@ -35,4 +35,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'
+21
View File
@@ -27,6 +27,27 @@ jobs:
if (pr.state !== 'open' || pr.base.ref !== 'main') {
return;
}
if (context.payload.action === 'reopened') {
let maintainer = false;
try {
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({
owner, repo, username: context.payload.sender.login
});
maintainer = ['admin', 'maintain', 'write'].includes(access.role_name ?? access.permission);
} catch (error) {
core.warning(`Could not check the permission of ${context.payload.sender.login}, so the open PR limit applies: ${error.message}`);
}
if (maintainer) {
try {
await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, name: 'open PR limit' });
} catch (error) {
if (error.status !== 404) {
core.warning(`Could not remove the open PR limit label: ${error.message}`);
}
}
return;
}
}
const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:pr is:merged base:main author:${pr.user.login}`,
per_page: 1
+6
View File
@@ -22,6 +22,12 @@ jobs:
- 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
+6
View File
@@ -40,3 +40,9 @@
[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
Vendored Submodule
+1
Submodule 3rdparty/ZArchive added at 965b66c8d6.
Vendored Submodule
+1
Submodule 3rdparty/zstd added at f8745da6ff.
+14
View File
@@ -0,0 +1,14 @@
# 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.
+30 -2
View File
@@ -6,10 +6,21 @@ endif()
project(Relinker CXX)
set(ANYPS5_TEST_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests")
set(ANYPS5_WINDOWS_TEST_EXIT "${CMAKE_CURRENT_SOURCE_DIR}/cmake/WindowsTestExit.cpp")
function(add_test_executable target)
add_executable(${target} ${ARGN})
set_target_properties(${target} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "$<1:${ANYPS5_TEST_OUTPUT_DIRECTORY}>")
if(MINGW)
target_sources(${target} PRIVATE "${ANYPS5_WINDOWS_TEST_EXIT}")
target_link_options(${target} PRIVATE LINKER:--wrap=main)
endif()
endfunction()
function(add_asan_test_options test options)
if(ANYPS5_ENABLE_ASAN)
set_property(TEST ${test} APPEND PROPERTY ENVIRONMENT_MODIFICATION "ASAN_OPTIONS=string_append::${options}")
endif()
endfunction()
option(BUILD_TESTING "Build and register relinker tests" OFF)
@@ -86,6 +97,17 @@ if(BUILD_TESTING)
add_test(NAME request_serializer COMMAND request_serializer_tests)
set_tests_properties(request_serializer PROPERTIES TIMEOUT 20)
find_package(Python3 QUIET COMPONENTS Interpreter)
add_test_executable(shader_ir_use_rewrite_tests
core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp
core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp
core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp
core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp
core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp
core/shader/recompiler/IntermediateRepresentation/src/GuestRegister.cpp)
target_include_directories(shader_ir_use_rewrite_tests PRIVATE
core/shader/recompiler/IntermediateRepresentation/include)
add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests)
set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20)
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
@@ -113,14 +135,14 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py)
foreach(shard RANGE 0 5)
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}/6)
${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}
@@ -135,8 +157,12 @@ if(BUILD_TESTING)
${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)
add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
@@ -267,6 +293,8 @@ 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)
add_subdirectory(core/shader/recompiler)
add_subdirectory(core/Decoder/Jpeg)
add_subdirectory(core/Decoder/Png)
+2
View File
@@ -57,7 +57,9 @@ The [pull request template](.github/pull_request_template.md) is the checklist f
- Keep the branch up to date with `main` and resolve conflicts yourself; rebasing and force-pushing is fine.
- A pull request that conflicts with `main` and has no activity for 7 days is labeled `stale` with a reminder. It is not closed.
- If a pull request needs another one first, say so on the checklist line (`Depends on: #N`; `none` otherwise).
- If a pull request fixes a 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.
- 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).
+31
View File
@@ -0,0 +1,31 @@
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <process.h>
#include <windows.h>
extern "C" int __real_main(int argc, char** argv);
namespace {
template<typename TFunction>
TFunction LibcFunction(const char* name) {
const HMODULE libc = GetModuleHandleW(L"libc.prx");
if (libc == nullptr) return nullptr;
return reinterpret_cast<TFunction>(reinterpret_cast<void*>(GetProcAddress(libc, name)));
}
}
extern "C" int __wrap_main(int argc, char** argv) {
const int code = __real_main(argc, argv);
const auto terminate = LibcFunction<void (*)(int)>("LibcTerminate_nid_no_patch");
if (terminate == nullptr) return code;
_cexit();
std::cout.flush();
std::clog.flush();
std::cerr.flush();
std::fflush(nullptr);
terminate(code);
return code;
}
+25
View File
@@ -0,0 +1,25 @@
enable_language(C)
set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_CONTRIB OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE)
set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "" FORCE)
add_subdirectory(${CMAKE_SOURCE_DIR}/3rdparty/zstd/build/cmake ${CMAKE_BINARY_DIR}/3rdparty/zstd EXCLUDE_FROM_ALL)
set_target_properties(libzstd_static PROPERTIES POSITION_INDEPENDENT_CODE ON)
set(ZARCHIVE_SOURCE_DIR ${CMAKE_SOURCE_DIR}/3rdparty/ZArchive)
add_library(zarchive STATIC EXCLUDE_FROM_ALL
${ZARCHIVE_SOURCE_DIR}/src/zarchivereader.cpp
${ZARCHIVE_SOURCE_DIR}/src/zarchivewriter.cpp
${ZARCHIVE_SOURCE_DIR}/src/sha_256.c
)
target_include_directories(zarchive SYSTEM PUBLIC ${ZARCHIVE_SOURCE_DIR}/include)
target_compile_features(zarchive PUBLIC cxx_std_20)
target_link_libraries(zarchive PRIVATE libzstd_static)
set_target_properties(zarchive PROPERTIES POSITION_INDEPENDENT_CODE ON)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(zarchive INTERFACE LINKER:--exclude-libs,libzarchive.a:libzstd.a)
endif()
+55 -11
View File
@@ -1,5 +1,7 @@
if(NOT WIN32)
string(REPLACE "-static " "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ")
foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static)
string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}")
endforeach()
string(STRIP "${CMAKE_EXE_LINKER_FLAGS}" CMAKE_EXE_LINKER_FLAGS)
endif()
@@ -221,6 +223,8 @@ if(BUILD_TESTING)
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}
@@ -238,6 +242,17 @@ if(BUILD_TESTING)
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
add_library(host_libstdcxx_exception_module SHARED tests/HostLibstdcxxExceptionModule.cpp)
target_link_libraries(host_libstdcxx_exception_module PRIVATE ${PATCHED_DIR}/libc.prx)
add_dependencies(host_libstdcxx_exception_module libs)
add_test_executable(host_libstdcxx_exception_tests tests/HostLibstdcxxException.cpp)
target_link_libraries(host_libstdcxx_exception_tests PRIVATE host_libstdcxx_exception_module ${PATCHED_DIR}/libc.prx)
add_dependencies(host_libstdcxx_exception_tests libs)
set_target_properties(host_libstdcxx_exception_module host_libstdcxx_exception_tests PROPERTIES BUILD_RPATH ${PATCHED_DIR})
add_test(NAME host_libstdcxx_exception COMMAND host_libstdcxx_exception_tests)
set_tests_properties(host_libstdcxx_exception PROPERTIES TIMEOUT 20)
endif()
if(Python3_Interpreter_FOUND)
add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $<TARGET_FILE:nid_patcher>)
@@ -277,6 +292,35 @@ if(BUILD_TESTING)
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_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)
@@ -351,6 +395,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
endif()
add_test_executable(guest_rudp_context_lifecycle_tests tests/GuestRudpContextLifecycle.cpp)
target_include_directories(guest_rudp_context_lifecycle_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_rudp_context_lifecycle_tests PRIVATE libSceRudp libc)
configure_windows_unwind(guest_rudp_context_lifecycle_tests)
add_test(NAME guest_rudp_context_lifecycle COMMAND guest_rudp_context_lifecycle_tests)
if(WIN32)
set_tests_properties(guest_rudp_context_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
endif()
add_test_executable(guest_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)
@@ -458,16 +512,6 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSaveData.native>")
endif()
add_test_executable(guest_ppsa01530_exports_tests tests/GuestPPSA01530Exports.cpp)
target_include_directories(guest_ppsa01530_exports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_ppsa01530_exports_tests PRIVATE libkernel libSceJson2 libSceNet libScePad libc)
configure_windows_unwind(guest_ppsa01530_exports_tests)
add_test(NAME guest_ppsa01530_exports COMMAND guest_ppsa01530_exports_tests)
if(WIN32)
set_tests_properties(guest_ppsa01530_exports PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
endif()
add_test_executable(guest_posix_random_tests tests/GuestPosixRandom.cpp)
target_include_directories(guest_posix_random_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_posix_random_tests PRIVATE libScePosixForWebKit)
+53 -5
View File
@@ -680,10 +680,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];
@@ -918,6 +921,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;
@@ -1369,10 +1380,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;
@@ -1965,6 +1972,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;
@@ -1993,6 +2036,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 (*)();
+5
View File
@@ -4,6 +4,7 @@
#include <nid/NidCompute.hpp>
#include <algorithm>
#include <unordered_map>
#include <utility>
#include <cstdint>
#include <cstring>
#include <stdexcept>
@@ -408,6 +409,7 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
};
std::vector<SymbolNameUse> uses;
std::unordered_map<std::string, std::pair<std::uint64_t, std::uint16_t>> patchedExports;
for (std::size_t i = 1u; i < symCount; ++i) {
const std::size_t symOffset = dynSymOffset + i * sizeof(Elf64_Sym);
const auto sym = Read<Elf64_Sym>(elf, symOffset);
@@ -424,6 +426,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);
@@ -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()
@@ -6,6 +6,7 @@
namespace Agc::Command {
void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* function);
int GetDefaultCxStateFlat(void* destination, std::uint32_t size);
}
@@ -107,7 +107,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>
@@ -44,6 +45,33 @@ struct Storage {
std::mutex defaultsMutex;
std::array<std::array<Storage, 14>, 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) {
@@ -76,4 +104,15 @@ void* GetRegisterDefaults(std::uint32_t version, bool internal, const char* func
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;
}
}
@@ -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
@@ -26,4 +26,8 @@ void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version) {
return Agc::Command::GetRegisterDefaults(version, true, __func__);
}
int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size) {
return Agc::Command::GetDefaultCxStateFlat(out_state, size);
}
}
@@ -127,13 +127,11 @@ int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Sh
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValue, true);
}
APS5_EXPORT("dbOlWdppb4o", sceAgcUnknownCreateInterpolantMapping);
int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, CreateInterpolantValueSplitF16, true);
}
APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw);
int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps) {
return CreateInterpolantMapping(__func__, regs, gs, ps, UpdateInterpolantValue, false);
}
@@ -30,11 +30,6 @@ int APS5_VABI sceAgcDcbSetMarkerSpan() {
return 0;
}
int APS5_VABI sceAgcGetDefaultCxStateFlat() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcSetSemaphoreMemory() {
NotImplemented_nid_no_patch(__func__);
return 0;
@@ -21,6 +21,7 @@ extern "C" int APS5_VABI sceAgcInit(std::uint32_t version);
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults();
extern "C" void* APS5_VABI sceAgcGetRegisterDefaultsInternal();
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version);
extern "C" int APS5_VABI sceAgcGetDefaultCxStateFlat(void* out_state, std::uint32_t size);
extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier);
@@ -637,6 +638,21 @@ void testDefaults() {
auto* internalDefaults = sceAgcGetRegisterDefaultsInternal();
check(internalDefaults != nullptr && internalDefaults == Agc::Command::GetRegisterDefaults(0, true, __func__) && internalDefaults == sceAgcGetRegisterDefaults2Internal(0), "internal register defaults are not the baseline internal table");
check(internalDefaults != sceAgcGetRegisterDefaults(), "internal register defaults returned the public table");
constexpr int kInvalidArgument = static_cast<int>(0x80890001u);
std::array<std::uint32_t, 512> flatCxBuffer{};
check(sceAgcGetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == kInvalidArgument, "null destination was accepted");
check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), 0) == 0, "zero size copy failed");
check(sceAgcGetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "flat CX state default retrieval failed");
check(flatCxBuffer[0x001] == 0x11000100u, "flat CX state register 0x001 mismatch");
check(flatCxBuffer[0x007] == 0x0000ffffu, "flat CX state register 0x007 mismatch");
check(flatCxBuffer[0x12d] == 0x3f800000u, "flat CX state register 0x12d mismatch");
check(flatCxBuffer[0x1e0] == 0x20010001u, "flat CX state register 0x1e0 mismatch");
std::array<std::uint32_t, 513> overBuffer{};
overBuffer[512] = 0x12345678u;
check(sceAgcGetDefaultCxStateFlat(overBuffer.data(), sizeof(overBuffer)) == 0, "oversized copy failed");
check(overBuffer[512] == 0x12345678u, "oversized copy overflowed buffer");
check(Agc::Command::GetDefaultCxStateFlat(nullptr, sizeof(flatCxBuffer)) == kInvalidArgument, "null command destination was accepted");
check(Agc::Command::GetDefaultCxStateFlat(flatCxBuffer.data(), sizeof(flatCxBuffer)) == 0, "command flat CX state retrieval failed");
expectFailure([] { Agc::Command::GetRegisterDefaults(14, false, __func__); });
expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); });
}
@@ -12,8 +12,8 @@
#include <span>
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_0100(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUpdateInterpolantMapping_10000(ShaderRegister* regs, const Shader* gs, const Shader* ps);
namespace {
@@ -61,14 +61,14 @@ void checkIdentity(const Registers& regs, std::uint32_t first, const char* messa
void testIdentity() {
auto regs = filled();
check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, nullptr) == 0, "mapping without shaders failed");
check(sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, nullptr) == 0, "mapping without shaders failed");
checkIdentity(regs, 0, "mapping without a pixel shader is not the identity");
Shader ps{};
regs = filled();
check(sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed");
check(sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, &ps) == 0, "mapping without inputs failed");
checkIdentity(regs, 0, "mapping without inputs is not the identity");
regs = filled();
check(sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps) == 0, "update without inputs failed");
check(sceAgcUpdateInterpolantMapping_10000(regs.data(), nullptr, &ps) == 0, "update without inputs failed");
check(preparedCount == 0u, "update without inputs prepared registers");
const auto untouched = filled();
check(std::memcmp(regs.data(), untouched.data(), sizeof(regs)) == 0, "update without inputs wrote registers");
@@ -94,7 +94,7 @@ void testMapping() {
ps.num_input_semantics = static_cast<std::uint32_t>(inputs.size());
auto split = filled();
check(sceAgcUnknownCreateInterpolantMapping(split.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(sceAgcCreateInterpolantMapping_10000(split.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(std::memcmp(split.data(), preparedMapping.data(), sizeof(split)) == 0, "prepared split mapping differs from published registers");
for (std::uint32_t i = 0; i < inputs.size(); ++i) {
check(split[i].offset == ShaderRegs::SPI_PS_INPUT_CNTL_0 + i, "interpolant register offset changed");
@@ -109,7 +109,7 @@ void testMapping() {
auto update = filled();
const auto untouched = filled();
const std::size_t written = inputs.size() * sizeof(ShaderRegister);
check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed");
check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update mapping failed");
check(preparedCount == inputs.size(), "update prepared registers past its inputs");
check(std::memcmp(update.data(), preparedMapping.data(), written) == 0, "prepared update differs from published registers");
check(std::memcmp(update.data(), split.data(), written) == 0, "update mapping differs from the split f16 mapping");
@@ -142,7 +142,7 @@ void testUpdateMapping() {
auto update = filled();
const auto untouched = filled();
check(sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps) == 0, "update mapping failed");
check(sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps) == 0, "update mapping failed");
check(update[0].value == 0x00000625u, "custom interpolant update changed");
check(update[1].value == 0x01380107u, "low f16 half update changed");
check(update[2].value == 0x02480220u, "unmatched high f16 half update changed");
@@ -154,14 +154,14 @@ void testUpdateMapping() {
check(std::memcmp(update.data() + inputs.size(), untouched.data(), sizeof(update) - written) == 0, "update wrote past its inputs");
auto create = filled();
check(sceAgcUnknownCreateInterpolantMapping(create.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(sceAgcCreateInterpolantMapping_10000(create.data(), &gs, &ps) == 0, "split f16 mapping failed");
check(std::memcmp(create.data(), update.data(), written) == 0, "update differs from the split f16 mapping");
checkIdentity(create, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
for (const std::uint32_t index : {1u, 3u}) {
inputs[index].default_value = 0;
update = filled();
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(update.data(), &gs, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(update.data(), &gs, &ps); });
check(std::memcmp(update.data(), untouched.data(), sizeof(update)) == 0, "rejected update wrote registers");
inputs[index].default_value = inputs[index].default_value_hi;
}
@@ -177,15 +177,15 @@ void testRejections() {
std::memset(regs.data(), 0xcc, sizeof(regs));
const auto saved = regs;
expectFailure([&] { sceAgcCreateInterpolantMapping_0100(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcCreateInterpolantMapping_10000(regs.data(), &gs, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), &gs, &ps); });
check(std::memcmp(regs.data(), saved.data(), sizeof(regs)) == 0, "rejected mapping wrote registers");
ps.num_input_semantics = 1;
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcUnknownCreateInterpolantMapping(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUnknown_vieBRwlh1Lw(regs.data(), &gs, nullptr); });
expectFailure([&] { sceAgcCreateInterpolantMapping_10000(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcCreateInterpolantMapping_10000(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), nullptr, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(nullptr, &gs, &ps); });
expectFailure([&] { sceAgcUpdateInterpolantMapping_10000(regs.data(), &gs, nullptr); });
}
}
@@ -193,6 +193,10 @@ void testDirectList() {
packet = writers[i](&split.buffer, paired.data(), 4);
check(std::equal(expectedSplit.begin(), expectedSplit.end(), packet) && split.buffer.cursor_up == packet + expectedSplit.size(), "register pairs were not written as two ranges");
check(sizes[i](0) == 0, "an empty register list has a size");
Storage empty;
empty.words.fill(sentinel);
check(writers[i](&empty.buffer, nullptr, 0) == empty.words.data() && empty.buffer.cursor_up == empty.words.data() && empty.words.front() == sentinel, "an empty register list wrote a packet");
expectFailure([&] { writers[i](nullptr, nullptr, 0); });
check(sizes[i](0x15555555u) == 0xfffffffcu, "largest register list size mismatch");
for (const auto count : {0x15555556u, 0x40000000u, 0xffffffffu}) {
expectFailure([&] { sizes[i](count); });
@@ -2,6 +2,7 @@
#include "prx/libSceAgc/Shader/include/PrimState.hpp"
#include "prx/libSceAgc/Shader/include/InterpolantMapping.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <algorithm>
#include <array>
@@ -194,9 +195,11 @@ int main() {
Require(preparations == 0 && mappings == 0 && links == 0);
OptionalGeometryOutputRegister(special, vertex, hull);
HandBuiltUserVgprRegister();
LibcRunShutdown_nid_postfix();
std::cout << "shader helper preparation tests passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
try { LibcRunShutdown_nid_postfix(); } catch (...) {}
return 1;
}
}
+394 -8
View File
@@ -154,6 +154,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawPipeline.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawPrecheck.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawRegisterKey.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawRelocate.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/DrawTiming.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Draw/IndirectDraw.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/EntryPoints.cpp
@@ -173,6 +174,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Presentation.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/QueueWorker.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/Submission.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/SubmissionRing.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Queues/WorkerAffinity.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Shaders/ShaderDiagnostics.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Shaders/ShaderRegistry.cpp
@@ -183,6 +185,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitStatistics.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/WorkerSampler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTrace.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTraceThread.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/TraceFile.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ThreadPriority.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp
@@ -204,6 +207,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Pipeline.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineLibrary.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineCache.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PipelineJournal.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Resources.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PlaceholderImages.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Recorder.cpp
@@ -316,6 +320,17 @@ set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(libSceAgcDriver)
if(BUILD_TESTING)
add_test_executable(agc_frame_dump_io_tests tests/FrameDumpIo.cpp)
target_include_directories(agc_frame_dump_io_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_frame_dump_io_tests PRIVATE libc)
configure_windows_unwind(agc_frame_dump_io_tests)
add_test(NAME agc_frame_dump_io COMMAND agc_frame_dump_io_tests)
set_tests_properties(agc_frame_dump_io PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(agc_frame_dump_io PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_write_watch_coverage_tests tests/WriteWatchCoverage.cpp)
target_include_directories(agc_write_watch_coverage_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_write_watch_coverage_tests PRIVATE libc)
@@ -337,6 +352,18 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_async_log_tests tests/AsyncLog.cpp)
target_include_directories(agc_async_log_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_async_log_tests PRIVATE libc)
configure_windows_unwind(agc_async_log_tests)
add_test(NAME agc_async_log COMMAND agc_async_log_tests)
add_test(NAME agc_async_log_fatal COMMAND agc_async_log_tests fatal)
set_tests_properties(agc_async_log agc_async_log_fatal PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(agc_async_log agc_async_log_fatal PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_stack_walk_tests tests/StackWalk.cpp)
target_include_directories(agc_stack_walk_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_stack_walk_tests PRIVATE libc)
@@ -358,6 +385,17 @@ if(BUILD_TESTING)
set_tests_properties(agc_performance_trace PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_handoff_spin_tests tests/HandoffSpin.cpp)
target_include_directories(agc_handoff_spin_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_handoff_spin_tests PRIVATE libc)
configure_windows_unwind(agc_handoff_spin_tests)
add_test(NAME agc_handoff_spin COMMAND agc_handoff_spin_tests)
set_tests_properties(agc_handoff_spin PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_handoff_spin PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(WIN32)
@@ -457,6 +495,7 @@ add_test_executable(agc_shader_memory_tests EXCLUDE_FROM_ALL
tests/ShaderMemory.cpp
Execution/src/ShaderMemory.cpp
Execution/src/GuestMemory.cpp
Execution/src/PerformanceTraceThread.cpp
)
target_include_directories(agc_shader_memory_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driver libc)
@@ -472,6 +511,34 @@ target_link_libraries(agc_vertex_input_tests PRIVATE shader_recompiler_agc_drive
set_target_properties(agc_vertex_input_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_vertex_input_tests)
add_test_executable(agc_guarded_memory_loads_tests EXCLUDE_FROM_ALL tests/GuardedMemoryLoads.cpp)
target_link_libraries(agc_guarded_memory_loads_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_guarded_memory_loads_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_guarded_memory_loads_tests)
if(BUILD_TESTING)
set_target_properties(agc_guarded_memory_loads_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_guarded_memory_loads COMMAND agc_guarded_memory_loads_tests)
set_tests_properties(agc_guarded_memory_loads PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_guarded_memory_loads PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_lds_lock_budget_tests EXCLUDE_FROM_ALL tests/LdsLockBudget.cpp)
target_link_libraries(agc_lds_lock_budget_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_lds_lock_budget_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_lds_lock_budget_tests)
if(BUILD_TESTING)
set_target_properties(agc_lds_lock_budget_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_lds_lock_budget COMMAND agc_lds_lock_budget_tests)
set_tests_properties(agc_lds_lock_budget PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_shared_write_tests EXCLUDE_FROM_ALL tests/SharedWrites.cpp)
target_include_directories(agc_shared_write_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel libc)
@@ -481,6 +548,7 @@ configure_windows_unwind(agc_shared_write_tests)
add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL
tests/WriteTracking.cpp
Execution/src/GuestMemory.cpp
Execution/src/PerformanceTraceThread.cpp
)
target_include_directories(agc_write_tracking_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_write_tracking_tests PRIVATE shader_recompiler_agc_driver libc)
@@ -542,6 +610,15 @@ if(BUILD_TESTING)
add_test(NAME agc_pipeline_link_optimization COMMAND agc_pipeline_link_optimization_tests)
endif()
add_test_executable(agc_robust_buffer_access_tests EXCLUDE_FROM_ALL tests/RobustBufferAccess.cpp)
target_include_directories(agc_robust_buffer_access_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_robust_buffer_access_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_robust_buffer_access_tests)
if(BUILD_TESTING)
set_target_properties(agc_robust_buffer_access_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_robust_buffer_access COMMAND agc_robust_buffer_access_tests)
endif()
add_test_executable(agc_device_fault_tests EXCLUDE_FROM_ALL tests/DeviceFault.cpp)
target_include_directories(agc_device_fault_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_device_fault_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -750,6 +827,24 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_pipeline_queue_failure_tests EXCLUDE_FROM_ALL tests/PipelineQueueFailure.cpp
Graphics/src/PipelineLibrary.cpp tests/AllocationFailure.cpp)
target_include_directories(agc_pipeline_queue_failure_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_pipeline_queue_failure_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_pipeline_queue_failure_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_pipeline_queue_failure_tests)
if(BUILD_TESTING)
set_target_properties(agc_pipeline_queue_failure_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
foreach(mode wait clear stop)
add_test(NAME agc_pipeline_queue_failure_${mode} COMMAND agc_pipeline_queue_failure_tests ${mode})
set_tests_properties(agc_pipeline_queue_failure_${mode} PROPERTIES TIMEOUT 15)
if(WIN32)
set_tests_properties(agc_pipeline_queue_failure_${mode} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endif()
add_test_executable(agc_pass_hazards_tests EXCLUDE_FROM_ALL tests/PassHazards.cpp Graphics/src/PassHazards.cpp)
target_include_directories(agc_pass_hazards_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_pass_hazards_tests PRIVATE libc)
@@ -771,6 +866,7 @@ if(BUILD_TESTING)
endif()
add_test_executable(agc_buffer_pool_tests EXCLUDE_FROM_ALL tests/BufferPool.cpp)
target_sources(agc_buffer_pool_tests PRIVATE Graphics/src/BufferPool.cpp tests/AllocationFailure.cpp)
target_include_directories(agc_buffer_pool_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_buffer_pool_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_buffer_pool_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -778,8 +874,14 @@ configure_windows_unwind(agc_buffer_pool_tests)
if(BUILD_TESTING)
set_target_properties(agc_buffer_pool_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_buffer_pool COMMAND agc_buffer_pool_tests)
add_test(NAME agc_buffer_pool_small_device_shared COMMAND agc_buffer_pool_tests --small-device-retention)
set_tests_properties(agc_buffer_pool_small_device_shared PROPERTIES ENVIRONMENT
"APS5_STAGING_POOL_MIB=256;APS5_BUFFER_POOL_SHARED=1")
add_test(NAME agc_buffer_pool_small_device_no_staging COMMAND agc_buffer_pool_tests --small-device-retention)
set_tests_properties(agc_buffer_pool_small_device_no_staging PROPERTIES ENVIRONMENT
"APS5_STAGING_POOL_MIB=0")
if(WIN32)
set_tests_properties(agc_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_buffer_pool agc_buffer_pool_small_device_shared agc_buffer_pool_small_device_no_staging PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@@ -1003,6 +1105,7 @@ add_custom_command(
add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
tests/Recorder.cpp
tests/HostImportLifetime.cpp
tests/ResidentPresent.cpp
${agcTriangleHeaders}
${agcSampleLodShaderHeader}
@@ -1029,6 +1132,11 @@ set_target_properties(agc_driver_recorder_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_recorder_tests)
if(BUILD_TESTING)
set_target_properties(agc_driver_recorder_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_recorder_device_selection COMMAND agc_driver_recorder_tests --device-selection-only)
if(WIN32)
set_tests_properties(agc_recorder_device_selection PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_pipeline_cache COMMAND agc_driver_recorder_tests --pipeline-cache-only)
add_test(NAME agc_pipeline_cache_file COMMAND agc_driver_recorder_tests --pipeline-cache-file-only)
add_test(NAME agc_pipeline_library COMMAND agc_driver_recorder_tests --pipeline-library-only)
@@ -1051,7 +1159,13 @@ if(BUILD_TESTING)
if(WIN32)
add_test(NAME agc_import_watch_other_family COMMAND agc_driver_recorder_tests --import-watch-other-family-only)
set_tests_properties(agc_import_watch_other_family PROPERTIES SKIP_RETURN_CODE 77)
set_tests_properties(agc_import_watch agc_import_watch_other_family PROPERTIES ENVIRONMENT_MODIFICATION
add_test(NAME agc_import_settle_deferred COMMAND agc_driver_recorder_tests --deferred-settle-only)
add_test(NAME agc_import_settle_deferred_other_family COMMAND agc_driver_recorder_tests --deferred-settle-other-family-only)
add_test(NAME agc_import_settle_first_use COMMAND agc_driver_recorder_tests --settle-first-use-only)
add_test(NAME agc_import_settle_earlier_import COMMAND agc_driver_recorder_tests --settle-earlier-import-only)
add_test(NAME agc_import_settle_retire COMMAND agc_driver_recorder_tests --settle-retire-only)
set_tests_properties(agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
set_tests_properties(agc_import_watch agc_import_watch_other_family agc_import_settle_deferred agc_import_settle_deferred_other_family agc_import_settle_first_use agc_import_settle_earlier_import agc_import_settle_retire PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_unchanged_cpu_stamps COMMAND agc_driver_recorder_tests --unchanged-cpu-stamps-only)
@@ -1074,6 +1188,12 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_storage_upload_lifetime COMMAND agc_driver_recorder_tests --storage-upload-lifetime-only)
add_test(NAME agc_sampled_storage_base_level_usage COMMAND agc_driver_recorder_tests --sampled-storage-base-level-usage-only)
set_tests_properties(agc_sampled_storage_base_level_usage PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_sampled_storage_base_level_usage PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_storage_upload_failure COMMAND agc_driver_recorder_tests --storage-upload-failure-only)
set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
@@ -1166,10 +1286,28 @@ if(BUILD_TESTING)
set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_sample_dumps COMMAND agc_driver_recorder_tests --sample-dumps-only)
set_tests_properties(agc_sample_dumps PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_draw_descriptors COMMAND agc_driver_recorder_tests --draw-descriptors-only)
set_tests_properties(agc_draw_descriptors PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
if(WIN32)
set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_host_import_lifetime COMMAND agc_driver_recorder_tests --host-import-lifetime-only)
add_test(NAME agc_driver_host_import_mutation_race COMMAND agc_driver_recorder_tests --host-import-mutation-race-only)
set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race PROPERTIES
ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_driver_unit_shadow COMMAND agc_driver_recorder_tests --unit-shadow-only)
add_test(NAME agc_driver_unit_shadow_budget COMMAND agc_driver_recorder_tests --unit-shadow-only)
set_tests_properties(agc_driver_unit_shadow PROPERTIES
ENVIRONMENT "APS5_PIN_WAIT_MS=2000" SKIP_RETURN_CODE 77 TIMEOUT 30)
set_tests_properties(agc_driver_unit_shadow_budget PROPERTIES
ENVIRONMENT "APS5_PIN_WAIT_MS=2000;APS5_UNIT_SHADOW_MIB=8" SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_host_import_lifetime agc_driver_host_import_mutation_race agc_driver_unit_shadow agc_driver_unit_shadow_budget PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_property(TEST agc_driver_host_import_lifetime APPEND PROPERTY ENVIRONMENT_MODIFICATION
"APS5_RELEASE_UNDER_LOCK=unset:" "APS5_RELEASE_ON_UNLOCK=unset:")
endif()
add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp)
@@ -1186,6 +1324,23 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_command_batch_failure_tests EXCLUDE_FROM_ALL
tests/CommandBatch.cpp tests/CommandBatchFailure.cpp Graphics/src/Resources.cpp tests/AllocationFailure.cpp)
target_include_directories(agc_command_batch_failure_tests PRIVATE
${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_command_batch_failure_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_command_batch_failure_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_command_batch_failure_tests)
if(BUILD_TESTING)
set_target_properties(agc_command_batch_failure_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_command_batch_failure COMMAND agc_command_batch_failure_tests)
set_tests_properties(agc_command_batch_failure PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_command_batch_failure PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_host_import_retire_tests EXCLUDE_FROM_ALL tests/HostImportRetireHold.cpp)
target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc)
@@ -1275,6 +1430,22 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
tests/DepthSurfaceReuse.cpp
)
if(BUILD_TESTING)
add_test_executable(agc_descriptor_cache_failure_tests
tests/DescriptorCacheFailure.cpp
Graphics/src/ShaderResources.cpp)
target_include_directories(agc_descriptor_cache_failure_tests PRIVATE
$<TARGET_PROPERTY:libSceAgcDriver,INCLUDE_DIRECTORIES>)
target_link_libraries(agc_descriptor_cache_failure_tests PRIVATE libSceAgcDriver shader_recompiler_agc_driver libc libkernel)
configure_windows_unwind(agc_descriptor_cache_failure_tests)
add_test(NAME agc_descriptor_cache_failure COMMAND agc_descriptor_cache_failure_tests)
set_tests_properties(agc_descriptor_cache_failure PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(agc_descriptor_cache_failure PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
target_include_directories(agc_driver_graphics_tests PRIVATE
${LIBS_INCLUDE_DIR}
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
@@ -1375,14 +1546,33 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
configure_windows_unwind(${agcExecutionTest})
list(APPEND agcExecutionTests ${agcExecutionTest})
endforeach()
if(BUILD_TESTING)
add_test(NAME agc_driver_address_passes_targets_split COMMAND agc_driver_address_passes_tests --targets)
set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1" SKIP_RETURN_CODE 77 TIMEOUT 60)
if(WIN32)
set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp)
message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution")
endif()
endforeach()
if(ANYPS5_ENABLE_SPIRV_TOOLS)
target_compile_definitions(agc_driver_stencil_null_feedback_tests PRIVATE ANYPS5_ENABLE_SPIRV_TOOLS=1)
target_link_libraries(agc_driver_stencil_null_feedback_tests PRIVATE SPIRV-Tools-static)
endif()
if(NOT ANYPS5_AGC_EXECUTION_TESTS OR "PooledBufferRange" IN_LIST ANYPS5_AGC_EXECUTION_TESTS)
target_sources(agc_driver_pooled_buffer_range_tests PRIVATE
Graphics/src/BufferPool.cpp Graphics/src/Resources.cpp tests/AllocationFailure.cpp tests/CommandBatchFailure.cpp)
endif()
target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber)
target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel)
target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc)
target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
@@ -1390,19 +1580,46 @@ target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $<TARGET_P
target_include_directories(agc_driver_sample_multisample_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
if(BUILD_TESTING)
add_test(NAME agc_driver_store_pending_identity COMMAND agc_driver_store_over_untracked_tests --pending-identity)
set_tests_properties(agc_driver_store_pending_identity PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_store_pending_identity PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(BUILD_TESTING)
add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input)
set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state)
add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state)
set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state)
set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_dithered_alpha_to_coverage COMMAND agc_driver_alpha_to_coverage_tests --dithered8)
set(multisampledDccClearTests)
foreach(samples IN ITEMS 2 4 8)
set(testName "agc_driver_msaa_dcc_clear_${samples}")
add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-fast-clear ${samples})
list(APPEND multisampledDccClearTests ${testName})
endforeach()
set_tests_properties(agc_driver_msaa_dcc_state ${multisampledDccClearTests} PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(${multisampledDccClearTests} PROPERTIES SKIP_RETURN_CODE 77)
set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_msaa_dcc_state PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@@ -1414,10 +1631,16 @@ target_include_directories(agc_driver_reversed_order_blend_tests PRIVATE $<TARGE
target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_packed_float_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobManager libSceAgc libkernel)
target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_color1555_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_color565_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
foreach(stage IN ITEMS vert frag)
set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage})
set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv)
@@ -1438,8 +1661,12 @@ target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestSh
target_include_directories(agc_driver_prepared_exports_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_pipeline_journal_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_prepared_publication_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
@@ -1447,6 +1674,8 @@ target_link_libraries(agc_driver_interpolation_modes_tests PRIVATE shader_recomp
target_include_directories(agc_driver_prepared_unregistered_graphics_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_link_libraries(agc_driver_guest_memory_lifetime_tests PRIVATE libkernel)
target_include_directories(agc_driver_prepared_shaders_tests PRIVATE
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
)
@@ -1498,8 +1727,10 @@ if(BUILD_TESTING)
add_test(NAME agc_shader_layout_pair_off COMMAND agc_shader_memory_tests --layout-pair)
set_tests_properties(agc_shader_layout_pair PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(agc_shader_layout_pair_off PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_NO_PERF_LAYOUT_PAIR=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_shader_entry_lane_proof_budget COMMAND agc_shader_memory_tests --entry-lane-proof-budget)
set_tests_properties(agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_shader_bindless agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests)
if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests")
@@ -1519,11 +1750,28 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240)
if(WIN32)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(WIN32)
add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint)
set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_registration_trace PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30)
set_property(TEST agc_driver_registration_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION
"APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:")
add_test(NAME agc_driver_registration_trace_disabled COMMAND ${CMAKE_COMMAND} -E env
--unset=APS5_TRACE_FRAMES --unset=APS5_TRACE_SECONDS --unset=APS5_TRACE_SUMMARY_EVERY
$<TARGET_FILE:agc_driver_registration_trace_tests> disabled)
set_tests_properties(agc_driver_registration_trace_disabled PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_registration_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes)
set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement)
@@ -1536,10 +1784,26 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_native_depth_clears agc_driver_native_depth_clear_refusals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30)
add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal)
add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build)
add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool)
set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_lds_lock_budget COMMAND agc_driver_lds_mixed_width_atomics_tests --lock-budget)
set_tests_properties(agc_driver_lds_lock_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests)
set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1"
@@ -1626,6 +1890,48 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_uint8x4_color_target_no_resident PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set(agcGuestMemoryLifetimeTests agc_driver_guest_memory_lifetime)
foreach(mode IN ITEMS copy direct locked heap heap_uncached)
set(testName agc_driver_guest_memory_lifetime_${mode})
if(mode STREQUAL "heap_uncached")
add_test(NAME ${testName} COMMAND agc_driver_guest_memory_lifetime_tests heap)
else()
add_test(NAME ${testName} COMMAND agc_driver_guest_memory_lifetime_tests ${mode})
endif()
list(APPEND agcGuestMemoryLifetimeTests ${testName})
endforeach()
set_tests_properties(${agcGuestMemoryLifetimeTests} PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000;APS5_HEAP_CACHE_MIB=4"
SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(agc_driver_guest_memory_lifetime_heap_uncached PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000;APS5_HEAP_CACHE_MIB=0")
if(WIN32)
set_tests_properties(${agcGuestMemoryLifetimeTests} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64" AND Python3_Interpreter_FOUND)
add_library(agc_guest_memory_lifetime_fixture SHARED tests/GuestMemoryLifetimeFixture.cpp)
target_include_directories(agc_guest_memory_lifetime_fixture PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include)
target_link_libraries(agc_guest_memory_lifetime_fixture PRIVATE libSceAgcDriver SDL2-static libc)
add_dependencies(agc_guest_memory_lifetime_fixture relinker libkernel)
set_target_properties(agc_guest_memory_lifetime_fixture PROPERTIES
OUTPUT_NAME GuestMemoryFixture SUFFIX ".prx" CXX_EXTENSIONS OFF
BUILD_WITH_INSTALL_RPATH TRUE INSTALL_RPATH "$ORIGIN")
add_test(NAME agc_driver_guest_memory_lifetime_converted COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_guest_memory_lifetime.py
${CMAKE_SOURCE_DIR}/core/relinker/relinker/tests
$<TARGET_FILE:relinker>
$<TARGET_FILE:agc_guest_memory_lifetime_fixture>
$<TARGET_FILE:libkernel>
$<TARGET_FILE:libc>
$<TARGET_FILE:libSceAgcDriver>)
set_tests_properties(agc_driver_guest_memory_lifetime_converted PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000" SKIP_RETURN_CODE 77 TIMEOUT 30)
endif()
set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE)
set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
foreach(flatAperturesPart RANGE 13)
@@ -1657,13 +1963,31 @@ if(BUILD_TESTING)
endif()
add_test(NAME agc_driver_pm4_failure COMMAND agc_driver_pm4_tests failure)
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
set(pm4RingCases "ring|APS5_PM4_RING=4" "ring-empty|APS5_PM4_RING=16" "ring-disabled|APS5_PM4_RING=0" "ring-invalid|APS5_PM4_RING=4x" "ring-report|APS5_PM4_RING=2" "stall-watchdog|APS5_STALL_WATCHDOG_SECS=1"
"stall-watchdog-invalid|APS5_STALL_WATCHDOG_SECS=1x" "stall-watchdog-rewind|APS5_STALL_WATCHDOG_SECS=1" "stall-watchdog-order|APS5_STALL_WATCHDOG_SECS=1")
if(WIN32)
list(APPEND pm4RingCases "ring-report-heap|APS5_PM4_RING=2")
endif()
foreach(pm4RingCase IN LISTS pm4RingCases)
string(REPLACE "|" ";" pm4RingCase "${pm4RingCase}")
list(GET pm4RingCase 0 pm4RingMode)
list(GET pm4RingCase 1 pm4RingEnvironment)
string(REPLACE "-" "_" pm4RingTest "agc_driver_pm4_${pm4RingMode}")
add_test(NAME ${pm4RingTest} COMMAND agc_driver_pm4_tests ${pm4RingMode})
set_tests_properties(${pm4RingTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;${pm4RingEnvironment}" SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(${pm4RingTest} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure)
add_test(NAME agc_descriptor_cache COMMAND agc_driver_graphics_tests --descriptor-cache)
add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown)
set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_pm4_failure_inline COMMAND agc_driver_pm4_tests failure)
set_tests_properties(agc_driver_pm4_failure_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_pm4_failure agc_driver_pm4_failure_inline agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_pm4_failure agc_driver_pm4_failure_inline agc_descriptor_pool_reset_failure agc_descriptor_cache agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(NOT WIN32)
@@ -1699,6 +2023,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_stencil_replay_no_gpl PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_pipelined_dispatch_item COMMAND agc_driver_pipelined_dispatch_tests)
set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINE_DISPATCH=1" SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv13_image_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests)
if(NOT TARGET ${agcTest})
@@ -1730,7 +2060,7 @@ if(BUILD_TESTING)
endif()
endforeach()
endforeach()
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync)
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure deferred deferred_undecodable registered_workgroup registered_workgroup_sync lazy lazy_sync)
set(preparedTest "agc_driver_prepared_shaders_${preparedMode}")
if(preparedMode STREQUAL "indirect")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect)
@@ -1738,6 +2068,8 @@ if(BUILD_TESTING)
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred)
elseif(preparedMode STREQUAL "deferred_undecodable")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --deferred-undecodable)
elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --lazy)
elseif(preparedMode STREQUAL "registered_workgroup" OR preparedMode STREQUAL "registered_workgroup_sync")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --registered-workgroup)
elseif(preparedMode STREQUAL "debug_failure")
@@ -1751,6 +2083,11 @@ if(BUILD_TESTING)
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_DISPATCH_CACHE=1")
elseif(preparedMode STREQUAL "registered_workgroup_sync")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1")
elseif(preparedMode STREQUAL "lazy" OR preparedMode STREQUAL "lazy_sync")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_LAZY_SHADER_PREPARE=1" "APS5_SUSPEND_DRAIN=1")
if(preparedMode STREQUAL "lazy_sync")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_SYNC_SHADER_PREPARE=1")
endif()
elseif(preparedMode STREQUAL "debug" OR preparedMode STREQUAL "debug_failure")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_PROFILE_DRAW=1" "APS5_NO_UNLOCKED_DEVICE=1")
endif()
@@ -1758,6 +2095,17 @@ if(BUILD_TESTING)
set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined)
add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers)
add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer)
add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer)
add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache)
add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held)
add_test(NAME agc_driver_shutdown_threads_exit_after_start COMMAND agc_driver_shutdown_threads_tests --exit-after-start)
set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered)
set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
@@ -1872,6 +2220,25 @@ endif()
set_target_properties(agc_shader_replay PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_replay)
if(BUILD_TESTING)
add_test_executable(agc_shader_replay_requests_tests tests/ShaderReplayRequests.cpp)
target_link_libraries(agc_shader_replay_requests_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_shader_replay_requests_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_replay_requests_tests)
add_dependencies(agc_shader_replay_requests_tests agc_shader_replay)
set(agcShaderReplayCapture "${CMAKE_CURRENT_BINARY_DIR}/shader-replay-capture")
add_test(NAME agc_shader_replay_requests COMMAND agc_shader_replay_requests_tests "${agcShaderReplayCapture}")
set_tests_properties(agc_shader_replay_requests PROPERTIES FIXTURES_SETUP agc_shader_replay_capture TIMEOUT 30)
add_test(NAME agc_shader_replay_tool COMMAND ${CMAKE_COMMAND} -DTOOL=$<TARGET_FILE:agc_shader_replay> "-DCAPTURE=${agcShaderReplayCapture}"
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ShaderReplayTool.cmake)
set_tests_properties(agc_shader_replay_tool PROPERTIES FIXTURES_REQUIRED agc_shader_replay_capture TIMEOUT 30)
set_tests_properties(agc_shader_replay_requests agc_shader_replay_tool PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
if(WIN32)
set_tests_properties(agc_shader_replay_requests agc_shader_replay_tool PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_executable(agc_pm4_replay EXCLUDE_FROM_ALL
tools/Pm4Replay.cpp
tools/Pm4Analysis.cpp
@@ -2345,3 +2712,22 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_constant_select_tests EXCLUDE_FROM_ALL tests/ConstantSelect.cpp)
target_link_libraries(agc_constant_select_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_constant_select_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_constant_select_tests)
if(BUILD_TESTING)
set_target_properties(agc_constant_select_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_constant_select COMMAND agc_constant_select_tests)
if(WIN32)
set_tests_properties(agc_constant_select PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(BUILD_TESTING)
get_directory_property(agcRegisteredTests TESTS)
list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$")
set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1")
endif()
@@ -0,0 +1,418 @@
#pragma once
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libc/include/general/LogMacros.hpp"
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <deque>
#include <initializer_list>
#include <map>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>
namespace AgcDriver {
struct LogLiteral {
const char* text;
};
namespace AsyncLogDetail {
template<typename T>
inline constexpr bool Deferrable = std::is_arithmetic_v<T> || std::is_enum_v<T> || std::is_same_v<T, LogLiteral>;
inline const char* Unwrap(LogLiteral value) {
return value.text;
}
template<typename T>
T Unwrap(T value) {
return value;
}
class Formatter {
public:
virtual ~Formatter() = default;
virtual void Format(std::string& out) const = 0;
};
template<typename... TArgs>
class Bound final : public Formatter {
public:
Bound(const char* format, TArgs... args) : format(format), values(args...) {}
void Format(std::string& out) const override {
std::apply([&](const auto&... value) { Append(out, value...); }, values);
}
private:
void Append(std::string& out, const auto&... value) const {
char small[512];
const int size = std::snprintf(small, sizeof(small), format, Unwrap(value)...);
if (size < 0) return;
if (static_cast<std::size_t>(size) < sizeof(small)) {
out.append(small, static_cast<std::size_t>(size));
return;
}
std::string large(static_cast<std::size_t>(size) + 1, '\0');
std::snprintf(large.data(), large.size(), format, Unwrap(value)...);
out.append(large.data(), static_cast<std::size_t>(size));
}
const char* format;
std::tuple<TArgs...> values;
};
struct Entry {
std::FILE* stream = nullptr;
long long micros = -1;
std::string text;
std::unique_ptr<Formatter> deferred;
std::size_t cost = 0;
};
}
class AsyncLog {
public:
using Clock = std::chrono::steady_clock;
static constexpr std::size_t Capacity = 16u * 1024u * 1024u;
static AsyncLog& Get() {
static AsyncLog* const instance = new AsyncLog();
return *instance;
}
int Write(std::FILE* stream, const char* text, std::size_t length) {
const auto start = Clock::now();
if (!asynchronous) return WriteNow(stream, text, length);
AsyncLogDetail::Entry entry;
entry.stream = stream;
entry.text.assign(text, length);
entry.cost = sizeof(entry) + length;
Enqueue(std::move(entry));
callerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed);
return 0;
}
template<typename... TArgs>
void Defer(std::FILE* stream, const char* format, TArgs... args) {
static_assert((AsyncLogDetail::Deferrable<TArgs> && ...), "deferred log arguments are numbers, enums or LogLiteral");
const auto start = Clock::now();
const auto micros = LogTimestampMicros();
auto bound = std::make_unique<AsyncLogDetail::Bound<TArgs...>>(format, args...);
if (!asynchronous) {
std::string text;
if (micros >= 0) AppendTimestamp(text, micros);
bound->Format(text);
WriteNow(stream, text.data(), text.size());
return;
}
AsyncLogDetail::Entry entry;
entry.stream = stream;
entry.micros = micros;
entry.deferred = std::move(bound);
entry.cost = sizeof(entry) + sizeof(AsyncLogDetail::Bound<TArgs...>);
Enqueue(std::move(entry));
callerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed);
}
bool Admit(const std::string& line) {
const auto now = Clock::now();
std::string summary;
{
std::lock_guard lock(coalesceMutex);
auto found = windows.find(line);
if (found == windows.end()) {
if (windows.size() >= CoalesceKeys) return true;
windows.emplace(line, Window{now, 1, 0});
return true;
}
auto& window = found->second;
if (now - window.first < CoalesceWindow) {
if (++window.seen <= PrintedRepeats) return true;
++window.suppressed;
coalescedTotal.fetch_add(1, std::memory_order_relaxed);
return false;
}
summary = Summary(found->first, window.suppressed);
window = Window{now, 1, 0};
}
if (!summary.empty()) WriteNow(stderr, summary.data(), summary.size());
return true;
}
void Flush() {
if (!asynchronous) return;
std::unique_lock lock(mutex);
const auto target = queued.load(std::memory_order_relaxed);
idle.wait(lock, [&] { return finished.load(std::memory_order_relaxed) >= target; });
}
bool FlushFor(std::chrono::milliseconds limit) {
if (!asynchronous) return true;
const auto target = queued.load(std::memory_order_acquire);
const auto deadline = Clock::now() + limit;
while (finished.load(std::memory_order_acquire) < target) {
if (Clock::now() >= deadline) return false;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
return true;
}
static void FlushAtEnd() {
auto& log = Get();
if (log.abandoned.load(std::memory_order_acquire)) return;
if (!log.FlushFor(EndFlushLimit)) log.abandoned.store(true, std::memory_order_release);
}
std::unique_lock<std::mutex> Hold() {
return std::unique_lock(mutex);
}
private:
struct Window {
Clock::time_point first;
std::uint64_t seen;
std::uint64_t suppressed;
};
static constexpr std::size_t CoalesceKeys = 128;
static constexpr std::uint64_t PrintedRepeats = 3;
static constexpr std::chrono::seconds CoalesceWindow{10};
static constexpr std::chrono::seconds StatsWindow{10};
static constexpr std::chrono::milliseconds Tick{500};
static constexpr std::chrono::milliseconds EndFlushLimit{2000};
AsyncLog() {
asynchronous = std::getenv("APS5_LOG_SYNC") == nullptr;
const char* stats = std::getenv("APS5_LOG_STATS");
statsEnabled = stats == nullptr || std::string(stats) != "0";
lastStats = Clock::now();
if (!asynchronous) return;
LogSink().store(&AsyncLog::Sink, std::memory_order_release);
std::atexit(&AsyncLog::FlushAtEnd);
InitializeWaits();
std::thread([this] { Run(); }).detach();
LibcSetEndFlush_nid_no_patch(&AsyncLog::FlushAtEnd);
}
void InitializeWaits() {
std::unique_lock lock(mutex);
for (auto* condition : {&wake, &space, &idle}) static_cast<void>(condition->wait_for(lock, std::chrono::nanoseconds(0)));
}
static int Sink(std::FILE* stream, const char* text, std::size_t length) {
return Get().Write(stream, text, length);
}
static std::uint64_t Elapsed(Clock::time_point start) {
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(Clock::now() - start).count());
}
static void AppendTimestamp(std::string& out, long long micros) {
char prefix[64];
const int size = FormatLogTimestamp(prefix, sizeof(prefix), micros);
if (size > 0) out.append(prefix, static_cast<std::size_t>(size));
}
static std::string Summary(const std::string& line, std::uint64_t suppressed) {
if (suppressed == 0) return {};
std::string text = line;
while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) text.pop_back();
text += " (repeated " + std::to_string(suppressed) + " more times in the last 10 s)\n";
return text;
}
static int WriteNow(std::FILE* stream, const char* text, std::size_t length) {
std::fwrite(text, 1, length, stream);
return std::fflush(stream);
}
void Enqueue(AsyncLogDetail::Entry&& entry) {
bool notify = false;
{
std::unique_lock lock(mutex);
space.wait(lock, [&] { return bytes == 0 || bytes + entry.cost <= Capacity; });
bytes += entry.cost;
peak = std::max(peak, bytes);
pending.push_back(std::move(entry));
queued.fetch_add(1, std::memory_order_release);
notify = sleeping;
sleeping = false;
}
if (notify) wake.notify_one();
}
void Run() noexcept {
std::deque<AsyncLogDetail::Entry> batch;
std::string buffer;
for (;;) {
{
std::unique_lock lock(mutex);
sleeping = true;
wake.wait_for(lock, Tick, [&] { return !pending.empty(); });
sleeping = false;
batch.swap(pending);
bytes = 0;
}
space.notify_all();
if (!batch.empty()) {
const auto start = Clock::now();
const auto count = batch.size();
try {
Drain(batch, buffer);
} catch (...) {
buffer.clear();
}
batch.clear();
loggerNanoseconds.fetch_add(Elapsed(start), std::memory_order_relaxed);
{
std::lock_guard lock(mutex);
finished.fetch_add(count, std::memory_order_release);
}
idle.notify_all();
}
try {
Housekeeping();
} catch (...) {
}
}
}
void Drain(std::deque<AsyncLogDetail::Entry>& batch, std::string& buffer) {
std::FILE* current = nullptr;
buffer.clear();
std::uint64_t outLines = 0;
std::uint64_t errLines = 0;
std::uint64_t written = 0;
const auto emit = [&] {
if (current != nullptr && !buffer.empty()) {
written += buffer.size();
WriteNow(current, buffer.data(), buffer.size());
}
buffer.clear();
};
for (auto& entry : batch) {
if (entry.stream != current) {
emit();
current = entry.stream;
}
if (entry.deferred) {
if (entry.micros >= 0) AppendTimestamp(buffer, entry.micros);
entry.deferred->Format(buffer);
} else {
buffer += entry.text;
}
(entry.stream == stderr ? errLines : outLines) += 1;
if (buffer.size() >= 65536) emit();
}
emit();
stdoutLines.fetch_add(outLines, std::memory_order_relaxed);
stderrLines.fetch_add(errLines, std::memory_order_relaxed);
loggedBytes.fetch_add(written, std::memory_order_relaxed);
}
void Housekeeping() {
const auto now = Clock::now();
std::vector<std::string> summaries;
{
std::lock_guard lock(coalesceMutex);
for (auto it = windows.begin(); it != windows.end();) {
if (now - it->second.first >= CoalesceWindow) {
const auto summary = Summary(it->first, it->second.suppressed);
if (!summary.empty()) summaries.push_back(summary);
it = windows.erase(it);
} else {
++it;
}
}
}
for (const auto& summary : summaries) WriteNow(stderr, summary.data(), summary.size());
if (now - lastStats < StatsWindow) return;
const double seconds = std::chrono::duration<double>(now - lastStats).count();
lastStats = now;
const auto outLines = stdoutLines.exchange(0, std::memory_order_relaxed);
const auto errLines = stderrLines.exchange(0, std::memory_order_relaxed);
const auto written = loggedBytes.exchange(0, std::memory_order_relaxed);
const auto caller = callerNanoseconds.exchange(0, std::memory_order_relaxed);
const auto logger = loggerNanoseconds.exchange(0, std::memory_order_relaxed);
const auto coalesced = coalescedTotal.exchange(0, std::memory_order_relaxed);
std::size_t high = 0;
{
std::lock_guard lock(mutex);
high = peak;
peak = bytes;
}
if (!statsEnabled || outLines + errLines + coalesced == 0) return;
char line[512];
const int size = std::snprintf(line, sizeof(line), "[log] %.1f s: %llu lines (%llu stdout, %llu stderr), %.1f KiB written, %llu repeats coalesced; callers spent %.2f ms queueing and formatting (%.3f%% of one thread), logger thread %.2f ms, queue peak %.1f KiB\n", seconds, static_cast<unsigned long long>(outLines + errLines), static_cast<unsigned long long>(outLines), static_cast<unsigned long long>(errLines), static_cast<double>(written) / 1024.0, static_cast<unsigned long long>(coalesced), static_cast<double>(caller) / 1e6, static_cast<double>(caller) / 1e9 / seconds * 100.0, static_cast<double>(logger) / 1e6, static_cast<double>(high) / 1024.0);
if (size > 0) WriteNow(stderr, line, std::min(static_cast<std::size_t>(size), sizeof(line) - 1));
}
bool asynchronous = true;
bool statsEnabled = true;
std::mutex mutex;
std::condition_variable wake;
std::condition_variable space;
std::condition_variable idle;
std::deque<AsyncLogDetail::Entry> pending;
std::size_t bytes = 0;
std::size_t peak = 0;
bool sleeping = false;
std::atomic<std::uint64_t> queued{0};
std::atomic<std::uint64_t> finished{0};
std::atomic<bool> abandoned{false};
std::mutex coalesceMutex;
std::map<std::string, Window> windows;
Clock::time_point lastStats;
std::atomic<std::uint64_t> stdoutLines{0};
std::atomic<std::uint64_t> stderrLines{0};
std::atomic<std::uint64_t> loggedBytes{0};
std::atomic<std::uint64_t> callerNanoseconds{0};
std::atomic<std::uint64_t> loggerNanoseconds{0};
std::atomic<std::uint64_t> coalescedTotal{0};
};
inline void InstallAsyncLog_nid_no_patch() {
static_cast<void>(AsyncLog::Get());
}
inline void FlushAsyncLog_nid_no_patch() {
AsyncLog::Get().Flush();
}
inline void FlushAsyncLogAtEnd_nid_no_patch() {
AsyncLog::FlushAtEnd();
}
std::unique_lock<std::mutex> HoldAsyncLog();
template<typename... TArgs>
void LogCoalescedStderr_nid_no_patch(const char* format, TArgs... args) {
char small[1024];
const int size = std::snprintf(small, sizeof(small), format, args...);
if (size < 0) return;
std::string line = static_cast<std::size_t>(size) < sizeof(small) ? std::string(small, static_cast<std::size_t>(size)) : std::string(static_cast<std::size_t>(size), '\0');
if (static_cast<std::size_t>(size) >= sizeof(small)) std::snprintf(line.data(), line.size() + 1, format, args...);
if (!AsyncLog::Get().Admit(line)) return;
std::fwrite(line.data(), 1, line.size(), stderr);
std::fflush(stderr);
}
}
#define AGC_LOG_DEFERRED(stream, fmt, ...) \
::AgcDriver::AsyncLog::Get().Defer(stream, "[%s:%d %s] " fmt "\n", ::AgcDriver::LogLiteral{_APS5_FILE_}, __LINE__, ::AgcDriver::LogLiteral{TrimNidPostfix(__func__)}, __VA_ARGS__)
@@ -22,6 +22,7 @@ struct DisplayBuffer {
std::uint32_t pitchInPixel = 0;
std::uint64_t dccAddress = 0;
std::uint64_t dccClearColor = 0;
std::uint32_t alignment = 65536;
};
std::size_t DisplayBufferSize(const DisplayBuffer& buffer);
@@ -29,6 +29,7 @@ struct DispatchVariant {
std::vector<std::uint32_t> words;
std::uint64_t forgetSerial = 0;
std::shared_ptr<const ShaderRecompiler::RecompileResult> compiled;
std::array<std::uint32_t, 4> meshIndexDescriptor{};
std::shared_ptr<const ShaderSnapshot> shader;
@@ -40,6 +41,9 @@ struct DispatchVariant {
std::uint32_t flatBinding = NoFlatBinding;
std::uint32_t pushOffset = 0;
std::vector<std::int8_t> runBases;
std::uint64_t relocatableWords = 0;
std::vector<std::pair<std::uint32_t, std::uint32_t>> shaderDataPatches;
std::shared_ptr<const ShaderRecompiler::ShaderVertexStageInfo> vertexInfo;
std::shared_ptr<ShaderMemory> memory;
@@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWCACHE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWCACHE_HPP
#include "UserDataUse.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Dispatch/DispatchCache.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include <array>
@@ -30,6 +31,7 @@ struct DrawProgram {
bool merged = false;
bool mergedPointerRequired = false;
std::uint32_t mergedPointer = 0;
std::uint32_t hiddenUserWords = 0;
};
struct DrawDecode {
@@ -118,6 +120,36 @@ struct StageCapture {
std::vector<ShaderRecompiler::MemoryRegion> regions;
std::uint64_t forgetSerial = 0;
std::uint32_t pushOffset = 0;
ShaderRecompiler::SrtUserDataUse userDataUse;
};
struct RelocatableUserWord {
std::uint32_t reg;
std::vector<std::pair<std::int64_t, std::uint32_t>> reads;
};
struct DrawRelocation {
std::vector<RelocatableUserWord> words;
std::vector<std::uint32_t> banned;
};
struct DrawUserPatch {
std::size_t program;
std::uint32_t index, value;
};
struct DrawRelocationPending {
std::uint64_t key = 0, alias = 0, identity = 0;
};
struct DrawRelocationTrial {
static constexpr std::uint32_t Window = 1024, MinimumHitShare = 16, IdleDraws = 16384, ProbationAdmissions = 2;
std::uint32_t lookups = 0, hits = 0, idle = 0, admissions = 0;
bool productive = false;
};
struct DrawRelocationCounters {
std::uint64_t lookups = 0, donors = 0, refused = 0, hits = 0, evicted = 0;
};
bool DecodeProgramsCurrent(const DrawDecode& decode, const ShaderRegistry& registry);
@@ -26,12 +26,16 @@ public:
static DrawPipeline& Queue0();
static std::size_t Depth();
static std::size_t QueuedItems();
static bool& Active();
static std::atomic<std::uint64_t>& EpochToken();
static void FollowEpoch(std::uint64_t token);
static unsigned Committers();
void Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress = 0, std::vector<std::byte> labelBytes = {});
void Drain(DrainReason reason, std::uint32_t opcode = 0x100);
void Stop();
static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode);
bool Busy() const { return outstanding.load(std::memory_order_acquire) != 0; }
std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); }
bool Overlaps(std::uint64_t address, std::size_t bytes);
@@ -58,6 +62,7 @@ private:
std::uint32_t idleWaiters = 0;
bool committerWaiting = false;
bool stopped = false;
bool closing = false;
std::thread thread;
std::uint64_t commits = 0;
std::uint64_t commitNs = 0;
@@ -67,6 +72,7 @@ private:
std::array<std::uint64_t, static_cast<std::size_t>(DrainReason::Count)> drains{};
std::array<std::uint64_t, static_cast<std::size_t>(DrainReason::Count)> drainWaitNs{};
std::array<std::uint64_t, 257> drainOpcodes{};
std::array<std::atomic<std::uint64_t>, 257> drainRequests{};
std::uint64_t depthSum = 0;
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
};
@@ -75,6 +75,7 @@ public:
};
DispatchCacheFootprint DispatchCacheSize();
static std::uint64_t DispatchCacheBytes();
std::shared_ptr<const ShaderRegistry> RegisteredShaders();
private:
friend class SampledReadScope;
@@ -102,6 +103,7 @@ private:
static bool& completionDeferred();
void deferCompletion(const Submission& submission);
void submitOpenWork();
void wakeQueueZeroForCompletions();
void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites);
static bool waitFree(const Submission& submission);
bool queue0Before(std::uint64_t received) const;
@@ -111,6 +113,7 @@ private:
static bool stampValidate();
static bool dataHits();
static bool verifyDataHits();
static bool pipelineDispatch();
static std::size_t dispatchVariants();
static bool insertCompare();
static std::size_t dispatchCacheEntries();
@@ -142,8 +145,16 @@ private:
void maybeReportDrawCache(bool profile);
void reportDrawCache(DrawEntryCounters& counters, bool profile);
std::string profileDrawCacheLines(const DrawEntryCounters& counters);
static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape = nullptr, std::array<std::uint64_t, 5>* components = nullptr);
static std::uint64_t drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape = nullptr, std::array<std::uint64_t, 5>* components = nullptr, const DrawRelocation* relocation = nullptr, std::uint64_t* alias = nullptr);
void traceDrawKeyLookup(const std::array<std::uint64_t, 5>& components, bool cacheMiss);
static std::uint64_t drawIdentityKey(const QueueState& queue, const ShaderRegistry& registry);
static bool drawRelocationEnabled();
static std::uint64_t relocatedContentKey(const RelocatableUserWord& word, std::uint32_t value, bool& readable);
static void tagVariantRuns(DispatchVariant& variant, std::span<const ShaderRecompiler::MemoryRegion> regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords);
static void applyShaderDataPatches(DispatchVariant& variant);
void learnPendingRelocation();
void relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std::shared_ptr<DrawEntry>& entry, std::shared_ptr<const DrawDecode>& decode, std::vector<DrawUserPatch>& patches);
void reportDrawRelocation();
static bool sameVertexInfo(const ShaderRecompiler::ShaderVertexStageInfo& a, const ShaderRecompiler::ShaderVertexStageInfo& b);
static bool sameDecode(const DrawDecode& a, const DrawDecode& b);
std::shared_ptr<DrawDecode> decodeDraw(const QueueState& queue, const Submission& submission);
@@ -348,6 +359,11 @@ private:
std::unordered_map<std::uint64_t, std::uint64_t> drawShapes;
std::unordered_set<std::uint64_t> traceShapes, traceKeysSeen, traceKeysEvicted;
std::list<std::uint64_t> drawOrder;
std::unordered_map<std::uint64_t, std::shared_ptr<const DrawRelocation>> drawRelocations;
std::unordered_map<std::uint64_t, std::shared_ptr<DrawEntry>> drawAliases;
DrawRelocationPending drawRelocationPending;
std::unordered_map<std::uint64_t, DrawRelocationTrial> drawRelocationTrials;
DrawRelocationCounters drawRelocationCounters;
std::mutex drawCacheMutex;
std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0;
std::optional<DrawKeySightings> drawKeySightings;
@@ -11,6 +11,7 @@
namespace AgcDriver::DriverDetail {
std::vector<std::pair<std::uint64_t, std::uint64_t>> DrawWriteRanges(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> stages, bool exact);
std::vector<std::pair<std::uint64_t, std::uint64_t>> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded);
bool ExactDrawWrites();
}
@@ -0,0 +1,40 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_QUEUES_SUBMISSIONRING_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_QUEUES_SUBMISSIONRING_HPP
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <span>
#include <string_view>
namespace AgcDriver::DriverDetail {
inline constexpr std::uint32_t SubmissionQueueEnd = 0x58;
std::size_t SubmissionRingCapacity();
std::chrono::seconds StallWatchdogPeriod();
bool ReadSubmissionRingSettings();
void PrepareSubmissionRing(std::uint32_t queue);
void RecordSubmission(std::uint32_t queue, std::uint64_t serial, std::span<const std::uint32_t> commands, const void* guest, bool rewindTail);
void DumpSubmissionRings(std::string_view reason);
class StallWatch {
public:
explicit StallWatch(std::uint32_t queue) : queue(queue) {}
void Progressed() { since = {}; }
void Check(const char* waiter, std::uint64_t value);
private:
std::uint32_t queue;
std::chrono::steady_clock::time_point since{};
bool dumped = false;
};
}
#endif
@@ -117,7 +117,9 @@ void SubmitRegistrationPreparation(std::function<void()> task, const PreparedSha
void PrioritizeRegistrationPreparation(PreparedShaders& prepared);
void AwaitRegistrationPreparation();
void SubmitLibraryPrebuild(std::function<void()> task);
void AwaitLibraryPrebuilds();
void CloseRegistrationPreparation();
unsigned RegistrationPreparationThreads();
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target);
@@ -0,0 +1,60 @@
#pragma once
#include <atomic>
#include <cerrno>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <span>
namespace AgcDriver {
struct FrameDumpIoFailure {
const char* operation = nullptr;
int error = 0;
explicit operator bool() const noexcept { return operation != nullptr; }
};
inline FrameDumpIoFailure WriteBmpStream_nid_no_patch(std::FILE* file, std::uint32_t width, std::uint32_t height, std::span<const std::byte> pixels) noexcept {
const std::uint32_t imageBytes = width * height * 4;
const std::uint32_t header[13] = {0, 0, 54, 40, width, static_cast<std::uint32_t>(-static_cast<std::int32_t>(height)), 1u | (32u << 16u), 0, imageBytes, 2835, 2835, 0, 0};
const std::uint16_t magic = 0x4d42;
const std::uint32_t fileBytes = 54 + imageBytes;
const auto write = [&](const void* data, std::size_t size, const char* operation) {
errno = 0;
if (std::fwrite(data, 1, size, file) != size) return FrameDumpIoFailure{operation, errno};
return FrameDumpIoFailure{};
};
if (const auto failure = write(&magic, 2, "header")) return failure;
if (const auto failure = write(&fileBytes, 4, "header")) return failure;
if (const auto failure = write(header + 1, 48, "header")) return failure;
return write(pixels.data(), pixels.size(), "pixels");
}
inline FrameDumpIoFailure WriteBmpAndClose_nid_no_patch(std::FILE* file, std::uint32_t width, std::uint32_t height, std::span<const std::byte> pixels) noexcept {
auto failure = WriteBmpStream_nid_no_patch(file, width, height, pixels);
errno = 0;
if (std::fclose(file) != 0 && !failure) failure = {"close", errno};
return failure;
}
class FrameDumpOutput {
public:
bool Enabled_nid_no_patch() const noexcept { return enabled.load(std::memory_order_relaxed); }
bool Write_nid_no_patch(const char* path, std::uint32_t width, std::uint32_t height, std::span<const std::byte> pixels) noexcept {
if (!Enabled_nid_no_patch()) return false;
errno = 0;
auto* file = std::fopen(path, "wb");
const auto failure = file != nullptr ? WriteBmpAndClose_nid_no_patch(file, width, height, pixels) : FrameDumpIoFailure{"open", errno};
if (!failure) return true;
if (enabled.exchange(false, std::memory_order_relaxed)) {
std::fprintf(stderr, "[gpu] optional BMP dumps disabled: %s failed (errno=%d)\n", failure.operation, failure.error);
}
return false;
}
private:
std::atomic<bool> enabled{true};
};
}
@@ -26,6 +26,7 @@ struct Commitment {
bool whole = false;
};
Commitment DescribeCommitted(std::uint64_t address, std::size_t bytes, bool writable = false);
void CopyGuestBytes(void* destination, const void* source, std::size_t bytes);
void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t alignment = 1);
// Variants for resources in heaps the guest commits on demand (partially resident textures, GPU
// scratch heaps): uncommitted pages read as zeros, are not compared, and are never stored.
@@ -60,6 +61,7 @@ void WriteChanged(std::uint64_t address, std::span<const std::byte> current, std
// UnchangedSince is always false, so callers fall back to comparing bytes.
bool WriteWatched();
void Unwatch(std::uint64_t address, std::size_t bytes);
void Rewatch(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges);
bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function<bool()>& import, bool staysWatched = true);
void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes));
bool Watched(std::uint64_t address, std::size_t bytes);
@@ -0,0 +1,61 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP
#include "prx/libc/include/CpuTopology.hpp"
#include <algorithm>
#include <bitset>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <thread>
#if defined(__x86_64__) || defined(_M_X64)
#include <immintrin.h>
#endif
namespace AgcDriver {
inline void SpinPause() {
#if defined(__x86_64__) || defined(_M_X64)
_mm_pause();
#endif
}
inline unsigned UsableProcessors() {
const auto count = static_cast<unsigned>(std::bitset<64>(CpuTopology::Get().process).count());
return count != 0 ? count : std::thread::hardware_concurrency();
}
inline std::chrono::nanoseconds HandoffSpinWindowFor(const char* configured, unsigned usableProcessors) {
constexpr std::chrono::microseconds automaticWindow{150};
constexpr unsigned automaticProcessorsAbove = 8;
constexpr long long longestWindowUs = 10000;
if (configured != nullptr) return std::chrono::microseconds(std::clamp<long long>(std::strtoll(configured, nullptr, 10), 0, longestWindowUs));
return usableProcessors > automaticProcessorsAbove ? std::chrono::nanoseconds(automaticWindow) : std::chrono::nanoseconds::zero();
}
inline std::chrono::nanoseconds HandoffSpinWindow() {
static const std::chrono::nanoseconds window = HandoffSpinWindowFor(std::getenv("APS5_HANDOFF_SPIN_US"), UsableProcessors());
return window;
}
template<typename TReady>
bool SpinUntil(std::chrono::nanoseconds window, TReady&& ready) {
if (ready()) return true;
if (window.count() <= 0) return false;
const auto until = std::chrono::steady_clock::now() + window;
do {
SpinPause();
if (ready()) return true;
} while (std::chrono::steady_clock::now() < until);
return false;
}
template<typename TMutex>
void LockSpinning(TMutex& mutex, std::chrono::nanoseconds window) {
if (window.count() > 0 && SpinUntil(window, [&] { return mutex.try_lock(); })) return;
mutex.lock();
}
}
#endif
@@ -21,6 +21,13 @@
#include <utility>
#include <vector>
namespace AgcDriver {
class TraceThread;
}
extern "C" bool AgcDriverTraceRecording_nid_postfix();
extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix();
namespace AgcDriver {
class VulkanDevice;
@@ -532,23 +539,26 @@ public:
}
static bool Recording() {
return recording.load(std::memory_order_relaxed);
return AgcDriverTraceRecording_nid_postfix();
}
static TraceThread* Thread() {
return local;
return AgcDriverTraceThread_nid_postfix();
}
static void StopOutput();
private:
friend bool ::AgcDriverTraceRecording_nid_postfix();
friend TraceThread* ::AgcDriverTraceThread_nid_postfix();
static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present);
static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device);
inline static std::atomic<bool> recording{false};
static std::atomic<bool> recording;
inline static std::atomic<bool> buffering{false};
inline static std::atomic<std::uint64_t> presentCount{0};
inline static thread_local TraceThread* local = nullptr;
static thread_local TraceThread* local;
inline static std::mutex gpuMutex;
inline static std::vector<TraceGpuRange> gpuRanges;
inline static std::uint64_t gpuLost = 0;
@@ -78,6 +78,7 @@ std::uint32_t ReadDrawCount(const DrawParameters::IndirectDraw& indirect);
inline bool IndirectDrawOpcode(std::uint32_t opcode) { return opcode == 0x24 || opcode == 0x25 || opcode == 0x2c || opcode == 0x38; }
inline bool DrawOpcode(std::uint32_t opcode) { return opcode == 0x27 || opcode == 0x2d || opcode == 0x35 || IndirectDrawOpcode(opcode); }
std::string_view KnownName(std::uint32_t header);
std::string Name(std::uint32_t header);
// A PM4 type-2 packet is a one-dword filler (command-buffer padding); type 3 and type 0 carry a
// dword count in bits 29:16. Type 1 is undefined.
@@ -47,7 +47,7 @@ struct Recipe {
std::uint64_t dataWordsHash = 0;
// The template's PresyncSurfaces() as of the attach and the host-import identity they were
// computed under (a surface is CPU-read only while its memory has no import).
std::vector<std::pair<std::uint64_t, std::uint64_t>> presyncSurfaces;
std::vector<Graphics::PresyncSurface> presyncSurfaces;
Graphics::HostImportsProof presyncProof;
// Both false for every recipe (only reusable objects get one); kept for the asserts.
bool needsCompletion = false;
@@ -0,0 +1,14 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_ROBUSTBUFFERACCESS_HPP
#include <vulkan/vulkan.h>
namespace AgcDriver {
inline bool RobustBufferAccess2Usable(VkDriverId driver) {
return driver != VK_DRIVER_ID_AMD_PROPRIETARY;
}
}
#endif
@@ -30,6 +30,8 @@ struct ComputePipelineObjects;
struct RecipeHit;
// The record tail shared by an ordinary dispatch and a recipe hit (VulkanDevice::recordDispatch).
struct RecordedDispatch;
// The module, layout and pipeline a compute dispatch binds (VulkanDevice::createComputePipeline).
struct ComputePipelineObjects;
class VulkanDevice {
public:
@@ -37,6 +39,14 @@ public:
struct CompileOnly {};
explicit VulkanDevice(CompileOnly);
ComputePipelineReplayResult ReplayComputePipeline(const ComputePipelineCapture& capture, std::span<const std::byte> initialCache = {});
// Builds the pipeline of a journaled compute result ahead of its first dispatch (the journal
// replay's hook, on its threads): false when the device already holds a pipeline for the module.
bool ReplayComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants);
// Starts the journal replay if the device has not (a headless device starts it at its first
// draw or dispatch) and joins it (tests; the teardown joins it anyway).
void WaitForPipelineJournalReplay();
// Compute pipelines this device built, by dispatches and by the journal replay (tests).
std::uint64_t ComputePipelinesCreated() const;
~VulkanDevice();
VulkanDevice(const VulkanDevice&) = delete;
VulkanDevice& operator=(const VulkanDevice&) = delete;
@@ -45,9 +55,11 @@ public:
bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const;
ShaderRecompiler::SpirvTarget Target() const;
std::optional<ShaderRecompiler::GeometryStageLimits> GeometryLimits() const;
std::uint32_t MaxFragmentInputComponents() const;
const std::uint32_t* WorkGroupCountLimit() const;
std::uint32_t VendorId() const;
ShaderRecompiler::SpirvTarget ComputeTarget(std::uint32_t waveSize) const;
bool ComputeFullSubgroups() const;
// Distinguishes this device from every earlier one in the process (a memo keyed by the device
// cannot rely on the pointer, which a replacement may reuse).
std::uint64_t Serial() const { return serial; }
@@ -58,6 +70,7 @@ public:
// three-step form below so the GPU wait happens without the mutex.
void WaitIdle();
void PrepareForReplacement();
void startJournalReplay();
bool ImportGuestMemory(const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice);
// Sends recorded work to the GPU without waiting for it. With `reapFirst` it first retires batches
// that already finished, so the in-flight list stays short (APS5_NO_OPPORTUNISTIC_REAP=1 skips
@@ -173,6 +186,10 @@ public:
std::uint64_t created, underGpuMutex;
};
static ComputePipelineCreation ComputePipelineCounts();
struct DispatchMoveCount {
std::uint64_t checked, exact, moved, refused, filtered, rebuilt, kept, served;
};
static DispatchMoveCount DispatchMoveCounts();
void ResolveMemory(std::uint64_t address, std::size_t bytes, bool writable);
void* Window() const;
void Resize(std::uint32_t width, std::uint32_t height);
@@ -232,6 +249,10 @@ public:
double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs;
};
static PresentStatistics PresentCounts();
struct PresyncStatistics {
std::uint64_t checked, presyncs, keyChecks, keyWaits;
};
static PresyncStatistics PresyncCounts();
// Stage A of a dispatch's resource build, run WITHOUT GuestMemory::GpuMutex before Dispatch or
// DispatchIndirect: the binding plan, data buffers, imports already serving the guest buffers,
// read-only copies and the descriptor set (see ShaderResources). The dispatch completes it
@@ -316,12 +337,16 @@ private:
// The device's Graphics::Context: a copy of the one built at setup (its instance functions
// resolved then, its function table filled then), or with APS5_NO_CONTEXT_CACHE=1 built anew.
Graphics::Context graphicsContext() const;
VkPipelineCreateFlags computePipelineFlags() const;
std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const;
VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const;
std::shared_ptr<ComputePipelineObjects> createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
ShaderRecompiler::SpirvTarget buildTarget() const;
Graphics::Context buildContext() const;
// Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups.
IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe);
// The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none.
std::uint64_t presync(std::span<const std::pair<std::uint64_t, std::uint64_t>> surfaces);
std::uint64_t presync(std::span<const Graphics::PresyncSurface> surfaces);
std::shared_ptr<ComputePipelineObjects> computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
// A DISPATCH_INDIRECT's argument path (see DispatchIndirect): GPU-side from the host import, or
// the counts read on the CPU (the record then continues as a direct dispatch).
@@ -94,7 +94,7 @@ void ComputePipelineCapture::Validate() const {
RequireCapture(mainEntry, "compute main entry point missing");
RequireCapture(deviceName.size() < VK_MAX_PHYSICAL_DEVICE_NAME_SIZE && deviceName.find('\0') == std::string::npos, "invalid device name");
RequireCapture((flags & ~(VK_PIPELINE_CREATE_DISPATCH_BASE_BIT | VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR)) == 0, "unsupported pipeline flags");
RequireCapture(stageFlags == 0, "unsupported shader stage flags");
RequireCapture(stageFlags == 0 || (stageFlags == VK_PIPELINE_SHADER_STAGE_CREATE_REQUIRE_FULL_SUBGROUPS_BIT && requiredSubgroupSize != 0), "unsupported shader stage flags");
RequireCapture(requiredSubgroupSize == 0 || (requiredSubgroupSize <= 128 && (requiredSubgroupSize & (requiredSubgroupSize - 1u)) == 0), "invalid required subgroup size");
RequireCapture(sets.size() <= 8, "too many descriptor sets");
std::size_t totalBindings = 0;
@@ -130,7 +130,8 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer) {
throw std::runtime_error(message);
}
require(!halfFloat || buffer.dccAddress == 0, "VideoOut: DCC metadata on a 16-bit float display buffer is not implemented");
require(buffer.address != 0 && (buffer.address & 65535u) == 0, "VideoOut: display buffer requires 64 KiB alignment");
require(buffer.alignment == 65536u || (buffer.alignment == 4096u && buffer.tilingMode == 1), "VideoOut: 4 KiB display buffer alignment is only supported for linear buffers");
require(buffer.address != 0 && (buffer.address & (buffer.alignment - 1u)) == 0, buffer.alignment == 65536u ? "VideoOut: display buffer requires 64 KiB alignment" : "VideoOut: display buffer requires 4 KiB alignment");
require(buffer.tilingMode <= 1, "VideoOut: unsupported display tiling mode");
require(buffer.tilingMode != 0 || buffer.pitchInPixel == 0, "VideoOut: tiled display pitch is unsupported");
require(buffer.dccAddress == 0 || buffer.tilingMode == 0, "VideoOut: a linear display buffer cannot carry DCC metadata");
@@ -249,7 +250,7 @@ std::vector<std::byte> ReadDisplayBuffer(const DisplayBuffer& buffer) {
std::fprintf(stderr, "[gpu] scanout display buffer 0x%llx %ux%u format 0x%016llx at %.1f s\n", static_cast<unsigned long long>(buffer.address), buffer.width, buffer.height, static_cast<unsigned long long>(buffer.pixelFormat), std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
}
GuestMemory::FlushGpuWrites(buffer.address, size);
GuestMemory::CheckRange(reinterpret_cast<const void*>(buffer.address), size, 65536);
GuestMemory::CheckRange(reinterpret_cast<const void*>(buffer.address), size, buffer.alignment);
const auto* source = reinterpret_cast<const std::byte*>(buffer.address);
return DecodeDisplayBuffer(buffer, {source, size});
}
@@ -267,7 +268,7 @@ std::vector<std::byte> DisplayBufferTexels(const DisplayBuffer& buffer, std::spa
std::vector<std::byte> ReadDisplayBufferTexels(const DisplayBuffer& buffer) {
const auto size = DisplayBufferSize(buffer);
GuestMemory::FlushGpuWrites(buffer.address, size);
GuestMemory::CheckRange(reinterpret_cast<const void*>(buffer.address), size, 65536);
GuestMemory::CheckRange(reinterpret_cast<const void*>(buffer.address), size, buffer.alignment);
return DisplayBufferTexels(buffer, {reinterpret_cast<const std::byte*>(buffer.address), size});
}
@@ -2,9 +2,11 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
@@ -15,6 +17,47 @@
namespace AgcDriver::DriverDetail {
namespace {
bool DispatchItemsEnabled() {
static const bool enabled = [] {
const char* text = std::getenv("APS5_PIPELINE_DISPATCH");
return text != nullptr && text[0] != '\0' && text[0] != '0';
}();
return enabled;
}
enum class ItemFallback : std::size_t { Miss, Reads, NoRecipe, Writes, Verify, Count };
constexpr std::array<const char*, static_cast<std::size_t>(ItemFallback::Count)> ItemFallbackNames{"miss", "reads", "no recipe", "writes", "verify"};
std::array<std::atomic<std::uint64_t>, static_cast<std::size_t>(ItemFallback::Count)> itemFallbacks{};
std::atomic<std::uint64_t> itemCommits{0};
std::atomic<std::uint64_t> itemRebuilds{0};
void NoteItemFallback(ItemFallback reason) {
itemFallbacks[static_cast<std::size_t>(reason)].fetch_add(1, std::memory_order_relaxed);
}
void ReportDispatchItems(bool profile) {
static auto lastReport = std::chrono::steady_clock::now();
if (!profile) return;
const auto now = std::chrono::steady_clock::now();
if (now - lastReport < std::chrono::seconds(10)) return;
lastReport = now;
std::string drained;
for (std::size_t i = 0; i < ItemFallbackNames.size(); ++i) {
char item[48];
std::snprintf(item, sizeof(item), " %s %llu", ItemFallbackNames[i], static_cast<unsigned long long>(itemFallbacks[i].load(std::memory_order_relaxed)));
drained += item;
}
std::fprintf(stderr, "[dispatch-item] %llu commit items (%llu recipes rebuilt on the committer); drained instead, cumulative:%s\n", static_cast<unsigned long long>(itemCommits.load(std::memory_order_relaxed)), static_cast<unsigned long long>(itemRebuilds.load(std::memory_order_relaxed)), drained.c_str());
}
}
bool Driver::pipelineDispatch() {
return DispatchItemsEnabled();
}
void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::uint64_t indirectArguments) {
PerformanceTimer timing("Driver.Dispatch");
const auto address = (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20c)) << 8u) | (static_cast<std::uint64_t>(readRegister(queue.shader, 0x20d) & 0xffu) << 40u);
@@ -72,6 +115,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
if (indirectArguments == 0 && (packet[4] & 0x2u) != 0) {
partialGroups = Pm4::PartialGroupThreads({packet[1], packet[2], packet[3]}, {readRegister(queue.shader, 0x207), readRegister(queue.shader, 0x208), readRegister(queue.shader, 0x209)});
}
const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute);
const bool partialLastGroup = partialGroups != std::array<std::uint32_t, 3>{};
if (resolving) {
if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return;
@@ -84,7 +128,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
pendingDispatchPhases().outcome = DispatchOutcome::FillHle;
return;
}
if (DrawPipeline::Active()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15);
if (DrawPipeline::Active() && !pipelineCandidate) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15);
if (const auto htile = Graphics::HtileDepthClearAddress(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads); htile != 0) {
const auto threads = (packet[4] & 0x20u) != 0 ? static_cast<std::uint64_t>(packet[1]) : static_cast<std::uint64_t>(packet[1]) * compute.numThreads[0];
const auto nativeBytes = Graphics::HtileDepthClearBytes_nid_no_patch(std::span(snapshot.code).subspan(codeOffset), userData, compute, packet, indirectArguments);
@@ -223,6 +267,27 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
}
phaseTiming.Phase(PhaseKey);
timing.Mark("decode_key_device_setup");
bool readsClear = false;
if (pipelineCandidate) {
std::vector<ShaderRecompiler::MemoryRegion> reads;
bool entryFound = false;
{
std::lock_guard cacheLock(dispatchCacheMutex);
if (const auto found = dispatchCache.find(key); found != dispatchCache.end()) {
entryFound = true;
for (const auto& variant : found->second->variants) appendEntryRegions(*variant, reads);
}
}
if (!entryFound || noDispatchCache || stampValidate()) {
NoteItemFallback(ItemFallback::Miss);
} else if (verifyDataHits()) {
NoteItemFallback(ItemFallback::Verify);
} else {
readsClear = std::none_of(reads.begin(), reads.end(), [](const auto& region) { return DrawPipeline::Queue0().Overlaps(region.guestAddress, region.bytes.size()); });
if (!readsClear) NoteItemFallback(ItemFallback::Reads);
}
if (!readsClear && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15);
}
if (fromAhead) {
compiledResult = std::move(ahead.compiledResult);
keepVariant = std::move(ahead.keepVariant);
@@ -244,6 +309,19 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
lookupDispatch(address, submission, key, noDispatchCache, traceCache, profile, memory, phaseTiming, phaseMs, compiledResult, keepVariant, captured, liveWords, dataHit, cached, validated, missedEntry, missedDiffering);
}
timing.Mark("lookup_cache");
std::vector<DrawPipeline::Range> itemWrites;
bool pipelined = false;
if (readsClear) {
if (cached && keepVariant != nullptr && keepVariant->recipe.load(std::memory_order_acquire) != nullptr) {
bool bounded = false;
itemWrites = DispatchWriteRanges(*compiledResult, bounded);
pipelined = bounded;
if (!bounded) NoteItemFallback(ItemFallback::Writes);
} else {
NoteItemFallback(ItemFallback::NoRecipe);
}
if (!pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, 0x15);
}
if (cached) {
require(keepVariant != nullptr && keepVariant->shader == registeredShader, "dispatch cache belongs to another registered shader");
captureMs += phaseTiming.Elapsed();
@@ -361,6 +439,64 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
};
phaseTiming.Phase(PhaseSnapshots);
if (pipelined) {
const auto recipe = keepVariant->recipe.load(std::memory_order_acquire);
const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed);
const auto queueId = submission.queue;
itemCommits.fetch_add(1, std::memory_order_relaxed);
DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), groups, address, queueId, recipe, keepVariant, dataHit, epoch] {
DrawPipeline::FollowEpoch(epoch);
GuestMemory::SetCurrentPacket(0x15, queueId);
const auto& compiledCommit = *result;
const bool noteWrites = writeEvidenceEnabled() || traceCapSync();
try {
std::shared_ptr<RecipeHit> recipeHit = localDevice->PrepareRecipe(recipe, false);
std::shared_ptr<const Recipe> builtRecipe;
bool writersNoted = false;
for (;;) {
std::shared_ptr<PreparedDispatch> prepared;
if (recipeHit == nullptr || VulkanDevice::VerifyRecipes()) prepared = localDevice->PrepareDispatch(compiledCommit, snapshots);
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
recordLabelsForPacket(localDevice.get(), queueId);
if (noteWrites && writerKeyedEvidence() && !writersNoted) {
noteWrittenBuffers(address, queueId, compiledCommit);
writersNoted = true;
}
if (recipeHit != nullptr) {
VulkanDevice::IndirectOutcome outcome{0, 0};
const auto recipeResult = localDevice->DispatchRecipe(compiledCommit, groups[0], groups[1], groups[2], 0, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit);
if (recipeResult == RecipeOutcome::Rebuild) {
recipeHit = nullptr;
VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, false);
itemRebuilds.fetch_add(1, std::memory_order_relaxed);
continue;
}
} else {
localDevice->Dispatch(compiledCommit, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), &builtRecipe);
}
break;
}
if (noteWrites && !writerKeyedEvidence()) noteWrittenBuffers(address, queueId, compiledCommit);
if (builtRecipe != nullptr) {
if (dataHit) {
auto own = std::make_shared<Recipe>(*builtRecipe);
own->dataWordsHash = Graphics::ShaderResources::DataWordsHash({ShaderRecompiler::ShaderStage::Compute, keepVariant->compiled.get(), 0});
builtRecipe = std::move(own);
}
keepVariant->recipe.store(std::move(builtRecipe), std::memory_order_release);
VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Attach, false);
}
} catch (const std::exception& error) {
char where[64];
std::snprintf(where, sizeof(where), "compute shader 0x%llx: ", static_cast<unsigned long long>(address));
throw std::runtime_error(where + std::string(error.what()));
}
}, std::move(itemWrites));
ReportDispatchItems(profile);
return;
}
std::shared_ptr<RecipeHit> recipeHit;
if (cached && keepVariant != nullptr && !stampValidate()) {
recipeHit = localDevice->PrepareRecipe(keepVariant->recipe.load(std::memory_order_acquire), indirectArguments != 0);
@@ -372,6 +508,19 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
bool writersNoted = false;
const bool noteWrites = writeEvidenceEnabled() || traceCapSync();
std::optional<Graphics::DepthPlaneClear> depthPlaneClear;
if (indirectArguments == 0 && !partialLastGroup) {
if (const auto clear = Graphics::MatchStorageImageClear(std::span(snapshot.code).subspan(codeOffset), userData, compute, groups)) {
const auto clearAddress = Graphics::StorageImageClearAddress(*clear);
if (clearAddress != 0 && Graphics::DepthSurfaceAt(clearAddress)) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Dispatch);
std::lock_guard clearLock(GuestMemory::GpuMutex());
recordLabelsForPacket(localDevice.get(), submission.queue);
depthPlaneClear.emplace(*clear);
}
}
}
for (;;) {
std::shared_ptr<PreparedDispatch> prepared;
@@ -77,10 +77,26 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
std::shared_ptr<const DrawDecode> decode;
std::shared_ptr<DrawEntry> dataEntry;
std::uint64_t shapeKey = 0;
std::uint64_t drawIdentity = 0, drawAlias = 0;
std::vector<DrawUserPatch> userPatches;
if (registerKey) {
const auto keyStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), &shapeKey, traceDrawCache() ? &drawKeyComponents : nullptr);
std::lock_guard cacheLock(drawCacheMutex);
std::shared_ptr<const DrawRelocation> relocation;
bool aliasFirst = false;
if (drawRelocationEnabled()) {
drawIdentity = drawIdentityKey(queue, *submission.shaders);
std::lock_guard cacheLock(drawCacheMutex);
learnPendingRelocation();
const auto known = drawRelocations.find(drawIdentity);
if (known != drawRelocations.end()) {
auto& trial = drawRelocationTrials[drawIdentity];
if (trial.idle != 0) --trial.idle;
else relocation = known->second;
aliasFirst = relocation != nullptr && trial.productive;
}
}
drawKey = drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), &shapeKey, traceDrawCache() ? &drawKeyComponents : nullptr, aliasFirst ? relocation.get() : nullptr, aliasFirst ? &drawAlias : nullptr);
std::unique_lock cacheLock(drawCacheMutex);
++drawEntryCounters.lookups;
++drawEntryCounters.registerKeyLookups;
if (profile) drawEntryCounters.keyUs += std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - keyStart).count();
@@ -90,6 +106,22 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
decode = entry->decode;
} else {
++drawEntryCounters.absent;
if (relocation != nullptr && !aliasFirst) {
cacheLock.unlock();
drawRegisterKey(queue, *submission.shaders, localDevice->Serial(), nullptr, nullptr, relocation.get(), &drawAlias);
cacheLock.lock();
}
if (drawIdentity != 0) drawRelocationPending = {drawKey, drawAlias, drawIdentity};
if (drawAlias != 0) relocateDrawEntry(queue, drawAlias, entry, decode, userPatches);
if (relocation != nullptr) {
auto& trial = drawRelocationTrials[drawIdentity];
++trial.lookups;
if (trial.lookups >= DrawRelocationTrial::Window) {
trial.productive = trial.hits * DrawRelocationTrial::MinimumHitShare >= trial.lookups;
if (!trial.productive) trial.idle = DrawRelocationTrial::IdleDraws;
trial.lookups = trial.hits = 0;
}
}
if (traceDrawCache()) {
auto& counters = drawEntryCounters;
if (traceKeysEvicted.contains(drawKey)) ++counters.absentEvicted;
@@ -97,7 +129,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
else if (traceShapes.contains(shapeKey)) ++counters.absentUserWords;
else ++counters.absentNewRegisters;
}
if (drawDataHits() && !drawParameters.indirect) {
if (decode == nullptr && drawDataHits() && !drawParameters.indirect) {
if (const auto shape = drawShapes.find(shapeKey); shape != drawShapes.end()) {
const auto kept = drawCache.find(shape->second);
if (kept != drawCache.end() && kept->second->decode != nullptr && kept->second->shape == shapeKey) dataEntry = kept->second;
@@ -113,6 +145,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
maybeReportDrawCache(profile);
}
if (decode != nullptr && !DecodeProgramsCurrent(*decode, *submission.shaders)) {
userPatches.clear();
std::lock_guard cacheLock(drawCacheMutex);
eraseDrawEntry(drawKey, entry);
++drawEntryCounters.absent;
@@ -144,6 +177,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
const auto& graphics = decode->state;
auto pixel = decode->pixel;
std::vector<DrawProgram> programs = decode->programs;
for (const auto& patch : userPatches) programs[patch.program].userData[patch.index] = patch.value;
if (const auto skipped = SkippedShader(); skipped != 0 && std::any_of(programs.begin(), programs.end(), [skipped](const DrawProgram& program) { return program.binary.codeAddress == skipped; })) return drawn();
const auto setMeshIndexBuffer = [&](const Pm4::DrawParameters& parameters) {
if (!graphics.stages.mesh) return;
@@ -238,6 +272,25 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
bool verifyHit = false;
const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u);
lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData);
for (std::size_t i = 0; i < matched.size(); ++i) {
if (matched[i] == nullptr || matched[i]->shaderDataPatches.empty()) continue;
applyShaderDataPatches(*matched[i]);
if (stageData[i].compiled != nullptr) stageData[i].compiled = patchDataWords(*matched[i], stageData[i].words);
}
const auto relocatedDecode = decode;
if (!userPatches.empty()) {
std::lock_guard cacheLock(drawCacheMutex);
if (drawHit || verifyHit) {
++drawRelocationCounters.hits;
++drawRelocationTrials[drawIdentity].hits;
}
if (profile && drawRelocationCounters.lookups >= 100000) reportDrawRelocation();
}
if (!userPatches.empty() && !drawHit) {
auto current = std::make_shared<DrawDecode>(*decode);
for (const auto& patch : userPatches) current->programs[patch.program].userData[patch.index] = patch.value;
decode = std::move(current);
}
const bool patchedStages = std::any_of(stageData.begin(), stageData.end(), [](const StageDataHit& hit) { return hit.compiled != nullptr; });
std::uint64_t stageDataVerified = 0;
@@ -383,15 +436,16 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
phaseTiming.Phase(DrawRowVectors);
require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs");
auto rectangle = DrawRectangle(*programs[0].snapshot, programs[1].snapshot, programResults[0]->variantId, programResults[1]->variantId, localDevice->Target());
auto& control = graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? rectangle.lastVertexControl : rectangle.control;
if (rectListBuilt) {
results[rectIndex] = std::move(rectangle.control);
results[rectIndex] = std::move(control);
results[rectIndex + 1] = std::move(rectangle.evaluation);
phaseTiming.Phase(DrawRowRectList);
return;
}
require(stages.size() == 2, "rect-list requires vertex and fragment programs");
rectIndex = results.size();
results.push_back(std::move(rectangle.control));
results.push_back(std::move(control));
results.push_back(std::move(rectangle.evaluation));
stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[rectIndex], 0}, {Stage::TessellationEvaluation, &results[rectIndex + 1], 0}});
rectListBuilt = true;
@@ -400,8 +454,18 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (graphics.rectList) buildRectList();
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it");
results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), localDevice->GeometryLimits()));
stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0});
const auto geometryLimits = localDevice->GeometryLimits();
if (geometryLimits || !localDevice->Target().tessellation) {
results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits));
stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0});
} else {
require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart");
auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents());
const auto first = results.size();
results.push_back(std::move(patches.control));
results.push_back(std::move(patches.evaluation));
stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[first], 0}, {Stage::TessellationEvaluation, &results[first + 1], 0}});
}
}
std::vector<Graphics::GuestMemorySnapshot> snapshots;
const auto snapshot = [&] {
@@ -102,7 +102,22 @@ bool Driver::admitDrawKey(std::uint64_t key, std::uint64_t shape) {
std::lock_guard cacheLock(drawCacheMutex);
if (drawCache.contains(key)) return true;
if (!drawKeySightings) drawKeySightings.emplace(drawCacheEntries() * DrawKeySightings::Ways);
const bool seen = drawKeySightings->SeenBefore(key);
bool seen = drawKeySightings->SeenBefore(key);
const auto& pending = drawRelocationPending;
if (pending.key == key && pending.identity != 0) {
if (pending.alias != 0) {
const auto trial = drawRelocationTrials.find(pending.identity);
if (drawKeySightings->SeenBefore(pending.alias ^ 0x9e3779b97f4a7c15ull) && trial != drawRelocationTrials.end()) {
auto& state = trial->second;
if (state.productive || state.hits != 0 || state.admissions < DrawRelocationTrial::ProbationAdmissions) {
if (!state.productive && state.hits == 0) ++state.admissions;
seen = true;
}
}
} else if (!drawRelocations.contains(pending.identity)) {
if (drawKeySightings->SeenBefore(pending.identity ^ 0xc2b2ae3d27d4eb4full)) seen = true;
}
}
if (seen || shape == 0 || drawShapes.contains(shape)) return seen;
if (!drawShapeSightings) drawShapeSightings.emplace(drawCacheEntries() * DrawKeySightings::Ways);
return drawShapeSightings->SeenBefore(shape);
@@ -125,7 +140,7 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<Disp
for (std::size_t i = 0; i < fresh.size(); ++i) {
if (fresh[i] == nullptr) continue;
auto& variants = replacement->stages[i];
const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr<DispatchVariant>& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; });
const auto present = std::find_if(variants.begin(), variants.end(), [&](const std::shared_ptr<DispatchVariant>& kept) { return kept->pushOffset == fresh[i]->pushOffset && kept->meshIndexDescriptor == fresh[i]->meshIndexDescriptor && kept->runs == fresh[i]->runs && kept->words == fresh[i]->words; });
if (present != variants.end()) {
++counters.present;
fresh[i] = *present;
@@ -5,6 +5,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <sstream>
@@ -55,6 +56,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
}();
stageCapture.regions = shaderMemory.TakeRecentRegions();
stageCapture.userDataUse = capture->userDataUse;
recompiled[i] = true;
memory = shaderMemory.Regions();
@@ -148,6 +150,9 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP
if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue;
auto variant = std::make_shared<DispatchVariant>();
variant->compiled = stageCapture.compiled;
if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh) {
std::copy_n(programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord, variant->meshIndexDescriptor.size(), variant->meshIndexDescriptor.begin());
}
variant->shader = programs[i].snapshot;
variant->forgetSerial = stageCapture.forgetSerial;
variant->pushOffset = stageCapture.pushOffset;
@@ -156,13 +161,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP
std::vector<ShaderRecompiler::MemoryRegion> regions(stageCapture.regions.begin(), stageCapture.regions.end());
for (const auto& read : decodeReads[i]) regions.push_back({read.address, std::as_bytes(std::span(read.bytes))});
std::stable_sort(regions.begin(), regions.end(), [](const ShaderRecompiler::MemoryRegion& a, const ShaderRecompiler::MemoryRegion& b) { return a.guestAddress < b.guestAddress; });
for (const auto& region : regions) {
variant->runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size());
const auto count = region.bytes.size() / sizeof(std::uint32_t);
const auto offset = variant->words.size();
variant->words.resize(offset + count);
std::memcpy(variant->words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t));
}
tagVariantRuns(*variant, regions, stageCapture.userDataUse, decodeReads[i].empty() && (!vertexInfos[i] || vertexInfos[i]->resourcesNum == 0), programs[i].hiddenUserWords);
if (drawStageDataHits() && stageCapture.capture != nullptr && !stageCapture.capture->readTrace.leaves.empty()) recordStageDataPositions(*variant, *stageCapture.capture, decodeReads[i]);
if (traceDrawCache() && entry != nullptr && matched[i] == nullptr && i < entry->stages.size()) {
const auto& variants = entry->stages[i];
@@ -174,7 +173,26 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP
++differingRunsChanged;
}
}
if (verifyHit && matched[i] != nullptr && (matched[i]->runs != variant->runs || matched[i]->words != variant->words)) {
const auto sameContent = [](const DispatchVariant& a, const DispatchVariant& b) {
const auto flatten = [](const DispatchVariant& source) {
std::vector<std::pair<std::uint64_t, std::uint32_t>> flat;
std::size_t word = 0;
for (const auto& [begin, end] : source.runs) {
for (auto address = begin; address < end && word < source.words.size(); address += sizeof(std::uint32_t)) flat.emplace_back(address, source.words[word++]);
}
std::sort(flat.begin(), flat.end());
return flat;
};
return (a.runs == b.runs && a.words == b.words) || flatten(a) == flatten(b);
};
const auto sameForwardedWords = [](const ShaderRecompiler::RecompileResult& a, const ShaderRecompiler::RecompileResult& b, std::uint32_t hidden) {
if (a.pushConstants.size() != b.pushConstants.size() || a.shaderDataUserWords != b.shaderDataUserWords) return false;
for (std::size_t slot = 0; slot < a.shaderDataUserWords.size() && (slot + 1) * sizeof(std::uint32_t) <= a.pushConstants.size(); ++slot) {
if (a.shaderDataUserWords[slot] >= hidden && std::memcmp(a.pushConstants.data() + slot * sizeof(std::uint32_t), b.pushConstants.data() + slot * sizeof(std::uint32_t), sizeof(std::uint32_t)) != 0) return false;
}
return true;
};
if (verifyHit && matched[i] != nullptr && (!sameContent(*matched[i], *variant) || !sameForwardedWords(*matched[i]->compiled, *variant->compiled, programs[i].hiddenUserWords))) {
++mismatches;
static std::atomic<std::uint64_t> reports{0};
if (reports.fetch_add(1) < 20) std::fprintf(stderr, "[draw-cache] verify: stage %zu (program 0x%llx) of a hit captured differently: %zu runs / %zu words matched, %zu / %zu fresh\n", i, static_cast<unsigned long long>(programs[i].binary.codeAddress), matched[i]->runs.size(), matched[i]->words.size(), variant->runs.size(), variant->words.size());
@@ -35,6 +35,7 @@ void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32
program.mergedPointerRequired = pointerRequired;
program.firstUserSgpr = 0;
program.userData.insert(program.userData.begin(), 8, 0);
program.hiddenUserWords = 8;
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase);
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase + 1);
if (staticAbi) return;
@@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include <algorithm>
namespace AgcDriver::DriverDetail {
@@ -21,7 +22,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(graphics.stages.mesh.has_value());
if (graphics.stages.mesh) {
const auto& mesh = *graphics.stages.mesh;
for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize}) mix(value);
for (const auto value : {mesh.inputPrimitive, mesh.primitivesPerGroup, mesh.verticesPerGroup, mesh.maxVertices, mesh.maxPrimitives, mesh.threadsPerGroup, mesh.ldsSizeDwords, mesh.provokingVertex, mesh.esgsItemSize, mesh.outputPrimitiveSize}) mix(value);
}
mix(graphics.stages.tessellation.has_value());
if (graphics.stages.tessellation) {
@@ -107,6 +108,11 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
for (std::size_t rank = 0; rank < variants.size(); ++rank) {
const auto& variant = variants[rank];
if (variant->pushOffset != cursor) continue;
if (programs[i].binary.stage == ShaderRecompiler::ShaderStage::Mesh && !std::equal(variant->meshIndexDescriptor.begin(), variant->meshIndexDescriptor.end(), programs[i].userData.begin() + ShaderRecompiler::MeshIndexBufferUserWord)) {
if (!anyLayout) outcome = EntryOutcome::Differing;
anyLayout = true;
continue;
}
auto& regions = matchedRegions[i];
regions.clear();
appendEntryRegions(*variant, regions);
@@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
@@ -14,6 +15,8 @@ namespace {
constexpr std::array<const char*, static_cast<std::size_t>(DrawPipeline::DrainReason::Count)> DrainNames{"packet", "flush", "labels", "capture", "indirect", "submission"};
std::atomic<unsigned> runningCommitters{0};
std::uint64_t elapsedNs(std::chrono::steady_clock::time_point since) {
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - since).count());
}
@@ -36,6 +39,12 @@ std::size_t DrawPipeline::Depth() {
return depth;
}
std::size_t DrawPipeline::QueuedItems() {
auto& pipeline = Queue0();
std::lock_guard lock(pipeline.mutex);
return pipeline.items.size();
}
bool& DrawPipeline::Active() {
static thread_local bool active = false;
return active;
@@ -53,6 +62,10 @@ void DrawPipeline::FollowEpoch(std::uint64_t token) {
GuestMemory::BumpCollectEpoch();
}
unsigned DrawPipeline::Committers() {
return runningCommitters.load(std::memory_order_acquire);
}
void DrawPipeline::rethrowFailure() {
if (failure == nullptr) return;
auto error = std::exchange(failure, nullptr);
@@ -63,6 +76,11 @@ void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
std::unique_lock lock(mutex);
rethrowFailure();
if (closing) {
lock.unlock();
commit();
return;
}
if (items.size() >= Depth()) {
if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline full while the enqueuing thread holds the GPU lock");
const auto start = std::chrono::steady_clock::now();
@@ -76,7 +94,10 @@ void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64
++enqueued;
items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes)});
outstanding.fetch_add(1, std::memory_order_release);
if (!thread.joinable()) thread = std::thread([this] { run(); });
if (!thread.joinable()) {
thread = std::thread([this] { run(); });
runningCommitters.fetch_add(1, std::memory_order_acq_rel);
}
if (committerWaiting) wake.notify_one();
if (!profile) return;
const auto now = std::chrono::steady_clock::now();
@@ -113,11 +134,13 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) {
void DrawPipeline::run() {
for (;;) {
Commit* commit = nullptr;
SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; });
{
std::unique_lock lock(mutex);
committerWaiting = true;
wake.wait(lock, [&] { return !items.empty(); });
wake.wait(lock, [&] { return !items.empty() || closing; });
committerWaiting = false;
if (items.empty()) break;
commit = stopped ? nullptr : &items.front().commit;
}
const auto start = std::chrono::steady_clock::now();
@@ -149,12 +172,28 @@ void DrawPipeline::run() {
outstanding.fetch_sub(1, std::memory_order_release);
if (idleWaiters != 0) idle.notify_all();
}
runningCommitters.fetch_sub(1, std::memory_order_acq_rel);
}
void DrawPipeline::Stop() {
std::thread committer;
{
std::lock_guard lock(mutex);
closing = true;
committer = std::move(thread);
}
wake.notify_all();
if (!committer.joinable()) return;
if (committer.get_id() == std::this_thread::get_id()) committer.detach();
else committer.join();
}
void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) {
drainRequests[std::min<std::uint32_t>(opcode, 0x100)].fetch_add(1, std::memory_order_relaxed);
if (!Busy()) return;
if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline drain under the GPU lock (reason " + std::to_string(static_cast<unsigned>(reason)) + ", opcode " + std::to_string(opcode) + ")");
const auto start = std::chrono::steady_clock::now();
SpinUntil(HandoffSpinWindow() * 2, [&] { return !Busy(); });
std::unique_lock lock(mutex);
++idleWaiters;
idle.wait(lock, [&] { return items.empty(); });
@@ -166,6 +205,10 @@ void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) {
rethrowFailure();
}
std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) {
return Queue0().drainRequests[std::min<std::uint32_t>(opcode, 0x100)].load(std::memory_order_relaxed);
}
std::optional<std::uint64_t> DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) {
if (bytes == 0 || bytes > 8) return std::nullopt;
const auto end = address + bytes;
@@ -33,6 +33,20 @@ const auto& registerMasks() {
return masks;
}
template <typename TVisit>
void forEachKeyedUserWord(const QueueState& queue, bool allUserWords, TVisit&& visit) {
const auto words = [&](std::uint32_t base, std::uint32_t count) {
for (auto it = queue.shader.lower_bound(base); it != queue.shader.end() && it->first < base + count; ++it) visit(it->first, it->second);
};
for (const auto base : {0x00cu, 0x08cu, 0x10cu}) {
const auto resources = queue.shader.find(base - 1);
const auto count = resources == queue.shader.end() ? 0u : ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
words(base, allUserWords ? 32u : std::min(count, 32u));
}
words(0x082u, 2);
words(0x102u, 2);
}
}
std::uint64_t RegisterStateKey(const QueueState& queue, bool allUserWords, bool incremental, std::uint64_t* shapeKey, RegisterStateKeyParts* parts) {
@@ -86,13 +100,59 @@ std::uint64_t DrawShaderSetKey(const QueueState& queue, const ShaderRegistry& re
return key;
}
std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape, std::array<std::uint64_t, 5>* components) {
std::uint64_t Driver::drawIdentityKey(const QueueState& queue, const ShaderRegistry& registry) {
std::uint64_t key = 0x84222325cbf29ce4ull;
const auto mix = [&](std::uint64_t value) {
key ^= value;
key *= 0x100000001b3ull;
};
for (const auto base : {0x008u, 0x088u, 0x0c8u, 0x108u, 0x148u}) {
const auto low = queue.shader.find(base);
const auto high = queue.shader.find(base + 1);
if (low == queue.shader.end() || high == queue.shader.end()) {
mix(0);
continue;
}
mix(low->second);
mix(high->second);
const auto resources = queue.shader.find(base + 3);
mix(resources != queue.shader.end() ? resources->second : 0);
const auto address = (static_cast<std::uint64_t>(low->second) << 8u) | (static_cast<std::uint64_t>(high->second & 0xffu) << 40u);
auto it = registry.upper_bound(address);
if (it == registry.begin()) {
mix(1);
continue;
}
--it;
for (const auto& snapshot : it->second) mix(reinterpret_cast<std::uintptr_t>(snapshot.get()));
mix(address - it->first);
}
return key == 0 ? 1 : key;
}
std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial, std::uint64_t* shape, std::array<std::uint64_t, 5>* components, const DrawRelocation* relocation, std::uint64_t* alias) {
const bool aliased = alias != nullptr && relocation != nullptr && !relocation->words.empty();
bool readable = true;
std::uint64_t other = 0x6c62272e07bb0142ull;
const auto mixOther = [&](std::uint64_t value) {
other ^= value;
other *= 0x100000001b3ull;
};
static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr;
static const bool verify = std::getenv("APS5_VERIFY_REGISTER_KEYS") != nullptr;
std::uint64_t shapeKey = 0;
RegisterStateKeyParts parts;
auto key = RegisterStateKey(queue, allUserWords, true, &shapeKey, components != nullptr ? &parts : nullptr);
if (verify && key != RegisterStateKey(queue, allUserWords, false)) throw std::runtime_error("incremental register fingerprint disagrees with full state");
if (aliased) {
mixOther(shapeKey);
forEachKeyedUserWord(queue, allUserWords, [&](std::uint32_t offset, std::uint32_t value) {
mixOther(offset);
const auto word = std::find_if(relocation->words.begin(), relocation->words.end(), [&](const RelocatableUserWord& candidate) { return candidate.reg == offset; });
mixOther(word != relocation->words.end() ? relocatedContentKey(*word, value, readable) : value);
});
mixOther(deviceSerial);
}
const auto mix = [&](std::uint64_t value) {
key ^= value;
key *= 0x100000001b3ull;
@@ -123,8 +183,13 @@ std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegis
--it;
for (const auto& snapshot : it->second) mix(reinterpret_cast<std::uintptr_t>(snapshot.get()));
mix(address - it->first);
if (aliased) {
for (const auto& snapshot : it->second) mixOther(reinterpret_cast<std::uintptr_t>(snapshot.get()));
mixOther(address - it->first);
}
}
if (shape != nullptr) *shape = shapeKey;
if (aliased && readable) *alias = other == 0 ? 1 : other;
return key;
}
@@ -170,7 +235,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
const auto sameMesh = [](const std::optional<ShaderRecompiler::MeshConfiguration>& x, const std::optional<ShaderRecompiler::MeshConfiguration>& y) {
if (x.has_value() != y.has_value()) return false;
if (!x) return true;
return x->inputPrimitive == y->inputPrimitive && x->primitivesPerGroup == y->primitivesPerGroup && x->verticesPerGroup == y->verticesPerGroup && x->maxVertices == y->maxVertices && x->maxPrimitives == y->maxPrimitives && x->threadsPerGroup == y->threadsPerGroup && x->ldsSizeDwords == y->ldsSizeDwords && x->provokingVertex == y->provokingVertex && x->esgsItemSize == y->esgsItemSize;
return x->inputPrimitive == y->inputPrimitive && x->primitivesPerGroup == y->primitivesPerGroup && x->verticesPerGroup == y->verticesPerGroup && x->maxVertices == y->maxVertices && x->maxPrimitives == y->maxPrimitives && x->threadsPerGroup == y->threadsPerGroup && x->ldsSizeDwords == y->ldsSizeDwords && x->provokingVertex == y->provokingVertex && x->esgsItemSize == y->esgsItemSize && x->outputPrimitiveSize == y->outputPrimitiveSize;
};
const auto sameTess = [](const std::optional<ShaderRecompiler::TessellationConfiguration>& x, const std::optional<ShaderRecompiler::TessellationConfiguration>& y) {
if (x.has_value() != y.has_value()) return false;
@@ -0,0 +1,269 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include <algorithm>
#include <array>
#include <bit>
#include <cstdlib>
#include <cstring>
#include <limits>
namespace AgcDriver::DriverDetail {
namespace {
constexpr std::size_t MaxAliases = 16384;
constexpr std::size_t MaxContentWords = 64;
std::uint64_t AlignedWord(std::uint32_t word) {
return word & ~std::uint32_t {3};
}
}
bool Driver::drawRelocationEnabled() {
static const bool enabled = std::getenv("APS5_NO_DRAW_RELOCATION") == nullptr;
return enabled;
}
std::uint64_t Driver::relocatedContentKey(const RelocatableUserWord& word, std::uint32_t value, bool& readable) {
std::uint64_t hash = 0x9e3779b97f4a7c15ull;
const auto mix = [&](std::uint64_t mixed) {
hash ^= mixed;
hash *= 0x100000001b3ull;
};
mix(value & 3u);
if (value == 0) {
readable = false;
return hash;
}
std::array<std::uint32_t, MaxContentWords> content;
for (const auto& [offset, words] : word.reads) {
mix(static_cast<std::uint64_t>(offset));
mix(words);
for (std::size_t done = 0; done < words; done += MaxContentWords) {
const auto count = std::min<std::size_t>(words - done, MaxContentWords);
const auto address = AlignedWord(value) + static_cast<std::uint64_t>(offset) + done * sizeof(std::uint32_t);
if (GuestMemory::CopyMapped(address, std::as_writable_bytes(std::span(content).first(count))) != GuestMemory::Compare::Equal) {
readable = false;
return hash;
}
for (std::size_t i = 0; i < count; ++i) mix(content[i]);
}
}
return hash;
}
void Driver::tagVariantRuns(DispatchVariant& variant, std::span<const ShaderRecompiler::MemoryRegion> regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords) {
std::uint64_t relocatable = eligible && drawRelocationEnabled() ? use.asBase & ~use.asValue : 0;
if (hiddenWords != 0) relocatable &= hiddenWords >= 64 ? 0 : ~((std::uint64_t {1} << hiddenWords) - 1);
std::vector<std::pair<std::uint64_t, std::int32_t>> reads;
const auto covered = [&](std::uint64_t address) {
return std::any_of(regions.begin(), regions.end(), [&](const ShaderRecompiler::MemoryRegion& region) { return address >= region.guestAddress && address - region.guestAddress + sizeof(std::uint32_t) <= region.bytes.size(); });
};
if (relocatable != 0) {
reads = use.reads;
std::sort(reads.begin(), reads.end());
reads.erase(std::unique(reads.begin(), reads.end()), reads.end());
const auto drop = [&](std::int32_t origin) {
if (origin >= 0) relocatable &= ~(std::uint64_t {1} << origin);
};
for (std::size_t i = 0; i < reads.size(); ++i) {
if (i + 1 < reads.size() && reads[i].first == reads[i + 1].first) {
drop(reads[i].second);
drop(reads[i + 1].second);
}
if (reads[i].second >= 0 && ((reads[i].first & 3u) != 0 || !covered(reads[i].first))) drop(reads[i].second);
}
}
const auto origin = [&](std::uint64_t address) -> std::int8_t {
const auto found = std::lower_bound(reads.begin(), reads.end(), std::make_pair(address, std::numeric_limits<std::int32_t>::min()));
if (found == reads.end() || found->first != address || found->second < 0) return -1;
return (relocatable >> found->second & 1u) != 0 ? static_cast<std::int8_t>(found->second) : std::int8_t {-1};
};
variant.relocatableWords = relocatable;
for (const auto& region : regions) {
const auto count = region.bytes.size() / sizeof(std::uint32_t);
const auto offset = variant.words.size();
variant.words.resize(offset + count);
std::memcpy(variant.words.data() + offset, region.bytes.data(), count * sizeof(std::uint32_t));
if (relocatable == 0) {
variant.runs.emplace_back(region.guestAddress, region.guestAddress + region.bytes.size());
continue;
}
std::size_t start = 0;
auto current = count != 0 ? origin(region.guestAddress) : std::int8_t {-1};
for (std::size_t word = 1; word <= count; ++word) {
const auto next = word < count ? origin(region.guestAddress + word * sizeof(std::uint32_t)) : std::int8_t {-2};
if (next == current) continue;
variant.runs.emplace_back(region.guestAddress + start * sizeof(std::uint32_t), region.guestAddress + word * sizeof(std::uint32_t));
variant.runBases.push_back(current);
start = word;
current = next;
}
}
}
namespace {
template <typename TMap>
std::size_t dropEveryOther(TMap& map) {
std::size_t dropped = 0;
bool drop = false;
for (auto it = map.begin(); it != map.end();) {
drop = !drop;
if (!drop) {
++it;
continue;
}
it = map.erase(it);
++dropped;
}
return dropped;
}
}
void Driver::learnPendingRelocation() {
const auto pending = drawRelocationPending;
drawRelocationPending = {};
if (pending.key == 0) return;
const auto inserted = drawCache.find(pending.key);
if (inserted == drawCache.end() || inserted->second->decode == nullptr) return;
const auto& entry = *inserted->second;
const auto& programs = entry.decode->programs;
if (entry.stages.size() != programs.size()) return;
const auto known = drawRelocations.find(pending.identity);
auto shape = std::make_shared<DrawRelocation>();
if (known != drawRelocations.end()) *shape = *known->second;
bool changed = known == drawRelocations.end();
for (std::size_t i = 0; i < programs.size(); ++i) {
if (entry.stages[i].empty()) continue;
const auto& variant = *entry.stages[i].front();
const auto& program = programs[i];
for (std::uint32_t index = program.hiddenUserWords; index < program.userData.size() && index < 64; ++index) {
const auto reg = program.userDataBase + index - program.hiddenUserWords;
const auto held = std::find_if(shape->words.begin(), shape->words.end(), [&](const RelocatableUserWord& word) { return word.reg == reg; });
if ((variant.relocatableWords >> index & 1u) == 0) {
if (held == shape->words.end()) continue;
shape->words.erase(held);
shape->banned.push_back(reg);
changed = true;
continue;
}
if (held != shape->words.end() || std::find(shape->banned.begin(), shape->banned.end(), reg) != shape->banned.end()) continue;
RelocatableUserWord word{reg, {}};
const auto base = AlignedWord(program.userData[index]);
for (std::size_t run = 0; run < variant.runs.size() && run < variant.runBases.size(); ++run) {
if (variant.runBases[run] != static_cast<std::int8_t>(index)) continue;
word.reads.emplace_back(static_cast<std::int64_t>(variant.runs[run].first - base), static_cast<std::uint32_t>((variant.runs[run].second - variant.runs[run].first) / sizeof(std::uint32_t)));
}
shape->words.push_back(std::move(word));
changed = true;
}
}
if (changed) {
std::sort(shape->words.begin(), shape->words.end(), [](const RelocatableUserWord& a, const RelocatableUserWord& b) { return a.reg < b.reg; });
drawRelocations[pending.identity] = std::move(shape);
}
if (pending.alias == 0) return;
if (drawAliases.size() >= MaxAliases) drawRelocationCounters.evicted += dropEveryOther(drawAliases);
drawAliases[pending.alias] = inserted->second;
}
void Driver::relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std::shared_ptr<DrawEntry>& entry, std::shared_ptr<const DrawDecode>& decode, std::vector<DrawUserPatch>& patches) {
auto& counters = drawRelocationCounters;
++counters.lookups;
const auto found = drawAliases.find(alias);
if (found == drawAliases.end() || found->second->decode == nullptr) return;
const auto donor = found->second;
const auto& programs = donor->decode->programs;
if (donor->stages.size() != programs.size()) return;
struct Move {
std::size_t program;
std::uint32_t index, from, to;
};
std::vector<Move> moves;
for (std::size_t i = 0; i < programs.size(); ++i) {
const auto& program = programs[i];
for (std::uint32_t index = program.hiddenUserWords; index < program.userData.size(); ++index) {
const auto current = readUserData(queue.shader, program.userDataBase + index - program.hiddenUserWords);
if (current != program.userData[index]) moves.push_back({i, index, program.userData[index], current});
}
}
if (moves.empty()) return;
++counters.donors;
auto relocated = std::make_shared<DrawEntry>();
relocated->decode = donor->decode;
relocated->stages.resize(donor->stages.size());
for (std::size_t i = 0; i < donor->stages.size(); ++i) {
const bool moved = std::any_of(moves.begin(), moves.end(), [&](const Move& move) { return move.program == i; });
if (!moved) {
relocated->stages[i] = donor->stages[i];
continue;
}
for (const auto& variant : donor->stages[i]) {
if (variant == nullptr || variant->compiled == nullptr) continue;
const bool allowed = std::all_of(moves.begin(), moves.end(), [&](const Move& move) { return move.program != i || (move.index < 64 && (variant->relocatableWords >> move.index & 1u) != 0 && move.from != 0 && move.to != 0 && ((move.from ^ move.to) & 3u) == 0); });
if (!allowed) {
++counters.refused;
continue;
}
auto copy = std::make_shared<DispatchVariant>();
copy->runs = variant->runs;
copy->runBases = variant->runBases;
copy->relocatableWords = variant->relocatableWords;
copy->words = variant->words;
copy->forgetSerial = variant->forgetSerial;
copy->compiled = variant->compiled;
copy->meshIndexDescriptor = variant->meshIndexDescriptor;
copy->shader = variant->shader;
copy->dataPositions = variant->dataPositions;
copy->dataSlots = variant->dataSlots;
copy->flatBinding = variant->flatBinding;
copy->pushOffset = variant->pushOffset;
copy->vertexInfo = variant->vertexInfo;
for (const auto& move : moves) {
if (move.program != i) continue;
const auto delta = AlignedWord(move.to) - AlignedWord(move.from);
for (std::size_t run = 0; run < copy->runs.size() && run < copy->runBases.size(); ++run) {
if (copy->runBases[run] != static_cast<std::int8_t>(move.index)) continue;
copy->runs[run].first += delta;
copy->runs[run].second += delta;
}
const auto& forwarded = variant->compiled->shaderDataUserWords;
for (std::uint32_t slot = 0; slot < forwarded.size(); ++slot) {
if (forwarded[slot] == move.index) copy->shaderDataPatches.emplace_back(slot, move.to);
}
}
relocated->stages[i].push_back(std::move(copy));
}
}
patches.clear();
for (const auto& move : moves) patches.push_back({move.program, move.index, move.to});
entry = std::move(relocated);
decode = donor->decode;
}
void Driver::applyShaderDataPatches(DispatchVariant& variant) {
if (variant.shaderDataPatches.empty()) return;
auto rebased = std::make_shared<ShaderRecompiler::RecompileResult>(*variant.compiled);
for (const auto& [slot, value] : variant.shaderDataPatches) {
const auto at = static_cast<std::size_t>(slot) * sizeof(std::uint32_t);
if (at + sizeof(std::uint32_t) <= rebased->pushConstants.size()) std::memcpy(rebased->pushConstants.data() + at, &value, sizeof(value));
for (auto& binding : rebased->bindings) {
if (binding.role == ShaderRecompiler::DescriptorRole::ShaderData && slot < binding.guestDescriptor.size()) binding.guestDescriptor[slot] = value;
}
}
variant.compiled = std::move(rebased);
variant.shaderDataPatches.clear();
}
void Driver::reportDrawRelocation() {
auto& counters = drawRelocationCounters;
AgcDriver::ProfilePrint_nid_no_patch("[draw-relocate] %llu alias lookups: %llu donors with moved user words, %llu refusals, %llu hits through a relocated entry; %zu shader sets learned, %zu aliases held (%llu dropped at the limit)\n", static_cast<unsigned long long>(counters.lookups), static_cast<unsigned long long>(counters.donors), static_cast<unsigned long long>(counters.refused), static_cast<unsigned long long>(counters.hits), drawRelocations.size(), drawAliases.size(), static_cast<unsigned long long>(counters.evicted));
counters = {};
}
}
@@ -31,7 +31,9 @@ std::optional<Graphics::IndirectDrawPath> Driver::ClassifyIndirectDraw(const Sha
constexpr std::uint32_t NoLocation = 0x280u;
const bool meshPath = graphics.stages.mesh.has_value() && !graphics.stages.tessellation && !graphics.rectList;
const bool meshGpuSide = meshPath && drawParameters.indexed && indirect.count == 1 && !indirect.countIndirect && indirect.baseVertexLocation == NoLocation && indirect.startInstanceLocation == NoLocation && (!indirect.drawIndexEnabled || indirect.drawIndexLocation == NoLocation);
if ((graphics.stages.path != Graphics::ShaderPath::Vertex || graphics.rectList) && !meshGpuSide) fail(Path::NonVertexPath);
static const bool rectListOnGpu = std::getenv("APS5_NO_RECT_INDIRECT") == nullptr;
const bool rectListGpuSide = rectListOnGpu && graphics.rectList && !graphics.stages.mesh && !graphics.stages.tessellation;
if ((graphics.stages.path != Graphics::ShaderPath::Vertex || (graphics.rectList && !rectListGpuSide)) && !meshGpuSide) fail(Path::NonVertexPath);
if (indirect.drawIndexEnabled && indirect.drawIndexSgpr >= 0) fail(Path::DrawIndex);
if (indirect.countIndirect && !support.count) fail(Path::FeatureGap);
if (indirect.countIndirect && indirect.count > 1 && !support.multi) fail(Path::FeatureGap);
@@ -1,12 +1,18 @@
#include "prx/libSceAgcDriver/Execution/include/AsyncLog.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "ShaderDiskCache.hpp"
#include <atomic>
#include <cstdint>
#include <cstdio>
@@ -38,6 +44,7 @@ void PrintFailure(std::exception_ptr error) {
} catch (...) {
std::fprintf(stderr, "[gpu] driver failure with a non-standard exception\n");
}
FlushAsyncLog_nid_no_patch();
std::fflush(stderr);
}
@@ -66,8 +73,7 @@ bool StackHoldsImport() {
const auto low = bounds.low;
const auto high = bounds.high;
#endif
const volatile unsigned char marker = 0;
const auto frame = reinterpret_cast<std::uintptr_t>(&marker);
const auto frame = reinterpret_cast<std::uintptr_t>(__builtin_frame_address(0));
return frame >= low && frame < high && frame - low >= InlineImportStackBytes;
}
@@ -112,18 +118,25 @@ void Driver::stop() {
for (auto& [queue, worker] : workers) {
if (worker.thread.joinable()) worker.thread.join();
}
DrawPipeline::Queue0().Stop();
StopWorkerSampler();
CloseRegistrationPreparation();
Graphics::StopPipelineLibraryWorker();
std::lock_guard gpuLock(GuestMemory::GpuMutex());
device.Reset();
replacedDevices.clear();
Graphics::FlushPipelineJournal();
Graphics::ShutdownGuestBufferWorkers();
Graphics::StopCompareHelpers();
ShaderRecompiler::ShaderDiskCache::Shutdown();
PerformanceTrace::StopOutput();
FlushAsyncLogAtEnd_nid_no_patch();
StopProfileOutput_nid_no_patch();
stopped = true;
}
Driver::Driver() {
InstallAsyncLog_nid_no_patch();
static_cast<void>(GuestMemory::TrackerGeneration());
ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs);
try {
@@ -216,3 +229,11 @@ bool& Driver::onWorkerThread() {
}
}
namespace AgcDriver {
std::unique_lock<std::mutex> HoldAsyncLog() {
return AsyncLog::Get().Hold();
}
}
@@ -50,6 +50,31 @@ void Driver::noteWrittenBuffers(std::uint64_t program, std::uint32_t queue, cons
while (writtenBuffers.size() > WrittenBufferRing) writtenBuffers.pop_front();
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded) {
bounded = compiled.bdaAbiVersion == 0;
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
constexpr std::uint64_t UnknownImageBytes = 64ull * 1024 * 1024;
for (const auto& binding : compiled.bindings) {
if (binding.role == ShaderRecompiler::DescriptorRole::Gds || binding.role == ShaderRecompiler::DescriptorRole::BdaPagetable || binding.role == ShaderRecompiler::DescriptorRole::FaultBuffer) bounded = false;
if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages || binding.kind != ShaderRecompiler::DescriptorKind::StorageImage) continue;
for (std::uint32_t element = 0; element < binding.count; ++element) {
const bool written = element >= binding.imageWritten.size() || binding.imageWritten[element];
if (!written) continue;
if (binding.guestDescriptor.size() < (static_cast<std::size_t>(element) + 1) * 8) {
bounded = false;
continue;
}
const auto resource = Graphics::DecodeTextureResource(std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8, 8));
if (resource.baseAddress == 0) continue;
const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes;
ranges.emplace_back(resource.baseAddress, resource.baseAddress + (surfaceBytes != 0 ? surfaceBytes : UnknownImageBytes));
if (resource.dccAddress != 0) ranges.emplace_back(resource.dccAddress, resource.dccAddress + (surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownImageBytes));
}
}
visitWrittenBuffers(compiled, [&](std::uint32_t, std::uint64_t begin, std::uint64_t end, bool) { ranges.emplace_back(begin, end); });
return ranges;
}
void Driver::noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue) {
if (!foreignWriters() || !(writeEvidenceEnabled() || traceCapSync()) || end <= begin) return;
std::lock_guard lock(writtenBuffersMutex);
@@ -314,8 +314,10 @@ void Driver::execute(const Submission& submission) {
currentPacketOffset() = cursor;
if (gateOpened()) {
gateOpened() = false;
if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode);
timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); });
if (!pipelineDispatch() || submission.queue != 0) {
if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Packet, opcode);
timed(&WorkerProfile::dispatchMs, [&] { resolveGroupAhead(submission, queue, cursor); });
}
}
if (opcode == 0x15) timed(&WorkerProfile::dispatchMs, [&] { dispatch(queue, packet, submission); });
else timed(&WorkerProfile::dispatchMs, [&] { dispatchIndirect(queue, packet, submission); });
@@ -409,7 +411,10 @@ void Driver::execute(const Submission& submission) {
return;
}
if ((std::atomic_ref<std::uint32_t>(*const_cast<std::uint32_t*>(submission.rewindTail - 1)).load(std::memory_order_acquire) & 0x80000000u) != 0) {
DrawPipeline::Queue0().Enqueue([this] { submitOpenWork(); }, {});
DrawPipeline::Queue0().Enqueue([this] {
submitOpenWork();
wakeQueueZeroForCompletions();
}, {});
executeRewindTail(submission);
return;
}
@@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp"
@@ -84,6 +85,12 @@ void Driver::submitOpenWork() {
if (localDevice != nullptr) localDevice->SubmitRecorded(true);
}
void Driver::wakeQueueZeroForCompletions() {
if (!completionsPending()) return;
std::lock_guard lock(mutex);
changed.notify_all();
}
void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites) {
struct Complete {
Driver& driver;
@@ -97,7 +104,7 @@ void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, st
driver.forgetUnfinishedWrites(driver.workers.at(0), labelWrites);
const auto found = std::find(driver.queue0Uncommitted.begin(), driver.queue0Uncommitted.end(), received);
if (found != driver.queue0Uncommitted.end()) driver.queue0Uncommitted.erase(found);
notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0;
notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0 || completionsPending();
}
if (notify) driver.changed.notify_all();
}
@@ -213,9 +220,9 @@ void Driver::run(std::uint32_t id) noexcept {
&& ((!storeIdleResults && Graphics::StorageTexture::AnyPending()) || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue;
if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed);
const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire);
changed.wait(lock, [&] { return ready() || (id == 0 && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait); });
changed.wait(lock, [&] { return ready() || (id == 0 && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())); });
if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed);
if (id == 0 && !ready() && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) continue;
if (id == 0 && !ready() && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())) continue;
break;
}
if (poll(ready)) break;
@@ -233,10 +240,18 @@ void Driver::run(std::uint32_t id) noexcept {
if (APS5_ENABLE_TIMING_LOG) submission.dequeuedAt = std::chrono::steady_clock::now();
if (id == 0) queue0Executing = submission.suspend ? 0 : submission.received;
if (submission.waitFree) {
orderHolders.fetch_add(1, std::memory_order_acq_rel);
const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); };
while (!released()) poll(released);
orderHolders.fetch_sub(1, std::memory_order_acq_rel);
{
struct OrderHold {
std::atomic<std::uint32_t>& holders;
explicit OrderHold(std::atomic<std::uint32_t>& holders) : holders(holders) { holders.fetch_add(1, std::memory_order_acq_rel); }
~OrderHold() { holders.fetch_sub(1, std::memory_order_acq_rel); }
OrderHold(const OrderHold&) = delete;
OrderHold& operator=(const OrderHold&) = delete;
} hold{orderHolders};
const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); };
StallWatch stall(id);
while (!released() && !poll(released)) stall.Check("order behind queue 0 for event", submission.received);
}
rethrowFailure();
}
if (APS5_ENABLE_TIMING_LOG) submission.orderedAt = std::chrono::steady_clock::now();
@@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/Submission.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
@@ -259,6 +260,8 @@ void Driver::reserveOutputs(Submission& submission) {
void Driver::executeRewindTail(const Submission& stalled) {
PerformanceTimer timing("Driver.Rewind");
static const bool ring = ReadSubmissionRingSettings();
if (ring) PrepareSubmissionRing(stalled.queue);
std::atomic_ref<std::uint32_t> control(*const_cast<std::uint32_t*>(stalled.rewindTail - 1));
if ((control.load(std::memory_order_acquire) & Pm4::RewindValid) == 0) {
noteWaitBlocked(stalled.queue, reinterpret_cast<std::uint64_t>(stalled.rewindTail - 1), true);
@@ -267,10 +270,12 @@ void Driver::executeRewindTail(const Submission& stalled) {
std::uint32_t queue;
~BlockedWait() { driver.noteWaitBlocked(queue, 0, false); }
} blocked{*this, stalled.queue};
StallWatch stall(stalled.queue);
while ((control.load(std::memory_order_acquire) & Pm4::RewindValid) == 0) {
CheckFailure();
checkStopping();
noteAwaitingTitle(stalled.queue, reinterpret_cast<std::uint64_t>(stalled.rewindTail - 1));
stall.Check("REWIND at", reinterpret_cast<std::uint64_t>(stalled.rewindTail - 1));
PollSleep();
}
}
@@ -303,6 +308,7 @@ void Driver::executeRewindTail(const Submission& stalled) {
tail.serial = stalled.serial;
tail.received = ++eventSerial;
if (capture != nullptr) capture->Take(captured, tail.serial, captureTime(), tail.heldAtSubmit);
if (ring) RecordSubmission(tail.queue, tail.serial, tail.commands, stalled.rewindTail, true);
}
if (capture != nullptr) writeCaptured(*this, *capture, captured);
if (APS5_ENABLE_TIMING_LOG) tail.enqueuedAt = tail.dequeuedAt = tail.orderedAt = std::chrono::steady_clock::now();
@@ -313,7 +319,9 @@ void Driver::executeRewindTail(const Submission& stalled) {
void Driver::Submit(const Packet* packet, std::uint32_t queue) {
const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
CheckFailure();
require(queue == 0 || (queue >= 0x20 && queue < 0x58), "unsupported compute queue");
require(queue == 0 || (queue >= 0x20 && queue < SubmissionQueueEnd), "unsupported compute queue");
static const bool ring = ReadSubmissionRingSettings();
if (ring) PrepareSubmissionRing(queue);
GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet));
const auto descriptor = *packet;
require(descriptor.flags == 0, "nonzero submission flags are not implemented");
@@ -355,6 +363,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
const auto now = std::chrono::steady_clock::now();
submission.enqueuedAt = now;
if (capture != nullptr) capture->Take(captured, submission.serial, captureTime(), submission.heldAtSubmit);
if (ring) RecordSubmission(queue, submission.serial, submission.commands, descriptor.addr, false);
if (profile) {
++costs.submissions;
costs.validateNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(validated - copied).count());
@@ -0,0 +1,281 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libc/include/FailureReport.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <cerrno>
#include <charconv>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <memory>
#include <mutex>
#include <thread>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <unistd.h>
#endif
namespace AgcDriver::DriverDetail {
namespace {
constexpr std::size_t KeptWords = 512;
constexpr std::size_t MaximumCapacity = 4096;
constexpr std::size_t MaximumStallSeconds = 86400;
constexpr std::size_t PayloadWordsShown = 8;
struct Entry {
std::uint64_t tenthsOfMs = 0;
std::uint64_t serial = 0;
std::uint64_t guest = 0;
std::size_t dwords = 0;
std::size_t kept = 0;
bool rewindTail = false;
std::array<std::uint32_t, KeptWords> words{};
};
struct Ring {
std::mutex mutex;
std::once_flag allocated;
std::atomic<std::uint64_t> recorded{0};
std::unique_ptr<Entry[]> entries;
};
void reportFailure() noexcept;
struct Rings {
std::array<Ring, SubmissionQueueEnd> queues;
};
Rings& rings() {
static auto* value = new Rings;
return *value;
}
struct Registration {
Registration() { LibcRegisterFailureReport_nid_no_patch(reportFailure); }
~Registration() { LibcUnregisterFailureReport_nid_no_patch(reportFailure); }
Registration(const Registration&) = delete;
Registration& operator=(const Registration&) = delete;
};
std::size_t environmentCount(const char* name, std::size_t fallback, std::size_t maximum, const char* reason) {
const char* text = std::getenv(name);
if (text == nullptr) return fallback;
const auto* end = text + std::strlen(text);
std::size_t value = 0;
const auto [last, error] = std::from_chars(text, end, value);
require(error == std::errc{} && last == end && value <= maximum, reason);
return value;
}
bool writeLine(const char* text, std::size_t length) {
#ifdef _WIN32
const HANDLE handle = GetStdHandle(STD_ERROR_HANDLE);
if (handle == nullptr || handle == INVALID_HANDLE_VALUE) return false;
DWORD written = 0;
return WriteFile(handle, text, static_cast<DWORD>(length), &written, nullptr) != 0 && written == length;
#else
while (length != 0) {
const auto written = ::write(2, text, length);
if (written < 0 && errno == EINTR) continue;
if (written <= 0) return false;
text += written;
length -= static_cast<std::size_t>(written);
}
return true;
#endif
}
class Line {
public:
void Add(const char* format, ...) {
if (length >= sizeof(text) - 2) return;
va_list arguments;
va_start(arguments, format);
const int added = std::vsnprintf(text + length, sizeof(text) - 1 - length, format, arguments);
va_end(arguments);
if (added > 0) length = std::min(length + static_cast<std::size_t>(added), sizeof(text) - 2);
}
bool Write() {
text[length] = '\n';
const bool written = writeLine(text, length + 1);
length = 0;
return written;
}
private:
char text[512];
std::size_t length = 0;
};
void addName(Line& line, std::uint32_t header) {
const auto name = Pm4::KnownName(header);
if (name.empty()) line.Add("UNKNOWN_0x%02x", static_cast<unsigned>((header >> 8u) & 0xffu));
else line.Add("%.*s", static_cast<int>(name.size()), name.data());
}
bool writePackets(const Entry& entry, Line& line) {
bool written = true;
for (std::size_t cursor = 0; cursor < entry.kept;) {
const auto header = entry.words[cursor];
line.Add("[pm4 ring] +0x%04zx ", cursor);
if (Pm4::FillerPacket(header)) {
std::size_t run = 1;
while (cursor + run < entry.kept && Pm4::FillerPacket(entry.words[cursor + run])) ++run;
addName(line, header);
line.Add(" x%zu", run);
written = line.Write() && written;
cursor += run;
continue;
}
if ((header >> 30u) != 3u) {
line.Add("not a type-3 header:");
for (std::size_t i = 0; i <= PayloadWordsShown && cursor + i < entry.kept; ++i) line.Add(" %08x", entry.words[cursor + i]);
written = line.Write() && written;
break;
}
const auto words = Pm4::PacketWords(header);
const auto shown = std::min(words, PayloadWordsShown + 1);
addName(line, header);
for (std::size_t i = 1; i < shown && cursor + i < entry.kept; ++i) line.Add(" %08x", entry.words[cursor + i]);
if (cursor + shown > entry.kept) line.Add(" (cut)");
else if (words > shown) line.Add(" (+%zu dwords)", words - shown);
written = line.Write() && written;
cursor += words;
}
return written;
}
bool dump(std::string_view reason, bool failing) {
const auto capacity = SubmissionRingCapacity();
const auto label = static_cast<int>(std::min<std::size_t>(reason.size(), 200));
Line line;
if (capacity == 0) {
line.Add("[pm4 ring] %.*s: disabled (APS5_PM4_RING=0)", label, reason.data());
return line.Write();
}
auto& state = rings();
std::size_t used = 0;
for (const auto& ring : state.queues) used += ring.recorded.load(std::memory_order_acquire) != 0 ? 1 : 0;
if (used == 0) {
line.Add("[pm4 ring] %.*s: no submissions recorded", label, reason.data());
return line.Write();
}
line.Add("[pm4 ring] %.*s: %zu queue(s), up to %zu submissions each, oldest first", label, reason.data(), used, capacity);
bool written = line.Write();
for (std::size_t queue = 0; queue < state.queues.size(); ++queue) {
auto& ring = state.queues[queue];
if (ring.recorded.load(std::memory_order_acquire) == 0) continue;
std::unique_lock lock(ring.mutex, std::defer_lock);
if (failing) {
for (int attempt = 0; attempt < 100 && !lock.try_lock(); ++attempt) std::this_thread::yield();
} else {
lock.lock();
}
const auto recorded = ring.recorded.load(std::memory_order_acquire);
if (!lock.owns_lock()) {
line.Add("[pm4 ring] queue 0x%zx: %llu submissions recorded, ring busy", queue, static_cast<unsigned long long>(recorded));
written = line.Write() && written;
continue;
}
const auto first = recorded > capacity ? recorded - capacity : 0;
line.Add("[pm4 ring] queue 0x%zx: %llu submissions recorded, last %llu kept", queue, static_cast<unsigned long long>(recorded), static_cast<unsigned long long>(recorded - first));
written = line.Write() && written;
for (auto ordinal = first; ordinal < recorded; ++ordinal) {
const auto& entry = ring.entries[ordinal % capacity];
line.Add("[pm4 ring] queue 0x%zx #%llu serial %llu t=%llu.%llu ms: %zu dwords from 0x%llx%s, first %zu kept", queue, static_cast<unsigned long long>(ordinal + 1), static_cast<unsigned long long>(entry.serial),
static_cast<unsigned long long>(entry.tenthsOfMs / 10), static_cast<unsigned long long>(entry.tenthsOfMs % 10),
entry.dwords, static_cast<unsigned long long>(entry.guest), entry.rewindTail ? " (rewind tail)" : "", entry.kept);
written = line.Write() && written;
written = writePackets(entry, line) && written;
}
}
return written;
}
void reportFailure() noexcept {
try {
dump("failure report", true);
} catch (...) {
static constexpr char text[] = "[pm4 ring] failure report: dump failed\n";
writeLine(text, sizeof(text) - 1);
}
}
}
std::size_t SubmissionRingCapacity() {
static const std::size_t value = environmentCount("APS5_PM4_RING", 16, MaximumCapacity, "APS5_PM4_RING must be a number of submissions from 0 to 4096");
return value;
}
std::chrono::seconds StallWatchdogPeriod() {
static const std::chrono::seconds value(environmentCount("APS5_STALL_WATCHDOG_SECS", 0, MaximumStallSeconds, "APS5_STALL_WATCHDOG_SECS must be a number of seconds from 0 to 86400"));
return value;
}
bool ReadSubmissionRingSettings() {
StallWatchdogPeriod();
return SubmissionRingCapacity() != 0;
}
void PrepareSubmissionRing(std::uint32_t queue) {
const auto capacity = SubmissionRingCapacity();
require(capacity != 0, "the PM4 ring is disabled");
require(queue < SubmissionQueueEnd, "PM4 ring queue out of range");
static const Registration registration;
auto& ring = rings().queues[queue];
std::call_once(ring.allocated, [&] { ring.entries = std::make_unique<Entry[]>(capacity); });
}
void RecordSubmission(std::uint32_t queue, std::uint64_t serial, std::span<const std::uint32_t> commands, const void* guest, bool rewindTail) {
const auto capacity = SubmissionRingCapacity();
require(capacity != 0, "the PM4 ring is disabled");
require(queue < SubmissionQueueEnd, "PM4 ring queue out of range");
auto& ring = rings().queues[queue];
require(ring.entries != nullptr, "PM4 ring recorded before it was prepared");
const auto tenthsOfMs = static_cast<std::uint64_t>(std::max(TraceMs(), 0.0) * 10.0 + 0.5);
std::lock_guard lock(ring.mutex);
const auto recorded = ring.recorded.load(std::memory_order_relaxed);
auto& entry = ring.entries[recorded % capacity];
entry.tenthsOfMs = tenthsOfMs;
entry.serial = serial;
entry.guest = reinterpret_cast<std::uintptr_t>(guest);
entry.dwords = commands.size();
entry.kept = std::min(commands.size(), KeptWords);
entry.rewindTail = rewindTail;
std::copy_n(commands.begin(), entry.kept, entry.words.begin());
ring.recorded.store(recorded + 1, std::memory_order_release);
}
void DumpSubmissionRings(std::string_view reason) {
require(dump(reason, false), "cannot write the PM4 ring dump to stderr");
}
void StallWatch::Check(const char* waiter, std::uint64_t value) {
if (dumped) return;
const auto period = StallWatchdogPeriod();
if (period.count() == 0) return;
const auto now = std::chrono::steady_clock::now();
if (since == std::chrono::steady_clock::time_point{}) {
since = now;
return;
}
if (now - since < period) return;
dumped = true;
char reason[192];
std::snprintf(reason, sizeof(reason), "queue 0x%x %s 0x%llx has not progressed for %lld s (APS5_STALL_WATCHDOG_SECS)", queue, waiter, static_cast<unsigned long long>(value), static_cast<long long>(period.count()));
DumpSubmissionRings(reason);
}
}
@@ -11,6 +11,7 @@
#include "CompiledVariant.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <set>
@@ -933,7 +934,8 @@ class RegistrationPreparePool {
public:
static RegistrationPreparePool& Instance() {
static auto* pool = [] {
auto* created = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
std::lock_guard lifecycle(Lifecycle());
auto* created = new RegistrationPreparePool(Closing() ? 0u : std::max(2u, std::thread::hardware_concurrency() / 2u));
PreparationSlot().store(created);
return created;
}();
@@ -947,7 +949,8 @@ public:
static RegistrationPreparePool& Prebuild() {
static auto* pool = [] {
const char* text = std::getenv("APS5_PREBUILD_THREADS");
auto* created = new RegistrationPreparePool(text != nullptr ? std::max(1u, static_cast<unsigned>(std::strtoul(text, nullptr, 10))) : 1u);
std::lock_guard lifecycle(Lifecycle());
auto* created = new RegistrationPreparePool(Closing() ? 0u : text != nullptr ? std::max(1u, static_cast<unsigned>(std::strtoul(text, nullptr, 10))) : 1u);
PrebuildSlot().store(created);
return created;
}();
@@ -958,7 +961,33 @@ public:
return PrebuildSlot().load();
}
static unsigned LiveWorkers() {
return liveWorkers.load(std::memory_order_acquire);
}
static void CloseAll() {
std::array<RegistrationPreparePool*, 2> pools{};
{
std::lock_guard lifecycle(Lifecycle());
Closing() = true;
pools = {Existing(), ExistingPrebuild()};
}
for (auto* pool : pools) {
if (pool != nullptr) pool->Close();
}
}
private:
static std::mutex& Lifecycle() {
static std::mutex lifecycle;
return lifecycle;
}
static bool& Closing() {
static bool closing = false;
return closing;
}
static std::atomic<RegistrationPreparePool*>& PreparationSlot() {
static std::atomic<RegistrationPreparePool*> slot{nullptr};
return slot;
@@ -1002,9 +1031,17 @@ public:
}
void Close() {
std::unique_lock lock(mutex);
closed = true;
idle.wait(lock, [&] { return tasks.empty() && running == 0; });
std::vector<pthread_t> joined;
{
std::unique_lock lock(mutex);
closed = true;
idle.wait(lock, [&] { return tasks.empty() && running == 0; });
joined.swap(workers);
}
ready.notify_all();
for (const auto worker : joined) {
if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr);
}
}
private:
@@ -1015,8 +1052,9 @@ private:
for (unsigned index = 0; index < count; ++index) {
pthread_t thread;
if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) {
pthread_detach(thread);
workers.push_back(thread);
++threads;
liveWorkers.fetch_add(1, std::memory_order_acq_rel);
}
}
pthread_attr_destroy(&attributes);
@@ -1029,7 +1067,11 @@ private:
Task task;
{
std::unique_lock lock(pool.mutex);
pool.ready.wait(lock, [&] { return !pool.tasks.empty(); });
pool.ready.wait(lock, [&] { return !pool.tasks.empty() || pool.closed; });
if (pool.tasks.empty()) {
liveWorkers.fetch_sub(1, std::memory_order_acq_rel);
return nullptr;
}
task = std::move(pool.tasks.front());
pool.tasks.pop_front();
++pool.running;
@@ -1062,7 +1104,9 @@ private:
std::condition_variable ready;
std::condition_variable idle;
std::deque<Task> tasks;
std::vector<pthread_t> workers;
unsigned threads = 0;
inline static std::atomic<unsigned> liveWorkers{0};
unsigned running = 0;
std::uint64_t submitted = 0;
std::set<std::uint64_t> unfinished;
@@ -1119,9 +1163,16 @@ void SubmitLibraryPrebuild(std::function<void()> task) {
RegistrationPreparePool::Prebuild().Submit(std::move(task));
}
void AwaitLibraryPrebuilds() {
if (auto* pool = RegistrationPreparePool::ExistingPrebuild()) pool->AwaitSubmitted();
}
void CloseRegistrationPreparation() {
RegistrationPreparePool::Instance().Close();
if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close();
RegistrationPreparePool::CloseAll();
}
unsigned RegistrationPreparationThreads() {
return RegistrationPreparePool::LiveWorkers();
}
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) {
@@ -1194,7 +1245,16 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
return prepared;
}
namespace {
void BindTitleThread() {
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("title", TraceThread::DefaultCapacity / 8);
}
}
void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
BindTitleThread();
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveGraphicsStagesAbi");
ShaderPreparationTransaction transaction;
@@ -1271,6 +1331,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std
}
void Driver::ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
BindTitleThread();
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveAbi");
ShaderPreparationTransaction transaction;
@@ -1342,6 +1403,7 @@ std::uint64_t NullPixelProgramAddress() {
}
void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
BindTitleThread();
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveGraphicsAbi");
CheckFailure();
@@ -1462,7 +1524,13 @@ ShaderSnapshot PrepareNullPixelProgram(const VulkanDevice& device) {
return null;
}
std::shared_ptr<const ShaderRegistry> Driver::RegisteredShaders() {
std::lock_guard lock(mutex);
return shaders;
}
void Driver::RegisterShader(const Shader* shader) {
BindTitleThread();
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.Register");
ShaderPreparationTransaction transaction;
@@ -1514,7 +1582,11 @@ void Driver::RegisterShader(const Shader* shader) {
registered.context = snapshot.registeredState->context;
registered.userConfig = snapshot.registeredState->userConfig;
std::shared_ptr<RegisteredPreparation> preparation;
if (AsyncRegistrationPrepare()) {
static const bool lazy = std::getenv("APS5_LAZY_SHADER_PREPARE") != nullptr;
if (lazy) {
static_cast<void>(PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred));
snapshot.prepared->deferred = true;
} else if (AsyncRegistrationPrepare()) {
preparation = PlanRegistered(snapshot, *localDevice, registered, true, &snapshot.prepared->deferred);
if (preparation != nullptr) preparation->prebuildDevice = localDevice;
if (preparation != nullptr && (snapshot.type == 0 || snapshot.type == 1)) BuildRegisteredAbiKey(registered, *localDevice, preparation->abiKey);
@@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
@@ -330,6 +331,7 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
const auto spinStart = std::chrono::steady_clock::now();
bool spinning = pauseSpin;
std::uint32_t polls = 0;
StallWatch stall(queue);
while (!Pm4::WaitSatisfiedUnchecked(packet)) {
if (std::uint32_t writer = queue; !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer)) {
if (writer != queue) notePendingWait(packet, queue, received);
@@ -358,7 +360,9 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
if (const auto done = packetsDone.load(); done != lastDone || (!requireMemory && packetsInFlight.load() != 0)) {
lastDone = done;
start = std::chrono::steady_clock::now();
stall.Progressed();
}
stall.Check(((packet[0] >> 8u) & 0xffu) == 0x93u ? "WAIT_REG_MEM_64 at" : "WAIT_REG_MEM at", awaited);
if (!warned && std::chrono::steady_clock::now() - start > std::chrono::milliseconds(waitTimeoutMs())) {
warned = true;
@@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp"
#include "ThreadOwned.hpp"
@@ -657,8 +658,7 @@ bool onOwnLiveStack(std::uintptr_t address, std::size_t bytes) {
}
};
thread_local const Bounds bounds;
const volatile unsigned char marker = 0;
const auto frame = reinterpret_cast<std::uintptr_t>(&marker);
const auto frame = reinterpret_cast<std::uintptr_t>(__builtin_frame_address(0));
return bounds.high != 0 && frame >= bounds.low && frame < bounds.high && address >= frame && address < bounds.high && bytes <= bounds.high - address;
}
@@ -918,6 +918,32 @@ struct WriteTracker {
}
}
bool blockPartWatched(std::uint64_t first, std::uint64_t stop) const {
#ifdef _WIN32
return covers(first, 1) || covers(stop - 1, 1);
#else
return GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(static_cast<std::uintptr_t>(first), static_cast<std::size_t>(stop - first));
#endif
}
void stampWatchedPart(std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) {
#ifdef _WIN32
address = std::max<std::uint64_t>(address, base);
end = std::min<std::uint64_t>(end, base + size);
if (address >= end) return;
#else
end = std::min<std::uint64_t>(end, LeafCount * LeafBlocks * WriteBlockBytes);
if (address >= end || !GuestWriteWatch::GuestWriteWatchOverlaps_nid_postfix(static_cast<std::uintptr_t>(address), static_cast<std::size_t>(end - address))) return;
#endif
for (auto block = blockOf(address); block <= blockOf(end - 1); ++block) {
const auto first = std::max(address, blockBegin(block));
const auto stop = std::min<std::uint64_t>(end, blockBegin(block) + WriteBlockBytes);
if (!blockPartWatched(first, stop)) continue;
stamp(block, stampGeneration, StampKind::Driver);
noteDriverStore(block, address, end, stampGeneration);
}
}
void noteDriverStore(std::uint64_t block, std::uint64_t address, std::uint64_t end, std::uint32_t stampGeneration) {
const auto begin = blockBegin(block);
const auto from = static_cast<std::uint32_t>(std::max(address, begin) - begin);
@@ -1322,6 +1348,31 @@ void Unwatch(std::uint64_t address, std::size_t bytes) {
unwatchLocked(tracker, address, bytes);
}
void Rewatch(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges) {
#ifdef _WIN32
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
tracker.initialize();
if (!tracker.watched) return;
bool restored = false;
for (const auto& [begin, end] : ranges) {
if (end <= begin || begin < tracker.base || end - tracker.base > tracker.size || (begin - tracker.base) % WriteBlockBytes != 0 || (end - tracker.base) % WriteBlockBytes != 0) continue;
const auto bytes = static_cast<std::size_t>(end - begin);
if (tracker.coverage.Covers(begin, bytes) || !walkWrites(tracker, begin, end, StampKind::Cpu) || !tracker.coverage.Restore(begin, bytes)) continue;
++tracker.generation;
tracker.noteCpuStore(begin, end, tracker.generation);
for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) {
tracker.driverPieces.erase(block);
tracker.stamp(block, tracker.generation, StampKind::Cpu);
}
restored = true;
}
if (restored) unwatchSerial.fetch_add(1, std::memory_order_release);
#else
static_cast<void>(ranges);
#endif
}
bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function<bool()>& import, bool staysWatched) {
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return import();
if (!staysWatched) {
@@ -1447,6 +1498,7 @@ std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::func
++tracker.generation;
if (!tracker.watched || !tracker.covers(stored.first, static_cast<std::size_t>(stored.second - stored.first))) {
tracker.noteUnstampedStore(stored.first, stored.second);
if (tracker.watched) tracker.stampWatchedPart(stored.first, stored.second, tracker.generation);
return 0;
}
for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) {
@@ -1492,6 +1544,7 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
++tracker.generation;
if (!tracker.watched || !tracker.covers(address, bytes)) {
tracker.noteUnstampedStore(address, address + bytes);
if (tracker.watched) tracker.stampWatchedPart(address, address + bytes, tracker.generation);
return 0;
}
{
@@ -1931,12 +1984,12 @@ void GpuMutexType::lock() {
if (!GpuLockProfiled()) {
auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr;
if (trace == nullptr) {
mutex.lock();
LockSpinning(mutex, HandoffSpinWindow());
} else {
const auto site = static_cast<std::size_t>(std::exchange(LockStats().nextSite, GpuLockSite::Other));
if (!mutex.try_lock()) {
const auto start = std::chrono::steady_clock::now();
mutex.lock();
LockSpinning(mutex, HandoffSpinWindow());
trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, std::chrono::steady_clock::now());
}
}
@@ -1954,7 +2007,7 @@ void GpuMutexType::lock() {
HolderMatrixValues before{};
if (holders) Holders().Snapshot(before);
const auto start = std::chrono::steady_clock::now();
mutex.lock();
LockSpinning(mutex, HandoffSpinWindow());
acquired();
const auto now = std::chrono::steady_clock::now();
if (auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr) trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, now);
@@ -2100,9 +2153,26 @@ void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t a
if (trace) std::fprintf(stderr, "[unreadable] read from +0x%llx\n", ModuleOffset(__builtin_return_address(0)));
throw;
}
std::memcpy(destination.data(), source, destination.size());
CopyGuestBytes(destination.data(), source, destination.size());
}
#ifdef ANYPS5_ENABLE_ASAN
[[gnu::no_sanitize("address")]] void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) {
auto* to = static_cast<unsigned char*>(destination);
const auto* from = static_cast<const unsigned char*>(source);
std::size_t index = 0;
for (; index + sizeof(std::uint64_t) <= bytes; index += sizeof(std::uint64_t)) {
const std::uint64_t word = *reinterpret_cast<const volatile std::uint64_t*>(from + index);
std::memcpy(to + index, &word, sizeof(word));
}
for (; index < bytes; ++index) to[index] = static_cast<const volatile unsigned char*>(from)[index];
}
#else
void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) {
std::memcpy(destination, source, bytes);
}
#endif
namespace {
void copyCommitted(std::uint64_t address, std::span<std::byte> destination) {
@@ -0,0 +1,16 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
namespace AgcDriver {
std::atomic<bool> PerformanceTrace::recording{false};
thread_local TraceThread* PerformanceTrace::local = nullptr;
}
extern "C" bool AgcDriverTraceRecording_nid_postfix() {
return AgcDriver::PerformanceTrace::recording.load(std::memory_order_relaxed);
}
extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix() {
return AgcDriver::PerformanceTrace::local;
}
@@ -175,7 +175,7 @@ std::vector<std::byte> copyDataSource(std::span<const std::uint32_t> packet, con
}
std::string Name(std::uint32_t header) {
std::string_view KnownName(std::uint32_t header) {
if (FillerPacket(header)) return "PM4_FILLER";
const auto opcode = (header >> 8u) & 0xffu;
if (opcode == 0x10 && (header & 0xfcu) != 0) {
@@ -194,9 +194,14 @@ std::string Name(std::uint32_t header) {
default: return "UNKNOWN_CUSTOM";
}
}
for (const auto& entry : Opcodes) if (entry.value == opcode) return std::string(entry.name);
for (const auto& entry : Opcodes) if (entry.value == opcode) return entry.name;
return {};
}
std::string Name(std::uint32_t header) {
if (const auto known = KnownName(header); !known.empty()) return std::string(known);
char text[24]{};
std::snprintf(text, sizeof(text), "UNKNOWN_0x%02x", opcode);
std::snprintf(text, sizeof(text), "UNKNOWN_0x%02x", (header >> 8u) & 0xffu);
return text;
}
@@ -31,6 +31,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta
const auto* mesh = findFeatures<VkPhysicalDeviceMeshShaderFeaturesEXT>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MESH_SHADER_FEATURES_EXT);
const auto* clock = findFeatures<VkPhysicalDeviceShaderClockFeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR);
const auto* drawParameters = findFeatures<VkPhysicalDeviceShaderDrawParametersFeatures>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES);
const auto* terminate = findFeatures<VkPhysicalDeviceShaderTerminateInvocationFeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_TERMINATE_INVOCATION_FEATURES_KHR);
const auto* maintenance8 = findFeatures<VkPhysicalDeviceMaintenance8FeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_8_FEATURES_KHR);
Require(target.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version, "shader device profile has an incompatible BDA ABI");
Require(bda != nullptr && bda->bufferDeviceAddress == VK_TRUE && core.shaderInt64 == VK_TRUE, "shader runtime requires enabled bufferDeviceAddress and shaderInt64");
@@ -46,6 +47,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta
Require(!hasCapability(capability) || enabled, std::string("shader device profile advertises a disabled feature: ") + name);
};
checkFeature(spv::CapabilityFloat64, core.shaderFloat64 == VK_TRUE, "shaderFloat64");
checkFeature(spv::CapabilityGeometry, core.geometryShader == VK_TRUE, "geometryShader");
checkFeature(spv::CapabilityTessellation, core.tessellationShader == VK_TRUE, "tessellationShader");
checkFeature(spv::CapabilityImageGatherExtended, core.shaderImageGatherExtended == VK_TRUE, "shaderImageGatherExtended");
checkFeature(spv::CapabilityClipDistance, core.shaderClipDistance == VK_TRUE, "shaderClipDistance");
@@ -76,6 +78,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta
extensions.erase(std::unique(extensions.begin(), extensions.end()), extensions.end());
for (const auto& extension : extensions) extensionViews.emplace_back(extension);
Require(std::ranges::find(extensionViews, "SPV_KHR_physical_storage_buffer") != extensionViews.end() && std::ranges::find(extensionViews, "SPV_KHR_8bit_storage") != extensionViews.end(), "shader device profile is missing runtime extensions");
Require(std::ranges::find(extensionViews, "SPV_KHR_terminate_invocation") == extensionViews.end() || (terminate != nullptr && terminate->shaderTerminateInvocation == VK_TRUE), "shader device profile advertises a disabled feature: shaderTerminateInvocation");
this->target.supportedCapabilities = capabilities;
this->target.supportedExtensions = extensionViews;
}
@@ -219,7 +219,7 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
const auto index = static_cast<std::size_t>((address % PageBytes) / sizeof(*value));
if (!page.valid.Test(index) && page.lazy) {
const auto first = index / BlockWords * BlockWords;
std::memcpy(&page.words[first], reinterpret_cast<const void*>((address & ~static_cast<std::uint64_t>(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t));
GuestMemory::CopyGuestBytes(&page.words[first], reinterpret_cast<const void*>((address & ~static_cast<std::uint64_t>(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t));
page.valid.SetBlock(index);
}
if (!page.valid.Test(index)) {
File diff suppressed because it is too large. Load diff
@@ -54,7 +54,8 @@ struct SlabSlot {
class BufferPool {
public:
explicit BufferPool(const Context& context);
static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory);
static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported = nullptr);
static VkDeviceSize ReclaimTarget(VkDeviceSize bytes);
~BufferPool();
BufferPool(const BufferPool&) = delete;
BufferPool& operator=(const BufferPool&) = delete;
@@ -116,6 +117,7 @@ private:
// when shared; the device tier for device-local memory while it has a budget).
Tier& tierFor(std::size_t capacity, VkMemoryPropertyFlags properties);
static bool DeviceTierEnabled();
static VkDeviceSize deviceBudget(const Context& context);
static bool DeviceTiered(VkMemoryPropertyFlags properties);
static std::size_t NextCapacity(std::size_t capacity);
VkDeviceSize destroy(const BufferAllocation& allocation) noexcept;
@@ -127,6 +129,7 @@ private:
// The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once.
static std::size_t MaxSlots();
std::size_t maxSlots;
VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes);
VkDevice device;
PFN_vkUnmapMemory unmap;
PFN_vkDestroyBuffer destroyBuffer;
@@ -171,6 +171,7 @@ struct Context {
std::uint32_t settleFamily = 0;
std::mutex* settleQueueMutex = nullptr;
PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr;
VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes) = nullptr;
RenderCache* renderCache = nullptr;
DrawQueue* drawQueue = nullptr;
GraphicsPipelineCache* graphicsPipelines = nullptr;
@@ -210,8 +211,10 @@ struct Context {
bool variableDescriptorCount = false;
std::uint32_t imageTableCapacity = 0;
std::uint32_t samplerTableCapacity = 0;
bool individuallyFreeDescriptorSets = false;
bool geometryShader = false;
bool sampleRateShading = false;
bool storageImageMultisample = false;
bool shaderClipDistance = false;
bool shaderCullDistance = false;
bool nullDescriptors = false;
@@ -232,6 +235,7 @@ struct Context {
VkSampleCountFlags sampleLocationSampleCounts = 0;
VkSampleCountFlags sampleLocationGridSampleCounts = 0;
PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr;
bool shaderTerminateInvocation = false;
PipelineCacheSynchronization::PipelineUseLock LockPipelineCacheForUse() const {
return pipelineCacheSynchronization != nullptr ? pipelineCacheSynchronization->LockForPipelineUse() : PipelineCacheSynchronization::PipelineUseLock{};
@@ -317,6 +321,7 @@ inline void EndRenderPass(const Context& context, VkCommandBuffer commands) {
}
std::uint32_t ImageTableCapacity(const Context& context);
std::uint32_t StorageTableCapacity(const Context& context);
std::uint32_t SamplerTableCapacity(const Context& context);
}
@@ -99,6 +99,16 @@ DccKeys ProvedCurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceByt
void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span<std::byte> bytes);
void NoteKeysFillOnGpu(std::uint64_t begin, std::size_t count, DccKeys keys);
std::optional<DccKeys> WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes);
bool DccKeyWritersPending(std::uint64_t metaAddress, std::size_t count);
bool DccKeyWriterNeedsSync(std::uint64_t metaAddress, std::size_t count);
struct DccKeyWaitCounts {
std::uint64_t calls;
std::uint64_t noPending;
std::uint64_t known;
std::uint64_t fillsUncompressed;
std::uint64_t synced;
};
DccKeyWaitCounts KeyWaitCounts();
}
@@ -26,6 +26,7 @@ void RequireNoNativeDepthFeedback_nid_no_patch(const Context& context, const Dep
void ClearDepthSurfaces(VkDevice device);
void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes);
bool DepthSurfaceAt(std::uint64_t address);
bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes);
bool DepthSurfaceAt(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource);
std::uint64_t HtileDepthClearAddress(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads);
std::size_t HtileDepthClearBytes_nid_no_patch(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span<const std::uint32_t> packet, std::uint64_t indirectArguments);
@@ -48,6 +49,49 @@ float HostDepthBiasConstant(const Context& context, VkFormat format, float const
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip);
bool DepthSurfaceStoreBack(const Context& context, const StorageTexture& storage);
struct StorageImageClear {
std::array<std::uint32_t, 8> image{};
std::array<std::uint32_t, 4> color{};
std::uint32_t width = 0;
std::uint32_t height = 0;
};
std::optional<StorageImageClear> MatchStorageImageClear(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::array<std::uint32_t, 3>& groups);
std::uint64_t StorageImageClearAddress(const StorageImageClear& clear);
struct DepthPlaneOverwrite {
std::array<std::uint32_t, 8> image{};
GuestTextureResource view{};
std::uint32_t bits = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
std::uint32_t raw = 0;
std::uint64_t colorAddress = 0;
std::uint32_t colorBits = 0;
float depth = 0.0f;
};
class DepthPlaneClear {
public:
explicit DepthPlaneClear(const StorageImageClear& clear);
~DepthPlaneClear();
DepthPlaneClear(const DepthPlaneClear&) = delete;
DepthPlaneClear& operator=(const DepthPlaneClear&) = delete;
bool Active() const { return installed; }
private:
DepthPlaneOverwrite value;
DepthPlaneOverwrite* previous = nullptr;
bool installed = false;
};
struct DepthClearStatistics {
std::uint64_t matched = 0;
std::uint64_t taken = 0;
std::uint64_t copiesSkipped = 0;
std::uint64_t clears = 0;
std::uint64_t bytesSkipped = 0;
std::array<std::uint64_t, 8> declined{};
};
DepthClearStatistics DepthClearCounts();
void SetDepthPlaneClearForTest(bool enabled);
}
#endif
@@ -31,7 +31,7 @@ struct DrawInputCopy {
DrawInputCopy CopyDrawInput(const Context& context, Recorder* recorder, std::uint64_t address, std::size_t bytes, std::size_t alignment, Recorder::SnapshotUse use);
DrawInputCopy CopyZeroPaddedDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t validBytes);
void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived);
const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment);
const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment, bool* behindGpuWrite = nullptr);
struct InPlaceIndices {
std::optional<std::uint32_t> highest;
bool reused = false;
@@ -93,6 +93,16 @@ bool DrawRecipes();
// `rewritten` counts a GPU-side draw whose records were copied and patched with a constant.
enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, Count };
void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten = false);
std::array<std::uint64_t, static_cast<std::size_t>(IndirectDrawPath::Count)> IndirectDrawCounts();
struct DrawPassCounts {
std::uint64_t draws = 0;
std::uint64_t passesBegun = 0;
std::uint64_t passesContinued = 0;
std::uint64_t keyStoreFlushes = 0;
std::uint64_t storeFlushes = 0;
};
std::array<DrawPassCounts, 2> DrawPassTotals();
const char* IndirectDrawPathName(IndirectDrawPath path);
// Draw packets that drew nothing, for the [draws] line: Nothing (an empty count, or no color writes
@@ -40,8 +40,13 @@ struct HostImport {
bool joined = false;
std::shared_ptr<void> chunk;
mutable std::uint64_t lastUse = 0;
std::uint64_t lead = 0;
};
inline VkDeviceSize HostImportOffset(const HostImport& import, std::uint64_t address) {
return address - import.base + import.lead;
}
enum class ImportWatch : std::uint8_t { Watch, Unwatch };
struct ImportProbe {
@@ -63,14 +68,26 @@ ImportWatch PrepareImportWatch(const Context& context);
void SetImportWatch(const Context& context, ImportWatch watch);
// The host import of the registered allocation containing [address, address + bytes), made on demand
// (alignment and budget permitting), or null. Bytes at `address` are at `address - import->base` in
// the import's buffer.
// (alignment and budget permitting), or null. Bytes at `address` are at `HostImportOffset(*import,
// address)` in the import's buffer.
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes);
const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes);
void SetHostImportWindow(std::uint64_t bytes);
// Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one).
void SetHostImportRangeWindow(std::uint64_t bytes);
void SetHostImportBudget(std::uint64_t bytes);
void SetHostImportDeferredSettles(bool enabled);
std::uint64_t HostImportLiveBytes();
struct HostImportSettleCounts {
std::uint64_t submitted = 0;
std::uint64_t waited = 0;
std::uint64_t rewatched = 0;
std::uint64_t parked = 0;
std::uint64_t unmapWaits = 0;
};
HostImportSettleCounts HostImportSettles();
// Tests: how many imports settleImport has used on the settle queue.
std::uint64_t HostImportsSettled();
bool ImportMappedRanges(const Context& context, const GuestAllocations::Mapped& ranges, std::uint64_t generation, bool adoptDevice);
// Whether an existing import covers [address, address + bytes), without reconciling the imports
// with the registry or making one (HostImportFor may take a registry lease): a hint for choices
@@ -374,7 +391,7 @@ private:
void completeSpaceWrites() const;
// The region containing `address`: this build's own regions (sorted after UploadPrepare) or
// the space's base regions, or null.
const Region* owner(std::uint64_t address) const;
const Region* owner(std::uint64_t address, std::uint64_t end) const;
// Builds this build's regions from a lease as the per-build path always did: `base` gets the
// ranges served in place (imports, mirrors), `copied` the ones copied per build.
struct CopiedRange {
@@ -429,6 +446,7 @@ private:
// Whether `regions` is in ascending address order (true right after AcquireRegistered, whose
// regions follow the registry's order), so AddSnapshot can search instead of scanning.
bool regionsSorted = false;
bool regionsOverlap = false;
std::vector<std::pair<std::uint64_t, std::uint64_t>> writes;
std::vector<std::pair<std::uint64_t, std::uint64_t>> readsOverBase;
std::vector<std::pair<std::uint64_t, std::vector<std::byte>>> heapReferences;
@@ -8,6 +8,7 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <span>
#include <string>
namespace AgcDriver::Graphics {
@@ -49,11 +50,15 @@ private:
};
std::uint64_t MultisampledColorBytes(const ColorTarget& target);
std::size_t MultisampledDccBytes(const ColorTarget& target);
VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span<const std::byte> dcc, std::span<const std::byte> cmask);
bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear);
bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear);
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target);
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components);
void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget& target);
VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource);
bool MultisampleTargetStorable(const Context& context, const ColorTarget& target);
void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView);
bool MultisampledColorFillAbsorbs(std::uint64_t address, std::size_t bytes, std::uint32_t pattern);
void NoteColorMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern);
@@ -16,6 +16,8 @@ struct CompareSpan {
void CompareSpans(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal);
void CompareSpansFrom(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal, std::size_t parallelBytes);
unsigned CompareHelpers();
void StopCompareHelpers();
unsigned CompareHelpersRunning();
}
@@ -94,14 +94,29 @@ private:
std::vector<CachedFramebuffer> framebuffers;
};
// The pipeline for this configuration, shared across draws and frames: keyed by the stages' variant
// ids (the rect-list tessellation pair by the vertex and fragment ids that generate it), the vertex
// input layout, the descriptor set layout key, push constant stages and every pipeline state field.
// A configuration whose stage has no variant id gets a private pipeline. Entries are LRU-bounded and
// only evicted once no recorded draw holds them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per
// draw as before.
// The pipeline for this configuration, shared across draws and frames: keyed by the stages'
// identities (the rect-list tessellation pair and the geometry stage by the vertex and fragment
// results that generate them), the vertex input layout, the descriptor set layout key, push constant
// stages and every pipeline state field. A configuration whose stage has no module gets a private
// pipeline. A miss builds the pipeline outside the store's mutex (a concurrent build of the same
// key keeps the first inserted) and records its recipe in the pipeline journal. Entries are
// LRU-bounded (APS5_PIPELINE_STORE_ENTRIES, 1024) and only evicted once no recorded draw holds
// them. Debug aid: APS5_NO_PIPELINE_CACHE=1 builds one per draw as before.
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
// `created` reports whether this call built the pipeline (false: served from the store or private).
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr);
// Cumulative pipeline store outcomes since start-up (the [pipecache] line reports 10 s windows).
struct PipelineStoreCounters {
std::uint64_t hits = 0;
std::uint64_t misses = 0;
std::uint64_t uncached = 0;
std::uint64_t evicted = 0;
std::uint64_t cached = 0;
std::uint64_t bound = 0;
};
PipelineStoreCounters PipelineStoreTotals();
// The pipeline store's key of a configuration (empty when the configuration gets a private pipeline).
void BuildPipelineStoreKey(std::vector<std::byte>& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout);
struct DescribedSetLayouts {
PipelineSetLayouts layouts;
std::vector<std::uint32_t> mainKey;
@@ -119,6 +134,7 @@ bool HasStencil(VkFormat format);
void ValidateViewport(const Context& context, const VkViewport& viewport);
void ValidateDepthBounds(const Context& context, const State& state);
void ValidateProvokingVertex(const Context& context, const State& state, std::span<const CompiledShader> shaders = {});
bool BarycentricPatches(const State& state, std::span<const CompiledShader> shaders);
bool ValidateSampleLocations(const Context& context, const State& state);
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
@@ -126,7 +142,7 @@ bool ShadersWriteMemory(std::span<const CompiledShader> shaders);
bool DeclaresEarlyFragmentTests(const ShaderRecompiler::RecompileResult& program);
void ValidateSampleMask(const Context& context, const State& state, std::span<const CompiledShader> shaders);
// Returns the color attachment locations the pixel shader writes.
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false);
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false, bool geometryShader = false, bool sampleRateShading = false, bool bufferInt64Atomics = false, bool shaderClipDistance = false, bool shaderCullDistance = false, bool viewportIndexLayer = false, bool runtimeDescriptorArray = false, bool demoteToHelperInvocation = false, bool shaderTerminateInvocation = false);
}
@@ -18,13 +18,14 @@ class PipelineCache {
public:
static constexpr int SaveIntervalSeconds = 10;
PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties);
PipelineCache(const Context& context, const VkPhysicalDeviceProperties& properties, bool loadFromFile = true);
~PipelineCache();
PipelineCache(const PipelineCache&) = delete;
PipelineCache& operator=(const PipelineCache&) = delete;
VkPipelineCache Handle() const { return cache; }
PipelineCacheSynchronization& Synchronization() { return synchronization; }
void SaveNow();
private:
struct FileHeader {
@@ -54,6 +55,7 @@ private:
VkPipelineCache cache = VK_NULL_HANDLE;
PipelineCacheSynchronization synchronization;
std::filesystem::path path;
bool holdsFile = false;
std::size_t loadedBytes = 0;
std::size_t settledBytes = 0;
std::optional<FileHeader> settledFile;
@@ -0,0 +1,104 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINEJOURNAL_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_PIPELINEJOURNAL_HPP
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "Recompiler.hpp"
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <functional>
#include <memory>
#include <span>
#include <vector>
namespace AgcDriver::Graphics {
// What a graphics pipeline is built from besides the device: the stage results (named by their
// run-stable identity), the vertex input layout, the descriptor set layout keys and the state
// fields the Pipeline constructor reads, with every other State field cleared
// (CanonicalPipelineState). The pipeline store keys on exactly this, and the journal records it so
// the next run builds the same VkPipeline before the title asks for it. A generated stage (the
// rect-list control and evaluation pair, the barycentric geometry stage) is built from the vertex
// and fragment results and the device's limits; its identity is in the key like any other stage's,
// so a replayed pipeline built from another device's generated module is missed, not used.
struct GraphicsPipelineRecipe {
struct Stage {
ShaderRecompiler::ShaderStage stage;
ShaderRecompiler::PipelineIdentity identity;
std::uint32_t pushConstantOffset;
bool generated;
};
VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
std::vector<Stage> stages;
VertexInputLayout vertexInput;
std::vector<std::uint32_t> mainLayoutKey;
std::vector<std::uint32_t> fragmentLayoutKey;
bool splitSets = false;
State state;
};
[[nodiscard]] State CanonicalPipelineState(const State& state);
[[nodiscard]] bool GeneratedPipelineStage(const State& state, ShaderRecompiler::ShaderStage stage);
[[nodiscard]] GraphicsPipelineRecipe MakeGraphicsPipelineRecipe(const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout);
[[nodiscard]] std::vector<std::byte> EncodeGraphicsPipelineRecipe(const GraphicsPipelineRecipe& recipe);
[[nodiscard]] bool DecodeGraphicsPipelineRecipe(std::span<const std::byte> bytes, GraphicsPipelineRecipe& recipe);
struct PipelineJournalCounters {
std::uint64_t loadedRecipes = 0;
std::uint64_t loadedModules = 0;
std::uint64_t loadedBytes = 0;
std::uint64_t droppedRecords = 0;
double loadMs = 0;
std::uint64_t replayedGraphics = 0;
std::uint64_t replayedCompute = 0;
std::uint64_t replayPresent = 0;
std::uint64_t replaySkipped = 0;
std::uint64_t replayRejected = 0;
std::uint64_t replayFailed = 0;
double replayMs = 0;
std::uint32_t replayThreads = 0;
std::uint64_t recordedGraphics = 0;
std::uint64_t recordedCompute = 0;
std::uint64_t recordedModules = 0;
std::uint64_t writtenBytes = 0;
std::uint64_t writeFailures = 0;
};
// A decimal switch of the pipeline store or journal, or its default when unset; anything else throws.
[[nodiscard]] std::uint64_t PipelineSwitch(const char* name, std::uint64_t fallback);
[[nodiscard]] bool PipelineJournalEnabled();
// True on a replay thread while it builds an entry: what it builds is already in the file.
[[nodiscard]] bool PipelineJournalReplaying();
[[nodiscard]] std::filesystem::path PipelineJournalPath();
[[nodiscard]] PipelineJournalCounters PipelineJournalTotals();
void RecordGraphicsPipeline(const GraphicsPipelineRecipe& recipe, std::span<const CompiledShader> shaders);
void RecordComputePipeline(const ShaderRecompiler::RecompileResult& shader, bool pushConstants);
void FlushPipelineJournal();
void ReportPipelineJournal();
// Cancel stops the threads taking further entries (the pipeline each is building completes);
// Wait joins them. A device cancels its replay before it is replaced or destroyed.
class PipelineJournalReplay {
public:
virtual ~PipelineJournalReplay() = default;
virtual void Cancel() = 0;
virtual void Wait() = 0;
[[nodiscard]] virtual bool Done() const = 0;
};
struct PipelineJournalHooks {
std::function<bool(const ShaderRecompiler::RecompileResult& shader, bool pushConstants)> compute;
};
// The journal belongs to the device that wrote it, named by the same properties as the driver's
// pipeline cache file; a replay for another device starts the file over.
[[nodiscard]] std::uint64_t PipelineJournalDeviceFingerprint(const VkPhysicalDeviceProperties& properties);
[[nodiscard]] std::shared_ptr<PipelineJournalReplay> StartPipelineJournalReplay(const Context& context, std::uint64_t deviceFingerprint, PipelineJournalHooks hooks);
}
#endif
@@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <array>
#include <atomic>
#include <functional>
#include <memory>
#include <cstddef>
#include <cstdint>
@@ -32,7 +33,13 @@ struct OptimizedPipeline {
PFN_vkDestroyPipeline destroy = nullptr;
std::atomic<VkPipeline> handle{VK_NULL_HANDLE};
std::atomic<bool> released{false};
// The relink of a pipeline the journal replay built waits for the first draw that binds it
// (Schedule), so the relink thread serves the title's own pipelines first and a replayed
// pipeline the title never draws is never relinked.
std::atomic<bool> deferred{false};
std::function<void()> relink;
void Release() noexcept;
void Schedule();
};
VkPipeline LinkPipelineFromLibraries(const Context& context, const VkGraphicsPipelineCreateInfo& info, const VkPipelineRenderingCreateInfoKHR& rendering, const VkPipelineLayoutCreateInfo& layout, const PipelineLibraryKeys& keys, std::shared_ptr<OptimizedPipeline>* optimized = nullptr, bool partsOnly = false, std::span<VkShaderModule> modules = {});
@@ -41,6 +48,9 @@ void WaitForOptimizedPipelines(VkDevice device);
void ClearPipelineLibraries(VkDevice device);
void StopPipelineLibraryWorker();
unsigned PipelineLibraryWorkers();
struct PipelineLibraryCounters {
std::array<std::uint64_t, 4> built{};
std::uint64_t linked = 0;
@@ -752,6 +752,8 @@ private:
bool syncThroughUnlocked(std::uint64_t address, std::uint64_t end, int source, const void* site);
// `source` is the CountSync source the wait is attributed to.
void finish(std::unique_ptr<Batch> batch, bool wait, int source, bool releaseNow = false);
void discard(std::unique_ptr<Batch> batch) noexcept;
void removeLabels(Batch& batch);
void release(Batch& batch) noexcept;
static bool overlaps(const Batch& batch, std::uint64_t address, std::uint64_t end);
static std::optional<ReadKind> readOverlap(const Batch& batch, std::uint64_t address, std::uint64_t end);
@@ -64,6 +64,7 @@ private:
VkBufferUsageFlags usage;
std::shared_ptr<BufferPool> cache;
bool hostPlaced = false;
bool ready = false;
};
// Records a whole-range buffer copy.
@@ -18,6 +18,7 @@
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace AgcDriver { class VulkanDevice; }
@@ -28,10 +29,12 @@ class Recorder;
void FlushCachedTextures(VkDevice device);
void ClearCachedTextures(VkDevice device);
std::uint64_t ReleaseTextureCaches(std::uint64_t bytes);
std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory);
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported);
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes);
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes);
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport);
std::uint64_t StorageTextureCacheBudget(const Context& context);
struct TextureCacheUse {
std::size_t sampledEntries = 0;
std::uint64_t sampledBytes = 0;
@@ -42,6 +45,7 @@ std::uint64_t SampledTextureCacheBudget(const Context& context);
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words);
bool SampledTexturesShareEntry(std::span<const std::uint32_t> first, std::span<const std::uint32_t> second);
bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes);
bool SpirvStoresThroughAddresses(std::span<const std::uint32_t> words);
// The cached storage image of a surface (render targets use it as their resident image); brought up
// to date with guest memory before it is returned.
@@ -98,6 +102,7 @@ private:
PFN_vkDestroyDescriptorSetLayout destroyLayout;
PFN_vkDestroyDescriptorPool destroyPool;
PFN_vkResetDescriptorPool resetPool;
PFN_vkFreeDescriptorSets freeDescriptorSets = nullptr;
mutable std::mutex mutex;
std::map<std::vector<std::uint32_t>, VkDescriptorSetLayout> layouts;
struct Pool {
@@ -122,6 +127,12 @@ struct ImageTableReuseCounts {
};
ImageTableReuseCounts ImageTableReuses();
struct PresyncSurface {
std::uint64_t address;
std::uint64_t bytes;
bool dccKeys;
};
class ShaderResources {
public:
ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes);
@@ -168,8 +179,16 @@ public:
std::size_t size;
std::vector<std::uint32_t> words;
};
std::shared_ptr<DrawBindings> PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved = {}) const;
std::shared_ptr<DrawBindings> PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved = {}, bool movedOnly = false) const;
std::optional<std::vector<MovedBuffer>> MovedReadOnlyBuffers(std::span<const CompiledShader> shaders, Recorder& recorder) const;
enum class MoveRefusal : std::size_t { None, Shape, NotGuest, Written, Limit, Inaccessible, Overlap, Bindings, Count };
struct MoveProof {
MoveRefusal refusal = MoveRefusal::None;
std::size_t buffers = 0;
std::size_t bytes = 0;
};
static const char* MoveRefusalName(MoveRefusal refusal);
std::optional<std::vector<MovedBuffer>> MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const;
static std::size_t MaxDrawSnapshotBytes();
void WriteBack();
// Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU
@@ -182,6 +201,7 @@ public:
// Whether a written buffer was copied (its results reach guest memory by the CPU write-back).
bool HasCopiedWrites() const { return guestMemory.HasCopiedWrites(); }
bool HoldsLease() const { return guestMemory.HoldsLease(); }
bool BdaReadOnly() const { return usesBda && bdaReadOnly; }
bool UsesAddressTable() const { return usesBda; }
std::size_t CopiedBytes() const { return guestMemory.CopiedBytes(); }
bool WritesOverlap(std::uint64_t address, std::size_t bytes) const { return guestMemory.WritesOverlap(address, bytes); }
@@ -227,6 +247,7 @@ public:
bool Reusable() const { return reusable; }
const std::vector<std::uint64_t>& MultisampleSources() const { return multisampleSources; }
static bool NeverReusable(std::span<const CompiledShader> shaders);
static std::uint64_t DepthGuardRefusals();
const std::vector<std::shared_ptr<Texture>>& SampledTextures() const { return textures; }
bool ChecksFaults() const { return bda != nullptr; }
void CheckFaults() const;
@@ -281,7 +302,7 @@ public:
// Usable right after the deferred constructor, for address-based builds too (it decodes the
// compiled shader's bindings itself when stage A has not run), and on a completed object from
// the resource cache (its Revalidate repeats the same lookups), without the device lock.
std::vector<std::pair<std::uint64_t, std::uint64_t>> PresyncSurfaces() const;
std::vector<PresyncSurface> PresyncSurfaces() const;
private:
friend class AgcDriver::VulkanDevice;
@@ -339,12 +360,15 @@ private:
std::uint32_t set = 0;
std::size_t firstSampler = 0;
std::size_t samplerCount = 0;
bool sampled = false;
bool storage = false;
std::vector<std::size_t> elementTextures;
std::vector<std::pair<std::uint32_t, std::uint32_t>> samplerRejections;
};
std::vector<TableStage> tableStages;
std::array<std::size_t, 2> tableBindings{std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::size_t>::max()};
std::array<std::uint32_t, 2> tableElements{};
std::array<std::size_t, 2> storageTableBindings{std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::size_t>::max()};
TableStage& tableStage(const CompiledShader& shader);
static const ShaderRecompiler::DescriptorBinding* tableImages(std::span<const CompiledShader> shaders, ShaderRecompiler::ShaderStage stage);
std::uint32_t rejectTableElement(TableStage& table, std::span<const std::uint32_t> words, const std::string& message);
@@ -376,6 +400,7 @@ private:
// same way but is left out of the write set (no write-back, no pending-write note). `atomic`
// is its bufferAtomic (see GuestBufferMemory::AddWritable).
std::size_t addGuestBuffer(std::span<const std::uint32_t> words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read);
std::uint64_t guestBufferBytes(std::span<const std::uint32_t> words) const;
std::size_t addDataBuffer(std::span<const std::uint32_t> words);
// Stage A: the layout entry of an image binding (samplers are taken at once, the sampler cache
// locks itself); stage B looks the sampled textures and storage images up (resolveImageBinding).
@@ -487,6 +512,7 @@ private:
std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes();
std::unique_ptr<BdaResources> bda;
bool usesBda = false;
bool bdaReadOnly = true;
std::vector<std::uint64_t> multisampleSources;
bool usesFaultBuffer = false;
bool faultChecks = false;
@@ -531,6 +557,7 @@ private:
std::vector<bool> textureBaseLevel;
std::vector<bool> textureSrgb;
std::vector<std::shared_ptr<StorageTexture>> storageTextures;
std::vector<VkImageView> multisampleStorageViews;
std::vector<std::uint32_t> storageMips;
std::vector<std::uint64_t> storageKeys;
std::vector<bool> storageFirstLayer;
@@ -629,13 +656,20 @@ public:
static std::uint64_t Touches();
void Clear();
std::size_t Size() const;
std::size_t Capacity() const;
void SetBase(std::size_t entries, std::size_t shrinkWindow);
std::uint64_t TableElements() const;
static std::uint64_t TableElementBudget();
static constexpr std::size_t InitialEntries = 1024;
static constexpr std::size_t MaximumEntries = 16384;
private:
void erase(const Key& key);
void noteMiss(const Key& key);
void dropReleasedTextures(std::vector<std::shared_ptr<ShaderResources>>* evicted);
void noteEvicted(const Key& key);
void noteReturn(const Key& key);
void noteUse(const Key& key);
struct KeyHash {
std::size_t operator()(const Key& key) const noexcept {
std::uint64_t hash = 14695981039346656037ull;
@@ -646,6 +680,20 @@ private:
mutable std::mutex mutex;
std::list<std::pair<Key, std::shared_ptr<ShaderResources>>> entries;
std::unordered_map<Key, decltype(entries)::iterator, KeyHash> index;
std::size_t capacity = 0;
bool fixedCapacity = false;
std::list<std::size_t> evictedOrder;
std::unordered_map<std::size_t, decltype(evictedOrder)::iterator> evictedKeys;
std::size_t evictionsSinceCheck = 0;
std::size_t returnsSinceCheck = 0;
std::unordered_set<std::size_t> usedKeys;
std::size_t usesSinceCheck = 0;
static constexpr std::size_t InitialShrinkWindow = 8;
static constexpr std::size_t MaximumShrinkWindow = 1024;
std::size_t baseEntries = InitialEntries;
std::size_t baseShrinkWindow = InitialShrinkWindow;
std::size_t shrinkWindow = InitialShrinkWindow;
bool shrankLast = false;
};
// The process-wide cache the device and the draw path share (keys name the device; a device clears
@@ -67,6 +67,7 @@ struct ColorTarget {
bool uintExport = false;
std::uint32_t pipeBankXor = 0;
std::uint32_t samples = 1;
std::uint32_t dccControl = 0;
};
struct DepthTarget {
@@ -34,6 +34,7 @@ struct HostImport;
VkFormat StorageFormatForGuest(const Context& context, std::uint32_t guestFormat);
// Whether StorageFormatForGuest would succeed, without throwing (unknown guest formats included).
bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat);
VkFormat HostStorageFormat(const Context& context, VkFormat format);
bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys);
std::uint64_t SampledTextureMemory();
bool ClearKeepsDenormals(const Context& context, VkFormat format);
@@ -188,6 +189,7 @@ public:
void MarkDirty();
void MarkSliceDirty(std::uint32_t slice);
void Flush();
bool Dirty() const;
// Stores every pending image overlapping the range, except `except`; returns whether any was.
// Stores into host-imported memory are recorded (not waited for): a CPU reader syncs afterwards.
// Then the unit shadows over the range are published into their imports as `scope` says (see
@@ -84,6 +84,11 @@ inline constexpr TextureSwizzleEquation kTextureSwizzleEquations[] = {
{0x109u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x00004000u, 0x00000004u, 0x04000000u, 0x00008000u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}},
{0x109u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u}},
{0x109u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u}},
{0x115u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x115u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x115u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x115u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x20002020u, 0x10010004u, 0x04008010u, 0x08004008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x115u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x20002010u, 0x10010002u, 0x04008008u, 0x08004004u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
{0x119u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000010u, 0x10000000u, 0x00010000u, 0x00000020u}},
{0x119u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000008u, 0x10000000u, 0x00010000u, 0x00000010u}},
{0x119u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}},
@@ -224,8 +224,6 @@ private:
std::unordered_map<std::uint64_t, Entry> entries;
};
// The whole byte range a vertex buffer descriptor covers (records * stride, or records when the
// stride is 0): what an indirect draw, whose counts only the GPU knows, copies for the fetch.
inline std::uint32_t VertexBufferOutOfBoundsSelect(const ShaderRecompiler::VertexAttribute& attribute) {
return (attribute.resource.fields[3] >> 28u) & 3u;
}
@@ -247,7 +245,7 @@ inline std::size_t VertexBufferExtent(const ShaderRecompiler::VertexAttribute& a
const auto stride = (attribute.resource.fields[1] >> 16u) & 0x3fffu;
const auto records = attribute.resource.fields[2];
Require(attribute.fetchIndex <= 1, "unsupported vertex fetch index");
const auto bytes = stride == 0 ? ZeroStrideAvailableBytes(attribute) : static_cast<std::uint64_t>(records) * stride;
const auto bytes = stride == 0 ? ZeroStrideAvailableBytes(attribute) : records == 0 ? 0 : static_cast<std::uint64_t>(records - 1u) * stride + DecodeVertexFormat(attribute).bytes;
Require(bytes != 0 && bytes <= std::numeric_limits<std::size_t>::max(), "empty or oversized vertex buffer descriptor");
const auto address = attribute.resource.fields[0] | (static_cast<std::uint64_t>(attribute.resource.fields[1] & 0xffffu) << 32u);
Require(address != 0 && bytes <= std::numeric_limits<std::uint64_t>::max() - address, "invalid vertex buffer address range");
@@ -36,10 +36,10 @@ constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
}
BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")), allocateMemory(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")), mapMemory(context.Function<PFN_vkMapMemory>("vkMapMemory")), maxSlots(std::max<std::size_t>(1, std::min<std::size_t>(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))) {
BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")), allocateMemory(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")), mapMemory(context.Function<PFN_vkMapMemory>("vkMapMemory")), maxSlots(std::max<std::size_t>(1, std::min<std::size_t>(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim) {
smallTier.budget = smallBudget;
largeTier.budget = budget;
deviceTier.budget = DeviceTierEnabled() ? DeviceBudget(context.memory) : 0;
deviceTier.budget = deviceBudget(context);
}
BufferPool::~BufferPool() {
@@ -57,7 +57,9 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation
const auto initialResult = result;
unsigned attempts = 1;
VkDeviceSize reclaimed = 0;
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) {
VkDeviceSize released = 0;
const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY; };
if (exhausted(result)) {
reclaimed = trim();
if (reclaimed != 0) {
memory = VK_NULL_HANDLE;
@@ -65,16 +67,28 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation
++attempts;
}
}
if (exhausted(result)) {
if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize));
if (released != 0) {
memory = VK_NULL_HANDLE;
result = allocateMemory(device, &allocation, nullptr, &memory);
++attempts;
}
}
if (result == VK_SUCCESS) {
if (attempts == 2) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu initialResult=%d attempts=%u\n", operation, static_cast<unsigned long long>(allocation.allocationSize), static_cast<unsigned long long>(reclaimed), static_cast<int>(initialResult), attempts);
if (attempts > 1) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu released=%llu initialResult=%d attempts=%u\n", operation, static_cast<unsigned long long>(allocation.allocationSize), static_cast<unsigned long long>(reclaimed), static_cast<unsigned long long>(released), static_cast<int>(initialResult), attempts);
return memory;
}
char details[512];
std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu initialResult=%d attempts=%u", operation, static_cast<unsigned long long>(allocation.allocationSize), allocation.memoryTypeIndex, static_cast<unsigned long long>(reclaimed), static_cast<int>(initialResult), attempts);
std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu released=%llu initialResult=%d attempts=%u", operation, static_cast<unsigned long long>(allocation.allocationSize), allocation.memoryTypeIndex, static_cast<unsigned long long>(reclaimed), static_cast<unsigned long long>(released), static_cast<int>(initialResult), attempts);
Check(result, details);
return VK_NULL_HANDLE;
}
VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) {
return bytes + std::max<VkDeviceSize>(bytes, VkDeviceSize{256} << 20u);
}
bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) {
if (capacity >= classLimit || size > capacity || !std::has_single_bit(capacity)) return false;
const auto alignmentOk = alignment == 0 || (std::has_single_bit(alignment) && alignment <= capacity);
@@ -106,7 +120,11 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType,
if (!slab.available.empty()) return takeFrom(*slab.available.back(), slab);
}
const auto blockBytes = SlabBlockBytes(capacity);
auto block = std::make_unique<SlabBlock>();
const auto release = [this](SlabBlock* block) noexcept {
if (block->memory != VK_NULL_HANDLE) freeBlock(*block);
delete block;
};
std::unique_ptr<SlabBlock, decltype(release)> block(new SlabBlock, release);
block->slotBytes = capacity;
block->slab = key;
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
@@ -118,7 +136,6 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType,
if ((context.memory.memoryTypes[memoryType].propertyFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) != 0) {
void* mapping = nullptr;
const auto mapped = mapMemory(device, block->memory, 0, VK_WHOLE_SIZE, 0, &mapping);
if (mapped != VK_SUCCESS) freeMemory(device, block->memory, nullptr);
Check(mapped, "vkMapMemory buffer slab");
block->mapping = static_cast<std::byte*>(mapping);
}
@@ -127,8 +144,10 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType,
for (std::uint32_t index = slots; index-- != 0;) block->free.push_back(index);
std::lock_guard lock(slabMutex);
auto& slab = slabs[key];
if (slab.available.size() == slab.available.capacity()) slab.available.reserve(std::max<std::size_t>(1, slab.available.capacity() * 2));
auto& inserted = *block;
slabBlocks.emplace(inserted.memory, std::move(block));
const auto owner = slabBlocks.try_emplace(inserted.memory).first;
owner->second.reset(block.release());
slab.available.push_back(&inserted);
++slab.emptyBlocks;
return takeFrom(*slab.available.back(), slab);
@@ -175,18 +194,27 @@ bool BufferPool::DeviceTierEnabled() {
return enabled;
}
VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory) {
VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) {
static const char* value = std::getenv("APS5_STAGING_POOL_MIB");
if (value != nullptr) return static_cast<VkDeviceSize>(std::strtoull(value, nullptr, 10)) << 20u;
VkDeviceSize largest = 0;
bool systemMemory = false;
std::optional<std::uint32_t> largest;
for (std::uint32_t heap = 0; heap < memory.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) {
if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 || memory.memoryHeaps[heap].size <= largest) continue;
largest = memory.memoryHeaps[heap].size;
systemMemory = systemMemoryHeap(memory, heap);
if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!largest || memory.memoryHeaps[heap].size > memory.memoryHeaps[*largest].size)) largest = heap;
}
if (systemMemory) return std::max<VkDeviceSize>(VkDeviceSize{128} << 20u, largest / 64u);
return std::max<VkDeviceSize>(VkDeviceSize{512} << 20u, largest / 8u);
VkDeviceSize size = largest ? memory.memoryHeaps[*largest].size : 0;
if (largest && reported != nullptr && reported->heapBudget[*largest] != 0) size = std::min(size, reported->heapBudget[*largest]);
if (largest && systemMemoryHeap(memory, *largest)) return std::max<VkDeviceSize>(VkDeviceSize{128} << 20u, size / 64u);
return std::max<VkDeviceSize>(VkDeviceSize{512} << 20u, size / 8u);
}
VkDeviceSize BufferPool::deviceBudget(const Context& context) {
if (!DeviceTierEnabled()) return 0;
if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return DeviceBudget(context.memory);
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2};
properties.pNext = &reported;
context.memoryProperties2(context.physical, &properties);
return DeviceBudget(context.memory, &reported);
}
VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept {
@@ -223,7 +251,7 @@ VkBufferUsageFlags BufferPool::Usage(VkBufferUsageFlags usage, VkMemoryPropertyF
}
BufferPool::Tier& BufferPool::tierFor(std::size_t capacity, VkMemoryPropertyFlags properties) {
if (DeviceTiered(properties)) return deviceTier;
if (DeviceTiered(properties) && (sharedTiers() || capacity >= classLimit)) return deviceTier;
return !sharedTiers() && capacity < classLimit ? smallTier : largeTier;
}
@@ -234,7 +234,7 @@ void TraceKeyStore(const char* path, std::uint64_t begin, std::size_t count) {
bool StoreUncompressedOnGpu(const Context& context, Recorder& recorder, std::uint64_t begin, std::size_t count) {
const auto* import = HostImportFor(context, begin, count);
if (import == nullptr) return false;
const VkDeviceSize first = begin - import->base;
const VkDeviceSize first = HostImportOffset(*import, begin);
const VkDeviceSize last = first + count;
const VkDeviceSize fillBegin = std::min((first + 3) & ~VkDeviceSize{3}, last);
const VkDeviceSize fillEnd = std::max(last & ~VkDeviceSize{3}, fillBegin);
@@ -761,13 +761,62 @@ bool PendingFillsLeaveUncompressed(const Recorder& recorder, const GuestTextureR
}
namespace {
struct KeyWaitTotals {
std::atomic<std::uint64_t> calls{0};
std::atomic<std::uint64_t> noPending{0};
std::atomic<std::uint64_t> known{0};
std::atomic<std::uint64_t> fillsUncompressed{0};
std::atomic<std::uint64_t> synced{0};
};
KeyWaitTotals& KeyWaits() {
static KeyWaitTotals totals;
return totals;
}
}
DccKeyWaitCounts KeyWaitCounts() {
const auto& totals = KeyWaits();
return {totals.calls.load(std::memory_order_relaxed), totals.noPending.load(std::memory_order_relaxed), totals.known.load(std::memory_order_relaxed), totals.fillsUncompressed.load(std::memory_order_relaxed), totals.synced.load(std::memory_order_relaxed)};
}
bool DccKeyWritersPending(std::uint64_t metaAddress, std::size_t count) {
if (metaAddress == 0 || count == 0 || !Recorder::SnapshotWriteOverlaps(metaAddress, count)) return false;
auto& memo = Memo();
std::lock_guard lock(memo.mutex);
MemoRecorder(memo);
return std::none_of(memo.entries.begin(), memo.entries.end(), [&](const GpuKeyStore& store) { return store.note != 0 && store.begin <= metaAddress && metaAddress + count <= store.end; });
}
bool DccKeyWriterNeedsSync(std::uint64_t metaAddress, std::size_t count) {
if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return false;
auto* recorder = Recorder::Active();
return recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count) && !PendingStoreKeys(*recorder, metaAddress, count).has_value();
}
std::optional<DccKeys> WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes) {
const auto count = static_cast<std::size_t>(guestBytes / KeyBytes);
const auto metaAddress = resource.dccAddress;
if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return std::nullopt;
auto& waits = KeyWaits();
waits.calls.fetch_add(1, std::memory_order_relaxed);
auto* recorder = Recorder::Active();
if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count) || PendingStoreKeys(*recorder, metaAddress, count).has_value()) return std::nullopt;
if (PendingFillsLeaveUncompressed(*recorder, resource, count)) return DccKeys::Uncompressed;
if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count)) {
waits.noPending.fetch_add(1, std::memory_order_relaxed);
return std::nullopt;
}
if (PendingStoreKeys(*recorder, metaAddress, count).has_value()) {
waits.known.fetch_add(1, std::memory_order_relaxed);
return std::nullopt;
}
if (PendingFillsLeaveUncompressed(*recorder, resource, count)) {
waits.fillsUncompressed.fetch_add(1, std::memory_order_relaxed);
return DccKeys::Uncompressed;
}
waits.synced.fetch_add(1, std::memory_order_relaxed);
Recorder::CountSync(2);
recorder->SyncThrough(metaAddress, count);
return std::nullopt;
@@ -13,6 +13,7 @@
#include <array>
#include <atomic>
#include <bit>
#include <cmath>
#include <limits>
#include <shared_mutex>
#include <cstdio>
@@ -119,6 +120,104 @@ DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarge
return state;
}
std::atomic<bool> depthPlaneClearEnabled{true};
enum DepthClearDecline : std::size_t { DeclineLayers, DeclineFormat, DeclineValue, DeclineColor, DeclinePending, DeclineCoverage, DeclineSize, DeclineCount };
static_assert(DeclineCount <= 8, "DepthClearStatistics::declined holds eight reasons");
struct DepthClearCounters {
std::atomic<std::uint64_t> matched{0};
std::atomic<std::uint64_t> taken{0};
std::atomic<std::uint64_t> copiesSkipped{0};
std::atomic<std::uint64_t> clears{0};
std::atomic<std::uint64_t> bytesSkipped{0};
std::array<std::atomic<std::uint64_t>, 8> declined{};
};
DepthClearCounters& depthClearCounters() {
static DepthClearCounters counters;
return counters;
}
thread_local DepthPlaneOverwrite* currentOverwrite = nullptr;
std::optional<DepthPlaneOverwrite> decodeOverwrite(const StorageImageClear& clear) {
auto& counters = depthClearCounters();
++counters.matched;
const auto decline = [&](DepthClearDecline why) -> std::optional<DepthPlaneOverwrite> {
++counters.declined[why];
return std::nullopt;
};
DepthPlaneOverwrite overwrite;
overwrite.image = clear.image;
try {
overwrite.view = DecodeTextureResource(clear.image);
} catch (...) {
return decline(DeclineFormat);
}
const auto& view = overwrite.view;
const bool oneLayer = view.dimension == TextureDimension::k2D || (view.dimension == TextureDimension::k2DArray && view.baseArray == 0 && view.depthOrLastArray == 0);
if (!oneLayer || view.baseLevel != 0) return decline(DeclineLayers);
VkFormat format = VK_FORMAT_UNDEFINED;
try {
format = ResolveTextureFormat(view.format);
} catch (...) {
return decline(DeclineFormat);
}
enum class Kind { Unorm16, Uint16, Float32, Uint32 };
Kind kind;
switch (format) {
case VK_FORMAT_R16_UNORM: kind = Kind::Unorm16; break;
case VK_FORMAT_R16_UINT: kind = Kind::Uint16; break;
case VK_FORMAT_R32_SFLOAT: kind = Kind::Float32; break;
case VK_FORMAT_R32_UINT:
case VK_FORMAT_R32_SINT: kind = Kind::Uint32; break;
default: return decline(DeclineFormat);
}
const std::uint64_t base = clear.color[0] | (static_cast<std::uint64_t>(clear.color[1] & 0xffffu) << 32u);
const std::uint64_t stride = (clear.color[1] >> 16u) & 0x3fffu;
const std::uint64_t extent = stride == 0 ? clear.color[2] : static_cast<std::uint64_t>(clear.color[2]) * stride;
if (base == 0 || (base & 3u) != 0u || extent < 16u || !GuestMemory::Accessible(reinterpret_cast<const void*>(base), 16)) return decline(DeclineColor);
if (const auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(base, 16)) return decline(DeclinePending);
const std::pair<std::uint64_t, std::uint64_t> colorRange{base, base + 16u};
if (StorageTexture::AnyPendingOverlaps(std::span(&colorRange, 1))) return decline(DeclinePending);
std::uint32_t c0 = 0;
try {
GuestMemory::Read(base, std::as_writable_bytes(std::span(&c0, 1)), 4);
} catch (...) {
return decline(DeclineColor);
}
switch (kind) {
case Kind::Unorm16:
if (c0 == 0u || c0 == 0x80000000u) overwrite.raw = 0u;
else if (c0 == 0x3f800000u) overwrite.raw = 0xffffu;
else return decline(DeclineValue);
overwrite.bits = 16u;
overwrite.depth = static_cast<float>(overwrite.raw) / 65535.0f;
break;
case Kind::Uint16:
if (c0 != 0u && c0 != 0xffffu) return decline(DeclineValue);
overwrite.raw = c0;
overwrite.bits = 16u;
overwrite.depth = static_cast<float>(overwrite.raw) / 65535.0f;
break;
case Kind::Float32:
case Kind::Uint32: {
const auto depth = std::bit_cast<float>(c0);
if (c0 != 0u && !(std::isnormal(depth) && depth > 0.0f && depth <= 1.0f)) return decline(DeclineValue);
overwrite.raw = c0;
overwrite.bits = 32u;
overwrite.depth = depth;
break;
}
}
overwrite.colorAddress = base;
overwrite.colorBits = c0;
overwrite.width = clear.width;
overwrite.height = clear.height;
return overwrite;
}
class DepthSurface {
public:
DepthSurface(const Context& context, const DepthTarget& target, DepthSamplingState sampling = {}) : context(context), target(target), hostFormat(HostDepthFormat(context, target.format)), sampling(std::move(sampling)) {
@@ -249,6 +348,14 @@ public:
}
void RecordClear(VkImageAspectFlags aspects) {
const float depth = depthFill.value_or(clearDepth);
const std::uint8_t stencil = stencilFill.value_or(clearStencil);
depthFill.reset();
stencilFill.reset();
RecordClearValue(aspects, depth, stencil);
}
void RecordClearValue(VkImageAspectFlags aspects, float depth, std::uint8_t stencil) {
written = ++depthWrites;
auto* recorder = RecorderFor(context);
std::unique_ptr<CommandBatch> batch;
@@ -257,7 +364,7 @@ public:
const VkImageSubresourceRange range{aspects, 0, 1, 0, 1};
constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_WRITE_BIT);
const VkClearDepthStencilValue clear{clearDepth, clearStencil};
const VkClearDepthStencilValue clear{depth, stencil};
context.Function<PFN_vkCmdClearDepthStencilImage>("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, access);
if (batch) batch->SubmitAndWait();
@@ -267,16 +374,20 @@ public:
}
}
bool UnormStencilView(const GuestTextureResource& resource) const {
return target.samples == 1 && target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress && ResolveTextureFormat(resource.format) == VK_FORMAT_R8_UNORM;
}
bool SampledAccepts(std::span<const std::uint32_t> words, const GuestTextureResource& resource) const {
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto expected = stencil ? VK_FORMAT_R8_UINT : d16 ? VK_FORMAT_R16_UNORM : VK_FORMAT_R32_SFLOAT;
const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u);
const bool oneSlice = target.samples != 1 ? resource.dimension == TextureDimension::k2DMsaa : resource.dimension == TextureDimension::k2D || (resource.dimension == TextureDimension::k2DArray && resource.depthOrLastArray == 0);
return (ResolveTextureFormat(resource.format) == expected || depthBits) && oneSlice && resource.width == target.extent.width && resource.height == target.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && resource.baseArray == 0;
return (ResolveTextureFormat(resource.format) == expected || depthBits || UnormStencilView(resource)) && oneSlice && resource.width == target.extent.width && resource.height == target.extent.height && resource.baseLevel == 0 && resource.lastLevel == 0 && resource.baseArray == 0;
}
std::shared_ptr<Texture> Sampled(std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components) {
std::shared_ptr<Texture> Sampled(std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, bool drawn = false) {
CheckCurrent();
CheckResource(resource);
std::array<std::uint32_t, 12> key{};
@@ -298,6 +409,9 @@ public:
Require(resource.samples == target.samples, "the sampled depth descriptor's sample count differs from its native target");
Require(hostFormat == target.format, "sampling a native multisampled depth plane that the device stores in another depth format is not implemented");
}
if (stencil && UnormStencilView(resource) && SampledAccepts(words, resource)) {
return unormStencil(key, components, resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, drawn);
}
if (!SampledAccepts(words, resource)) {
char text[448];
std::snprintf(text, sizeof(text), "AGC graphics: sampling the %s plane of depth surface 0x%llx (%ux%u, vk format %d) as a %ux%u texture of guest format %u (vk %d), tile mode %u, dimension %d, levels %u-%u, slice %u is not implemented (T# %08x %08x %08x %08x %08x %08x %08x %08x)",
@@ -315,8 +429,25 @@ public:
return texture;
}
void ForgetClear(VkImageAspectFlags aspects) {
pendingClear &= ~aspects;
if ((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) depthFill.reset();
if ((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) stencilFill.reset();
}
void NoteHtileClear(VkImageAspectFlags aspects) {
ForgetClear(aspects);
pendingClear |= aspects;
}
void NotePlaneFill(VkImageAspectFlagBits aspect, float depth, std::uint8_t stencil) {
pendingClear |= aspect;
if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) depthFill = depth;
else stencilFill = stencil;
}
void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) {
pendingClear &= ~static_cast<VkImageAspectFlags>(aspect);
ForgetClear(aspect);
auto* recorder = RecorderFor(context);
std::unique_ptr<CommandBatch> batch;
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
@@ -334,7 +465,8 @@ public:
}
bool Holds(const Texture* texture) const {
return std::any_of(textures.begin(), textures.end(), [&](const auto& entry) { return entry.second.get() == texture; });
const auto held = [&](const auto& entry) { return entry.second.get() == texture; };
return std::any_of(textures.begin(), textures.end(), held) || std::any_of(stencilCopy.textures.begin(), stencilCopy.textures.end(), held);
}
std::uint64_t written = ++depthWrites;
@@ -362,6 +494,8 @@ public:
VkImageAspectFlags pendingClear = 0;
float clearDepth = 0.0f;
std::uint8_t clearStencil = 0;
std::optional<float> depthFill;
std::optional<std::uint8_t> stencilFill;
bool StorageViewMatches(std::span<const std::uint32_t> words, const GuestTextureResource& resource) const {
if (resource.width != target.extent.width || resource.height != target.extent.height) return false;
@@ -372,7 +506,7 @@ public:
return ResolveTextureFormat(resource.format) == expected || depthBits;
}
void RequireStorable(const GuestTextureResource& resource, std::uint32_t mip) const {
void RequireStorable(const GuestTextureResource& resource) const {
const bool stencil = target.stencilAddress != 0 && resource.baseAddress == target.stencilAddress;
Require(target.samples == 1, "storage image access to a multisampled depth surface is not implemented");
if (!stencil && hostFormat != target.format) {
@@ -380,11 +514,6 @@ public:
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to the depth plane of depth surface 0x%llx (vk format %d) stored as vk format %d is not implemented", static_cast<unsigned long long>(target.address), static_cast<int>(target.format), static_cast<int>(hostFormat));
throw std::runtime_error(text);
}
if (mip != 0 || resource.baseLevel != 0) {
char text[224];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to level %u of the %s plane of depth surface 0x%llx is not implemented", mip, stencil ? "stencil" : "depth", static_cast<unsigned long long>(target.address));
throw std::runtime_error(text);
}
}
std::shared_ptr<StorageTexture> Storage(std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
@@ -396,7 +525,8 @@ public:
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to depth/stencil surface 0x%llx is not implemented (storage=%ux%u guest format=%u mip=%u dimension=%d tile=%d)", static_cast<unsigned long long>(resource.baseAddress), resource.width, resource.height, resource.format, mip, static_cast<int>(resource.dimension), static_cast<int>(resource.tileMode));
throw std::runtime_error(text);
}
RequireStorable(resource, mip);
RequireStorable(resource);
auto* overwrite = stencil ? nullptr : PlaneOverwritten(words);
ApplyFastClear();
auto& plane = stencil ? stencilPlane : depthPlane;
if (plane.storage == nullptr || plane.storage->Descriptor().format != resource.format || plane.storage->Descriptor().tileMode != resource.tileMode || plane.storage->Descriptor().baseAddress != resource.baseAddress || plane.storage->Descriptor().dimension != resource.dimension) {
@@ -404,15 +534,67 @@ public:
if (plane.storage != nullptr) plane.replaced.push_back(plane.storage);
plane.storage = StorageTexture::Create(context, *context.detiler, resource, 0);
}
copyPlane(plane, plane.storage, stencil, true);
if (overwrite != nullptr) {
++depthClearCounters().copiesSkipped;
} else {
copyPlane(plane, plane.storage, stencil, true);
}
return plane.storage;
}
bool OverwritesWholePlane(const DepthPlaneOverwrite& overwrite) const {
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
return overwrite.bits == (d16 ? 16u : 32u) && overwrite.width >= target.extent.width && overwrite.height >= target.extent.height;
}
DepthPlaneOverwrite* PlaneOverwritten(std::span<const std::uint32_t> words) const {
auto* overwrite = currentOverwrite;
if (overwrite == nullptr || words.size() < overwrite->image.size() || !std::equal(overwrite->image.begin(), overwrite->image.end(), words.begin())) return nullptr;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
auto& declined = depthClearCounters().declined;
if (overwrite->bits != (d16 ? 16u : 32u)) {
++declined[DeclineSize];
return nullptr;
}
if (overwrite->width < target.extent.width || overwrite->height < target.extent.height) {
++declined[DeclineCoverage];
return nullptr;
}
return overwrite;
}
bool StoreOverwrite(const StorageTexture& mirror) {
auto* overwrite = currentOverwrite;
if (overwrite == nullptr || !OverwritesWholePlane(*overwrite)) return false;
const auto& described = mirror.Descriptor();
const auto& view = overwrite->view;
if (described.baseAddress != view.baseAddress || described.format != view.format || described.tileMode != view.tileMode || described.dimension != view.dimension) return false;
std::uint32_t current = 0;
try {
GuestMemory::Read(overwrite->colorAddress, std::as_writable_bytes(std::span(&current, 1)), 4);
} catch (...) {
return false;
}
if (current != overwrite->colorBits) return false;
auto* recorder = RecorderFor(context);
Require(recorder != nullptr, "depth surface storage clears need the recorder of the surface's device");
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto bytes = static_cast<std::size_t>(target.extent.width) * target.extent.height * (d16 ? 2u : 4u);
recorder->Commands();
const auto timing = recorder->BeginGpuTiming(Recorder::CommandClass::Copy);
RecordClearValue(VK_IMAGE_ASPECT_DEPTH_BIT, overwrite->depth, clearStencil);
recorder->EndGpuTiming(timing, bytes);
auto& counters = depthClearCounters();
++counters.clears;
counters.bytesSkipped += 2u * bytes;
return true;
}
bool StoreBack(const StorageTexture& candidate) {
for (const bool stencil : {false, true}) {
auto& plane = stencil ? stencilPlane : depthPlane;
if (plane.storage.get() == &candidate) {
copyPlane(plane, plane.storage, stencil, false);
if (stencil || !StoreOverwrite(candidate)) copyPlane(plane, plane.storage, stencil, false);
written = ++depthWrites;
return true;
}
@@ -481,11 +663,101 @@ private:
std::vector<std::weak_ptr<StorageTexture>> replaced;
std::unique_ptr<DeviceBuffer> staging;
};
struct StencilCopy {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
std::unique_ptr<DeviceBuffer> staging;
std::map<std::array<std::uint32_t, 12>, std::shared_ptr<Texture>> textures;
std::uint64_t written = 0;
bool filled = false;
};
std::map<std::array<std::uint32_t, 12>, std::shared_ptr<Texture>> textures;
PlaneStorage depthPlane;
PlaneStorage stencilPlane;
std::uint64_t depthMemory = 0;
std::uint64_t stencilMemory = 0;
StencilCopy stencilCopy;
std::shared_ptr<Texture> unormStencil(const std::array<std::uint32_t, 12>& key, VkComponentMapping components, VkImageViewType viewType, bool drawn) {
if (stencilCopy.image == VK_NULL_HANDLE) {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.imageType = VK_IMAGE_TYPE_2D;
info.format = VK_FORMAT_R8_UNORM;
info.extent = {target.extent.width, target.extent.height, 1};
info.mipLevels = 1;
info.arrayLayers = 1;
info.samples = VK_SAMPLE_COUNT_1_BIT;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
try {
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &stencilCopy.image), "vkCreateImage stencil copy");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, stencilCopy.image, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &stencilCopy.memory), "vkAllocateMemory stencil copy");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, stencilCopy.image, stencilCopy.memory, 0), "vkBindImageMemory stencil copy");
stencilCopy.staging = std::make_unique<DeviceBuffer>(context, static_cast<std::size_t>(target.extent.width) * target.extent.height, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT);
} catch (...) {
releaseStencilCopy();
throw;
}
}
if (drawn || !stencilCopy.filled || stencilCopy.written != written) {
copyStencil();
stencilCopy.written = drawn ? 0 : written;
}
auto& texture = stencilCopy.textures[key];
if (texture == nullptr) {
texture = std::make_shared<Texture>(context, stencilCopy.image, VK_FORMAT_R8_UNORM, VK_IMAGE_ASPECT_COLOR_BIT, components, viewType);
texture->MarkRefreshedPerUse();
}
return texture;
}
void copyStencil() {
auto* recorder = RecorderFor(context);
std::unique_ptr<CommandBatch> batch;
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
const auto commands = recorder != nullptr ? recorder->Commands() : batch->Handle();
const auto barrier = context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier");
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
VkBufferImageCopy region{0, 0, 0, {VK_IMAGE_ASPECT_STENCIL_BIT, 0, 0, 1}, {0, 0, 0}, {target.extent.width, target.extent.height, 1}};
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, image, VK_IMAGE_LAYOUT_GENERAL, stencilCopy.staging->Handle(), 1, &region);
VkImageMemoryBarrier toTransfer{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toTransfer.srcAccessMask = VK_ACCESS_SHADER_READ_BIT;
toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toTransfer.oldLayout = stencilCopy.filled ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED;
toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toTransfer.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toTransfer.image = stencilCopy.image;
toTransfer.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
const VkBufferMemoryBarrier staged{VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, nullptr, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, VK_QUEUE_FAMILY_IGNORED, VK_QUEUE_FAMILY_IGNORED, stencilCopy.staging->Handle(), 0, VK_WHOLE_SIZE};
barrier(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 1, &staged, 1, &toTransfer);
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
context.Function<PFN_vkCmdCopyBufferToImage>("vkCmdCopyBufferToImage")(commands, stencilCopy.staging->Handle(), stencilCopy.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &region);
VkImageMemoryBarrier toGeneral = toTransfer;
toGeneral.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toGeneral.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
toGeneral.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
barrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
stencilCopy.filled = true;
if (batch) batch->SubmitAndWait();
else Recorder::CountBarriers(Recorder::CommandClass::Copy, 3);
}
void releaseStencilCopy() noexcept {
stencilCopy.textures.clear();
stencilCopy.staging.reset();
if (stencilCopy.image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, stencilCopy.image, nullptr);
if (stencilCopy.memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, stencilCopy.memory, nullptr);
stencilCopy = {};
}
std::uint64_t depthBytes() const {
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
@@ -532,6 +804,7 @@ private:
void release() noexcept {
depthPlane = {};
stencilPlane = {};
releaseStencilCopy();
textures.clear();
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (owned != nullptr) owned.reset();
@@ -857,8 +1130,13 @@ void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address
auto& entry = owners[{surface.context.device, address}];
auto* owner = entry.surface;
if (owner != nullptr && owner->active->load(std::memory_order_acquire) && owner != &surface && owner->target.format == surface.target.format && owner->target.samples == surface.target.samples && owner->target.extent.width == surface.target.extent.width && owner->target.extent.height == surface.target.extent.height) {
if ((owner->pendingClear & aspect) != 0) surface.pendingClear |= aspect;
else surface.CopyAspectFrom(*owner, aspect);
if ((owner->pendingClear & aspect) != 0) {
surface.pendingClear |= aspect;
if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) surface.depthFill = owner->depthFill;
else surface.stencilFill = owner->stencilFill;
} else {
surface.CopyAspectFrom(*owner, aspect);
}
}
entry = {&surface, ++planeBinds};
}
@@ -1040,7 +1318,7 @@ void RetireUnmappedDepth_nid_no_patch(std::uintptr_t address, std::size_t bytes)
}
bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples) {
if (samples <= 1 || (context.sampleLocationSampleCounts & samples) == 0) return false;
if (samples == 0 || (context.sampleLocationSampleCounts & samples) == 0) return false;
VkImageFormatProperties properties{};
const auto result = context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, VK_IMAGE_CREATE_SAMPLE_LOCATIONS_COMPATIBLE_DEPTH_BIT_EXT, &properties);
@@ -1128,9 +1406,9 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target)
if (created && !inherits && target.htileAddress == 0) surface->TakeStorage();
if (cleared && (!created || inherits)) {
if (target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented";
surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT;
surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT);
}
surface->pendingClear |= htileCleared;
surface->NoteHtileClear(htileCleared);
surface->ApplyFastClear();
if (!created || inherits) surface->written = ++depthWrites;
surface->NoteMemory();
@@ -1171,6 +1449,64 @@ void NoteMemoryValueFill(const MemoryValueFill& fill) {
NoteDepthMetadataFill(fill.address, fill.bytes, value);
}
std::optional<StorageImageClear> MatchStorageImageClear(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, const std::array<std::uint32_t, 3>& groups) {
static constexpr std::array<std::uint32_t, 15> kernel{0xd7460004u, 0x0401060cu, 0xf4280204u, 0xfa000000u, 0xd7460005u, 0x0405060du, 0x7e0c020eu, 0xbf8cc07fu, 0x7e000208u, 0x7e020209u, 0x7e04020au, 0x7e06020bu, 0xf0200f28u, 0x00000004u, 0xbf810000u};
if (code.size() < kernel.size() || code[0] != kernel[0] || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt;
if (userData.size() != 12u) return std::nullopt;
if (compute.numThreads != std::array<std::uint32_t, 3>{8u, 8u, 1u}) return std::nullopt;
if (!compute.groupIdEnable[0] || !compute.groupIdEnable[1] || !compute.groupIdEnable[2]) return std::nullopt;
if (compute.threadIdComponentCount < 2u) return std::nullopt;
std::array<std::uint64_t, 3> covered{};
for (std::size_t axis = 0; axis < 3u; ++axis) covered[axis] = compute.partialThreads[axis] != 0u ? compute.partialThreads[axis] : static_cast<std::uint64_t>(groups[axis]) * compute.numThreads[axis];
if (covered[0] == 0u || covered[1] == 0u || covered[2] != 1u) return std::nullopt;
StorageImageClear clear;
std::copy_n(userData.begin(), clear.image.size(), clear.image.begin());
std::copy_n(userData.begin() + static_cast<std::ptrdiff_t>(clear.image.size()), clear.color.size(), clear.color.begin());
clear.width = static_cast<std::uint32_t>(std::min<std::uint64_t>(covered[0], std::numeric_limits<std::uint32_t>::max()));
clear.height = static_cast<std::uint32_t>(std::min<std::uint64_t>(covered[1], std::numeric_limits<std::uint32_t>::max()));
return clear;
}
std::uint64_t StorageImageClearAddress(const StorageImageClear& clear) {
try {
return DecodeTextureResource(clear.image).baseAddress;
} catch (...) {
return 0u;
}
}
DepthPlaneClear::DepthPlaneClear(const StorageImageClear& clear) {
if (!depthPlaneClearEnabled.load(std::memory_order_relaxed)) return;
const auto decoded = decodeOverwrite(clear);
if (decoded) {
value = *decoded;
previous = currentOverwrite;
currentOverwrite = &value;
installed = true;
++depthClearCounters().taken;
}
}
DepthPlaneClear::~DepthPlaneClear() {
if (installed) currentOverwrite = previous;
}
DepthClearStatistics DepthClearCounts() {
const auto& counters = depthClearCounters();
DepthClearStatistics statistics;
statistics.matched = counters.matched.load();
statistics.taken = counters.taken.load();
statistics.copiesSkipped = counters.copiesSkipped.load();
statistics.clears = counters.clears.load();
statistics.bytesSkipped = counters.bytesSkipped.load();
for (std::size_t reason = 0; reason < statistics.declined.size(); ++reason) statistics.declined[reason] = counters.declined[reason].load();
return statistics;
}
void SetDepthPlaneClearForTest(bool enabled) {
depthPlaneClearEnabled.store(enabled, std::memory_order_relaxed);
}
std::size_t HtileDepthClearBytes_nid_no_patch(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span<const std::uint32_t> packet, std::uint64_t indirectArguments) {
if (userData.size() != 6u || packet.size() < 5u || indirectArguments != 0u || compute.PartialGroups() || !compute.groupIdEnable[0] || compute.threadIdComponentCount == 0u || compute.threadIdComponentCount > 3u) return 0u;
if (packet[1] == 0u || packet[1] > 0x400000u || packet[2] != 1u || packet[3] != 1u || (packet[4] & 0x23u) != 1u) return 0u;
@@ -1196,7 +1532,7 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz
}
if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue;
if (surface->target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented";
surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT;
surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT);
existing = true;
}
if (!existing) clearedHtiles()[htileAddress] = bytes;
@@ -1244,7 +1580,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
Recorder::CountBarriers(Recorder::CommandClass::Draw, 1);
return found->Sampled(words, resource, components);
}
if (resource.width != base.extent.width || resource.height != base.extent.height) return nullptr;
if (resource.width != base.extent.width || resource.height != base.extent.height || resource.baseLevel != 0) return nullptr;
const bool d16 = base.format == VK_FORMAT_D16_UNORM || base.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto format = ResolveTextureFormat(resource.format);
const bool depthBits = !stencil && words.size() >= 4 && ShaderRecompiler::DepthBitsTextureWidth(words[1], words[3]) == (d16 ? 16u : 32u);
@@ -1260,7 +1596,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
return nullptr;
}
found->ApplyFastClear();
if (sampled) return found->Sampled(words, resource, components);
if (sampled) return found->Sampled(words, resource, components, found == drawnDepth);
Require(found->hostFormat == base.format, "sampling several slices or an integer view of a depth plane that the device stores in another depth format is not implemented");
const auto layers = cube ? 6u : layered ? resource.depthOrLastArray + 1u : 1u;
if (!planes) {
@@ -1324,6 +1660,26 @@ bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t addre
return address + bytes - htile >= HtileMetadataBytes(extent);
}
bool PlaneFillCovers(std::uint64_t plane, std::uint64_t planeBytes, std::uint64_t address, std::size_t bytes) {
return plane != 0 && plane >= address && plane + planeBytes <= address + bytes;
}
std::optional<std::uint8_t> StencilFillValue(std::uint32_t pattern) {
const auto byte = static_cast<std::uint8_t>(pattern & 0xffu);
if (pattern != byte * 0x01010101u) return std::nullopt;
return byte;
}
std::optional<float> DepthFillValue(std::uint32_t pattern, bool d16) {
if (d16) {
if ((pattern >> 16u) != (pattern & 0xffffu)) return std::nullopt;
return static_cast<float>(pattern & 0xffffu) / 65535.0f;
}
const auto value = std::bit_cast<float>(pattern);
if (!(value >= 0.0f && value <= 1.0f)) return std::nullopt;
return value;
}
void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) {
std::lock_guard lock(surfacesMutex());
forgetHtileReads(address, bytes);
@@ -1331,9 +1687,20 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32
if (!surface->active->load(std::memory_order_acquire)) continue;
const auto& target = surface->target;
if (target.samples != 1u && target.htileAddress != 0u && address < target.htileAddress + HtileMetadataBytes(target.extent) && target.htileAddress < address + bytes) surface->sampling.refusal = "native multisample depth metadata fill transition is not implemented";
if (!HtileFillCovers(target.htileAddress, target.extent, address, bytes)) continue;
const VkImageAspectFlags written = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u);
surface->pendingClear = (surface->pendingClear & ~written) | HtileFillClears(pattern, target.htileStencil);
if (HtileFillCovers(target.htileAddress, target.extent, address, bytes)) {
const VkImageAspectFlags written = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u);
surface->ForgetClear(written);
surface->NoteHtileClear(HtileFillClears(pattern, target.htileStencil));
continue;
}
if (target.samples != 1) continue;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
if (PlaneFillCovers(target.address, DepthSliceBytes(target.extent, d16 ? 2u : 4u), address, bytes)) {
if (const auto depth = DepthFillValue(pattern, d16)) surface->NotePlaneFill(VK_IMAGE_ASPECT_DEPTH_BIT, *depth, 0);
}
if (PlaneFillCovers(target.stencilAddress, DepthSliceBytes(target.extent, 1u), address, bytes)) {
if (const auto stencil = StencilFillValue(pattern)) surface->NotePlaneFill(VK_IMAGE_ASPECT_STENCIL_BIT, 0.0f, *stencil);
}
}
}
@@ -1369,7 +1736,7 @@ std::shared_ptr<StorageTexture> layeredDepthStorage(const Context& context, Dept
const auto layers = resource.depthOrLastArray + 1u;
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
const auto bytesPerTexel = d16 ? 2u : 4u;
surface.RequireStorable(resource, mip);
surface.RequireStorable(resource);
const auto geometry = DescribeSurface(resource);
if (layers > 1 && geometry.GuestLayerOffset(1) != DepthSliceBytes(target.extent, bytesPerTexel)) {
char text[256];
@@ -1388,6 +1755,7 @@ std::shared_ptr<StorageTexture> layeredDepthStorage(const Context& context, Dept
}
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip) {
if (mip != 0 || resource.baseLevel != 0) return nullptr;
std::lock_guard lock(surfacesMutex());
const auto& list = surfaces();
const auto found = std::find_if(list.rbegin(), list.rend(), [&](const auto& surface) {
@@ -1424,6 +1792,11 @@ bool DepthSurfaceAt(std::uint64_t address) {
return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && !surface->retired && (surface->target.address == address || surface->target.stencilAddress == address); });
}
bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes) {
std::lock_guard lock(surfacesMutex());
return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && !surface->retired && surface->context.device == device && surface->Overlaps(address, bytes); });
}
void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes) {
std::lock_guard lock(surfacesMutex());
for (const auto& surface : surfaces()) {
@@ -258,7 +258,7 @@ void recordCmaskClear(const Context& context, Recorder& recorder, const HostImpo
recorder.FlushKeyStoresOverlapping(address, bytes);
const auto commands = recorder.Commands();
recorder.Keep(resident);
context.detiler->DispatchCmaskClear(commands, import.buffer, address - import.base, bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0);
context.detiler->DispatchCmaskClear(commands, import.buffer, HostImportOffset(import, address), bytes, view, color.extent.width, color.extent.height, color.elementBytes, color.clearWords, color.dccAddress == 0);
recorder.NotePendingWrite(address, bytes);
proveCmask(address, bytes, GuestMemory::MarkWritten(address, bytes));
}
@@ -289,7 +289,7 @@ bool materializeCmaskClear(const Context& context, const ColorTarget& color, con
Require(color.dccAddress == 0, "CMASK fast clear of a DCC color target, whose clear belongs in its DCC keys");
const auto texel = clearTexel(color, DccKeys::ClearRegister);
const char* refusal = nullptr;
const bool filled = state == CmaskState::Cleared && resident != nullptr && clearToTexel(*resident, texel, color.elementBytes, refusal);
const bool filled = state == CmaskState::Cleared && color.mipCount == 1 && resident != nullptr && clearToTexel(*resident, texel, color.elementBytes, refusal);
if (!filled) {
storeCmaskClearTexels(color, layout, state == CmaskState::Cleared ? nullptr : &cleared, texel);
if (resident != nullptr) resident->Refresh();
@@ -353,6 +353,9 @@ struct DrawOutcome {
// A recorded draw began a render pass of its own, or continued the previous draw's.
bool passBegun = false;
bool passContinued = false;
bool keyStoresFlushed = false;
bool storesFlushed = false;
bool addressDraw = false;
SyncReason reason = SyncNone;
// Shader validation memo (see CachedFragmentOutputs).
bool validateMemoized = false;
@@ -425,6 +428,7 @@ struct DrawProfile {
// Render passes recorded draws began, and draws that continued the previous draw's pass.
std::uint64_t passesBegun = 0;
std::uint64_t passesContinued = 0;
std::array<DrawPassCounts, 2> passTotals{};
std::array<std::uint64_t, SyncCount> reasons{};
// The CPU inside Graphics::Draw, split by outcome: a synchronous draw costs ten times a recorded
// one, so one average would only show the synchronous population.
@@ -495,6 +499,14 @@ void reportDraw(const std::array<double, PhaseCount>& us, const ShaderResources:
if (outcome.completion) ++profile.completion;
if (outcome.passBegun) ++profile.passesBegun;
if (outcome.passContinued) ++profile.passesContinued;
if (outcome.recorded) {
auto& totals = profile.passTotals[outcome.addressDraw ? 1 : 0];
++totals.draws;
if (outcome.passBegun) ++totals.passesBegun;
if (outcome.passContinued) ++totals.passesContinued;
if (outcome.keyStoresFlushed) ++totals.keyStoreFlushes;
if (outcome.storesFlushed) ++totals.storeFlushes;
}
++profile.reasons[outcome.reason];
(outcome.recorded ? (outcome.waited ? profile.waitedUs : profile.recordedUs) : profile.synchronousUs) += drawUs;
profile.targetLookups += outcome.targetLookups;
@@ -646,6 +658,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(context.shaderClipDistance);
add(context.shaderCullDistance);
add(context.viewportIndexLayer);
add(context.shaderTerminateInvocation);
add(state.paClVsOutCntl);
add(state.stages.mesh.has_value());
if (state.stages.mesh) {
@@ -658,6 +671,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(mesh.threadsPerGroup);
add(mesh.ldsSizeDwords);
add(mesh.provokingVertex);
add(mesh.outputPrimitiveSize);
}
add(state.stages.tessellation.has_value());
if (state.stages.tessellation) {
@@ -701,7 +715,7 @@ std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<
}
std::set<std::uint32_t> outputs;
try {
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer, context.runtimeDescriptorArray, context.demoteToHelperInvocation);
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics, context.geometryShader, context.sampleRateShading, context.bufferInt64Atomics, context.shaderClipDistance, context.shaderCullDistance, context.viewportIndexLayer, context.runtimeDescriptorArray, context.demoteToHelperInvocation, context.shaderTerminateInvocation);
} catch (const std::exception& error) {
if (keyed) {
std::lock_guard lock(validationMutex());
@@ -832,6 +846,18 @@ void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten) {
stats.indirectReadUs += readMs * 1000.0;
}
std::array<std::uint64_t, IndirectPathCount> IndirectDrawCounts() {
auto& stats = Profile();
std::lock_guard lock(stats.mutex);
return stats.indirect;
}
std::array<DrawPassCounts, 2> DrawPassTotals() {
auto& stats = Profile();
std::lock_guard lock(stats.mutex);
return stats.passTotals;
}
void CountDrawSkip(DrawSkip kind, double us) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
if (!profile) return;
@@ -925,18 +951,26 @@ void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCop
recorder->KeepDrawSnapshot(address, copy.buffer->Bytes().size(), copy.generation, copy.registryGeneration, copy.buffer, use, derived);
}
const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment) {
const HostImport* InPlaceDrawInput(const Context& context, std::uint64_t address, std::size_t bytes, std::size_t alignment, bool* behindGpuWrite) {
auto* recorder = context.recorder;
if (recorder == nullptr || bytes == 0) return nullptr;
GuestMemory::FlushGpuWrites(address, bytes);
if (recorder->PendingWriteOverlaps(address, bytes) || recorder->PendingLabelIn(address, bytes) || recorder->QueuedStoreOverlaps(address, bytes)) return nullptr;
bool behind = false;
if (behindGpuWrite == nullptr) {
GuestMemory::FlushGpuWrites(address, bytes);
if (recorder->PendingWriteOverlaps(address, bytes)) return nullptr;
} else {
StorageTexture::FlushPending(address, bytes, nullptr, "in-place vertex input");
behind = recorder->PendingWriteOverlaps(address, bytes);
}
if (recorder->PendingLabelIn(address, bytes) || recorder->QueuedStoreOverlaps(address, bytes)) return nullptr;
const auto overlaps = [&](const auto& writer) { return writer->WritesOverlap(address, bytes); };
if (context.copiedWriters != nullptr && std::any_of(context.copiedWriters->begin(), context.copiedWriters->end(), overlaps)) return nullptr;
const auto drawWriters = DrawCopiedWriters();
if (std::any_of(drawWriters->begin(), drawWriters->end(), overlaps)) return nullptr;
const auto* import = HostImportFor(context, address, bytes);
if (import == nullptr || (address - import->base) % alignment != 0) return nullptr;
if (import == nullptr || HostImportOffset(*import, address) % alignment != 0) return nullptr;
recorder->NotePendingRead(address, bytes, Recorder::ReadKind::DrawInput);
if (behindGpuWrite != nullptr && behind) *behindGpuWrite = true;
return import;
}
@@ -978,6 +1012,7 @@ struct DrawInputs {
std::vector<VkBuffer> vertexHandles;
std::vector<VkDeviceSize> vertexOffsets;
std::vector<std::pair<std::uint64_t, std::uint64_t>> inPlaceReads;
bool vertexInputBehindGpuWrite = false;
std::set<std::uint32_t> fragmentOutputs;
VkPipelineStageFlags shaderStages = 0;
std::uint32_t meshGroups = 0;
@@ -1023,7 +1058,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
inputs.nothing = true;
return inputs;
}
} else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation && !state.rectList, "indirect draw on a non-vertex path must be resolved by the driver");
} else Require((!state.stages.mesh || draw.indexed) && !state.stages.tessellation, "indirect draw on a non-vertex path must be resolved by the driver");
inputs.indexBytes = static_cast<std::uint64_t>(draw.indexCount) * draw.indexSize;
const auto indexBytes = inputs.indexBytes;
APS5_LOG_OUT_DEBUG("Index buffer bytes=%llu", static_cast<unsigned long long>(indexBytes));
@@ -1031,6 +1066,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
if (draw.indexed) GuestMemory::CheckRange(reinterpret_cast<const void*>(draw.indexAddress), static_cast<std::size_t>(indexBytes), draw.indexSize);
APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK");
Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target");
if (draw.indexed) RequireNoMultisampleTarget(draw.indexAddress, static_cast<std::size_t>(indexBytes), "an index buffer read");
APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders");
if (recipe != nullptr) {
inputs.fragmentOutputs = recipe->fragmentOutputs;
@@ -1114,7 +1150,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
}
if (import != nullptr) {
inputs.indexHandle = import->buffer;
inputs.indexOffset = draw.indexAddress - import->base;
inputs.indexOffset = HostImportOffset(*import, draw.indexAddress);
inputs.inPlaceReads.emplace_back(draw.indexAddress, draw.indexAddress + indexBytes);
} else {
inputs.indices = std::move(copy.buffer);
@@ -1180,9 +1216,10 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
const auto [begin, end] = plan.copies[range];
const auto bytes = static_cast<std::size_t>(end - begin);
GuestMemory::CheckRange(reinterpret_cast<const void*>(begin), bytes, 1);
if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range])) {
RequireNoMultisampleTarget(begin, bytes, "a vertex buffer read");
if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range], &inputs.vertexInputBehindGpuWrite)) {
rangeHandles.push_back(import->buffer);
rangeOffsets.push_back(begin - import->base);
rangeOffsets.push_back(HostImportOffset(*import, begin));
inputs.inPlaceReads.emplace_back(begin, end);
continue;
}
@@ -1348,8 +1385,8 @@ void recordDrawCommands(const Context& context, VkCommandBuffer commands, const
else context.Resolved(&DeviceFunctions::cmdDraw, "vkCmdDraw")(commands, draw.indexCount, draw.instanceCount, draw.firstVertex, draw.firstInstance);
} else if (indirect->path == IndirectDrawPath::Gpu) {
if (args->countIndirect) {
if (draw.indexed) context.Function<PFN_vkCmdDrawIndexedIndirectCountKHR>("vkCmdDrawIndexedIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, args->countAddress - indirect->countImport->base, args->count, args->stride);
else context.Function<PFN_vkCmdDrawIndirectCountKHR>("vkCmdDrawIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, args->countAddress - indirect->countImport->base, args->count, args->stride);
if (draw.indexed) context.Function<PFN_vkCmdDrawIndexedIndirectCountKHR>("vkCmdDrawIndexedIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, HostImportOffset(*indirect->countImport, args->countAddress), args->count, args->stride);
else context.Function<PFN_vkCmdDrawIndirectCountKHR>("vkCmdDrawIndirectCountKHR")(commands, argumentBuffer, argumentOffset, indirect->countImport->buffer, HostImportOffset(*indirect->countImport, args->countAddress), args->count, args->stride);
} else if (args->count <= 1 || context.multiDrawIndirect) {
if (draw.indexed) context.Resolved(&DeviceFunctions::cmdDrawIndexedIndirect, "vkCmdDrawIndexedIndirect")(commands, argumentBuffer, argumentOffset, args->count, args->stride);
else context.Resolved(&DeviceFunctions::cmdDrawIndirect, "vkCmdDrawIndirect")(commands, argumentBuffer, argumentOffset, args->count, args->stride);
@@ -1397,7 +1434,7 @@ bool recordIndirectArguments(const Context& context, VkCommandBuffer commands, R
if (args->countIndirect) recorder->NotePendingRead(args->countAddress, 4, Recorder::ReadKind::Indirect);
}
argumentBuffer = indirect.argumentImport->buffer;
argumentOffset = args->arguments - indirect.argumentImport->base;
argumentOffset = HostImportOffset(*indirect.argumentImport, args->arguments);
const bool rewritten = args->vertexRule == Rule::Constant || args->instanceRule == Rule::Constant;
if (!rewritten) return false;
const auto bytes = static_cast<std::size_t>(args->RangeBytes());
@@ -1537,6 +1574,10 @@ void keepRecordedDraw(Recorder& recorder, const std::shared_ptr<ShaderResources>
if (index < slices.size()) residents[index]->MarkSliceDirty(slices[index]);
else residents[index]->MarkDirty();
}
for (const auto& resident : residents) {
if (resident == nullptr || resident->Descriptor().tileMode != TextureTileMode::kLinear) continue;
StorageTexture::FlushPending(resident->Descriptor().baseAddress, static_cast<std::size_t>(resident->GuestBytes()), nullptr, "linear target");
}
}
// What recordDraw records from: the recorder, the resources, the pipeline and framebuffer, the
@@ -1676,7 +1717,7 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c
}
const auto* imported = HostImportFor(context, begin, bytes);
Require(imported != nullptr, "capture input has no host import");
addSample(begin, bytes, imported->buffer, begin - imported->base, reinterpret_cast<const std::byte*>(begin));
addSample(begin, bytes, imported->buffer, HostImportOffset(*imported, begin), reinterpret_cast<const std::byte*>(begin));
}
CaptureTrace::Log("input-capture draw=%llu batch=%llu target=%llx ranges=%zu bytes=%zu", draw, batch, static_cast<unsigned long long>(target), samples.size(), total);
if (samples.empty()) return;
@@ -1730,9 +1771,15 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
if (!gpuIndirect) return false;
return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end);
};
if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores();
if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) {
recorder->FlushKeyStores();
outcome.keyStoresFlushed = true;
}
// A queued label store over such memory likewise (Recorder::RecordStore).
if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores();
if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) {
recorder->FlushStores();
outcome.storesFlushed = true;
}
const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved);
const bool capture = CaptureInputsEnabled();
const bool meshIndirect = state.stages.mesh && args != nullptr;
@@ -1749,7 +1796,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
if (target != nullptr) passAttachments.emplace_back(target->Image(), true);
}
const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites();
const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownMemory};
static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr;
const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly();
outcome.addressDraw = addressBased;
const bool unknownWrites = ((addressBased || resources.UsesAddressTable()) && !readOnlyBda) || resources.HasCopiedWrites();
const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites};
const bool fenced = state.depth.has_value() || !record.proxies.empty();
const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess);
const bool continued = start.continued;
@@ -1793,6 +1844,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
countBarrier(1);
}
APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded");
if (inputs.vertexInputBehindGpuWrite) {
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT);
countBarrier(1);
}
if (meshIndirect) {
meshArguments = recordMeshArguments(context, commands, recorder, true, state, draw, *record.indirect, countBarrier, inputs.restartTable);
argumentBuffer = meshArguments->Handle();
@@ -1989,7 +2044,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
timer.phase(PhaseSetup);
// Debug aid: APS5_NO_RESIDENT_TARGETS=1 copies every target in and out again.
static const bool residentTargets = std::getenv("APS5_NO_RESIDENT_TARGETS") == nullptr;
if ((binding.gpuTiling || (color.tileMode != ColorTileMode::Linear && context.detiler != nullptr)) && residentTargets) {
static const bool residentLinear = std::getenv("APS5_NO_RESIDENT_LINEAR") == nullptr;
const bool linearResident = residentLinear && color.tileMode == ColorTileMode::Linear && ExistingHostImportFor(context, color.address, colorLayout.Bytes()) != nullptr;
if ((binding.gpuTiling || ((color.tileMode != ColorTileMode::Linear || linearResident) && context.detiler != nullptr)) && residentTargets) {
// The lookup refreshes the image on every draw (StorageTexture::Refresh: FlushPending,
// CollectWrites over the target's pages, the DCC key scan of TextureClearKeys, then
// UnchangedSince). The page walk is skipped while the worker's collect epoch lasts
@@ -2078,6 +2135,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (binding.role != ShaderRecompiler::DescriptorRole::GuestImages && binding.role != ShaderRecompiler::DescriptorRole::ImageTable) continue;
for (std::size_t offset = 0; offset + 8u <= binding.guestDescriptor.size(); offset += 8u) {
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(offset, 8u);
if (words[0] == 0u && (words[1] & 0xffu) == 0u) continue;
const auto texture = DecodeTextureResource(words);
Require(!DepthSurfaceAt(context, words, texture), "stencil replay refuses sampled depth-surface feedback");
}
@@ -2347,6 +2405,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
countBarrier();
}
APS5_LOG_CHARS_OUT_DEBUG("Upload barrier recorded");
if (inputs.vertexInputBehindGpuWrite) {
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT);
countBarrier();
}
std::shared_ptr<Buffer> meshArguments;
if (state.stages.mesh && args != nullptr) {
meshArguments = recordMeshArguments(context, commands, recorder, recorded, state, draw, indirect, countBarrier, inputs.restartTable);
File diff suppressed because it is too large. Load diff
@@ -116,7 +116,6 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words,
Require(mipFilter != 2u || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with a linear mip filter, which is not implemented");
const auto aniso = isAnisoFilter(xyMagFilter) || isAnisoFilter(xyMinFilter);
Require(!aniso || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with anisotropic filtering, which is not implemented");
auto anisoRatio = 1.0f;
if (aniso) {
if (maxAnisoRatio > 4u) Require(false, "guest sampler descriptor uses an unknown anisotropy ratio " + std::to_string(maxAnisoRatio));
@@ -55,7 +55,7 @@ bool IsNullTextureResource(std::span<const std::uint32_t> words) {
Require(words.size() == 8, "guest texture descriptor must contain 8 dwords");
const auto base40 = (static_cast<std::uint64_t>(words[0]) | (static_cast<std::uint64_t>(words[1]) << 32u)) & 0xffffffffffull;
const auto type = (words[3] >> 28u) & 0xfu;
return base40 == 0 || type == 0;
return base40 == 0 || type <= 1;
}
GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words) {
@@ -53,55 +53,35 @@ class MultisampleTarget {
public:
MultisampleTarget(const Context& context, const ColorTarget& target) : context(context), target(target), cmask(target.cmaskAddress, target.cmaskBytes), colorFill(target) {
this->context.bufferPool.reset();
if (target.dccAddress != 0) metadataValid = std::make_shared<std::atomic<bool>>(true);
const auto initial = initialDccClear(target);
const auto samples = static_cast<VkSampleCountFlagBits>(target.samples);
Require((context.limits.framebufferColorSampleCounts & samples) != 0, "the device cannot render color with the target's sample count");
VkFormatProperties properties{};
context.formatProperties(context.physical, target.format, &properties);
Require((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT) != 0, "multisampled render-target format cannot be a color attachment");
VkImageUsageFlags usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
usage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
VkImageFormatProperties supported{};
sampleable = (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0 && context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage | VK_IMAGE_USAGE_SAMPLED_BIT, 0, &supported) == VK_SUCCESS && (supported.sampleCounts & samples) != 0;
if (sampleable) usage |= VK_IMAGE_USAGE_SAMPLED_BIT;
Check(context.imageFormatProperties(context.physical, target.format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, usage, 0, &supported), "vkGetPhysicalDeviceImageFormatProperties multisampled target");
Require((supported.sampleCounts & samples) != 0 && target.extent.width <= supported.maxExtent.width && target.extent.height <= supported.maxExtent.height, "multisampled render target exceeds device image limits");
storageFormat = HostStorageFormat(context, target.format);
storageFlags = storageFormat != target.format ? VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT | VK_IMAGE_CREATE_EXTENDED_USAGE_BIT : 0u;
const auto storageSupports = [&](VkFormat format, VkImageUsageFlags formatUsage, VkImageCreateFlags flags) {
VkImageFormatProperties properties{};
return context.imageFormatProperties(context.physical, format, VK_IMAGE_TYPE_2D, VK_IMAGE_TILING_OPTIMAL, formatUsage, flags, &properties) == VK_SUCCESS && (properties.sampleCounts & samples) != 0;
};
if (storageFormat != VK_FORMAT_UNDEFINED && (!context.storageImageMultisample || !storageSupports(storageFormat, VK_IMAGE_USAGE_STORAGE_BIT, 0u) || !storageSupports(target.format, usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags))) storageFormat = VK_FORMAT_UNDEFINED;
try {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.imageType = VK_IMAGE_TYPE_2D;
info.format = target.format;
info.extent = {target.extent.width, target.extent.height, 1};
info.mipLevels = 1;
info.arrayLayers = 1;
info.samples = samples;
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = usage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &image), "vkCreateImage multisampled target");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, image, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory multisampled target");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory multisampled target");
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = target.format;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView multisampled target");
const auto created = allocate(usage, 0u);
image = created.image;
memory = created.memory;
view = created.view;
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toGeneral.image = image;
toGeneral.subresourceRange = viewInfo.subresourceRange;
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
const VkClearColorValue zero{};
context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &toGeneral.subresourceRange);
recordToGeneral(commands, image);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
});
@@ -114,7 +94,90 @@ public:
MultisampleTarget(const MultisampleTarget&) = delete;
MultisampleTarget& operator=(const MultisampleTarget&) = delete;
struct Allocation {
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
};
Allocation allocate(VkImageUsageFlags imageUsage, VkImageCreateFlags flags) {
Allocation created;
try {
VkImageCreateInfo info{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
info.flags = flags;
info.imageType = VK_IMAGE_TYPE_2D;
info.format = target.format;
info.extent = {target.extent.width, target.extent.height, 1};
info.mipLevels = 1;
info.arrayLayers = 1;
info.samples = static_cast<VkSampleCountFlagBits>(target.samples);
info.tiling = VK_IMAGE_TILING_OPTIMAL;
info.usage = imageUsage;
info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
Check(context.Function<PFN_vkCreateImage>("vkCreateImage")(context.device, &info, nullptr, &created.image), "vkCreateImage multisampled target");
VkMemoryRequirements requirements{};
context.Function<PFN_vkGetImageMemoryRequirements>("vkGetImageMemoryRequirements")(context.device, created.image, &requirements);
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
allocation.allocationSize = requirements.size;
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &created.memory), "vkAllocateMemory multisampled target");
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, created.image, created.memory, 0), "vkBindImageMemory multisampled target");
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = created.image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = target.format;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &created.view), "vkCreateImageView multisampled target");
} catch (...) {
destroy(created);
throw;
}
return created;
}
void destroy(const Allocation& allocation) noexcept {
if (allocation.view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, allocation.view, nullptr);
if (allocation.image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, allocation.image, nullptr);
if (allocation.memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, allocation.memory, nullptr);
}
void recordToGeneral(VkCommandBuffer commands, VkImage destination) {
VkImageMemoryBarrier toGeneral{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
toGeneral.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
toGeneral.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
toGeneral.newLayout = VK_IMAGE_LAYOUT_GENERAL;
toGeneral.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toGeneral.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
toGeneral.image = destination;
toGeneral.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &toGeneral);
}
void makeStorable(Recorder& recorder) {
if ((usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0) return;
const auto storable = allocate(usage | VK_IMAGE_USAGE_STORAGE_BIT, storageFlags);
const auto source = image;
recordToGeneral(recorder.Commands(), storable.image);
RecordMemoryBarrier(context, recorder.Commands(), VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT);
VkImageCopy region{};
region.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1};
region.dstSubresource = region.srcSubresource;
region.extent = {target.extent.width, target.extent.height, 1};
context.Function<PFN_vkCmdCopyImage>("vkCmdCopyImage")(recorder.Commands(), source, VK_IMAGE_LAYOUT_GENERAL, storable.image, VK_IMAGE_LAYOUT_GENERAL, 1, &region);
RecordMemoryBarrier(context, recorder.Commands(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
Recorder::CountBarriers(Recorder::CommandClass::Draw, 3);
for (const auto& [key, texture] : textures) recorder.Keep(texture);
textures.clear();
retired.push_back({image, memory, view});
image = storable.image;
memory = storable.memory;
view = storable.view;
usage |= VK_IMAGE_USAGE_STORAGE_BIT;
}
void ApplyFastClear(const ColorTarget& use) {
checkDccCurrent(use);
VkClearColorValue clear{};
if (!TakeMultisampledClear(cmask, colorFill, use, clear)) return;
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
@@ -126,11 +189,50 @@ public:
});
}
void SettleStorageWrites() {
if (!storageWritten) return;
storageWritten = false;
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT);
if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 1);
});
}
VkImageView Storage(VkFormat format) {
Require(storageFormat != VK_FORMAT_UNDEFINED, "the device cannot write the multisampled color target's format as a storage image at its sample count");
Require(format == storageFormat, "a storage image access to a multisampled color target in another format than the target's storage form is not implemented");
Require(cmask.Refusal().empty(), cmask.Refusal());
Require(cmask.Keys() != DccKeys::Clear0000, "a storage image access to a multisampled color target whose CMASK fast clear no draw has applied is not implemented");
auto* recording = Recorder::Active();
Require(recording != nullptr, "a multisampled color target is only written by recorded dispatches");
makeStorable(*recording);
ApplyFastClear(target);
if (storageView == VK_NULL_HANDLE) {
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = storageFormat;
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &storageView), "vkCreateImageView multisampled target storage");
}
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 1);
});
storageWritten = true;
return storageView;
}
bool Storable() const {
return (usage & VK_IMAGE_USAGE_STORAGE_BIT) != 0;
}
std::shared_ptr<Texture> Sampled(VkComponentMapping components) {
Require(sampleable, "the device cannot sample the multisampled color target's format at its sample count");
Require(cmask.Refusal().empty(), cmask.Refusal());
Require(cmask.Keys() != DccKeys::Clear0000, "sampling a multisampled color target whose CMASK fast clear no draw has applied is not implemented");
Require(Recorder::Active() != nullptr, "a multisampled color target is only sampled by recorded draws and dispatches");
SettleStorageWrites();
ApplyFastClear(target);
recordCommands(context, [&](VkCommandBuffer commands, Recorder*) {
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT);
@@ -178,6 +280,7 @@ public:
Context context;
std::shared_ptr<std::atomic<bool>> active = std::make_shared<std::atomic<bool>>(true);
std::shared_ptr<std::atomic<bool>> metadataValid;
ColorTarget target;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
@@ -185,17 +288,73 @@ public:
VkRenderPass resolvePass = VK_NULL_HANDLE;
MultisampledCmask cmask;
MultisampledColorFill colorFill;
bool fromTexture = false;
private:
bool sampleable = false;
VkImageUsageFlags usage = 0;
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
VkImageCreateFlags storageFlags = 0;
std::vector<Allocation> retired;
VkImageView storageView = VK_NULL_HANDLE;
bool storageWritten = false;
std::map<std::array<std::uint32_t, 4>, std::shared_ptr<Texture>> textures;
std::vector<GuestMemory::UnchangedQuery> dccRanges;
VkClearColorValue initialDccClear(const ColorTarget& use) {
if (use.dccAddress == 0) return {};
const auto dccBytes = MultisampledDccBytes(use);
Require((use.dccAddress & 4095u) == 0, "multisampled DCC metadata base alignment is not implemented");
Require(use.cmaskAddress == 0 ? use.cmaskBytes == 0 : use.cmaskBytes == CmaskBytes(use.extent.width, use.extent.height), "multisampled DCC CMASK footprint is invalid");
const auto colorBytes = MultisampledColorBytes(use);
Require(colorBytes != 0, "multisampled DCC color footprint is not implemented");
dccRanges.push_back({use.address, static_cast<std::size_t>(colorBytes), 0});
dccRanges.push_back({use.dccAddress, dccBytes, 0});
if (use.cmaskAddress != 0) dccRanges.push_back({use.cmaskAddress, use.cmaskBytes, 0});
for (auto& range : dccRanges) {
GuestMemory::CheckRange(reinterpret_cast<const void*>(range.address), range.bytes, 256);
Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory");
range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes);
Require(range.generation != 0, "initial multisampled DCC contents have no write generation");
}
for (std::size_t first = 0; first < dccRanges.size(); ++first) {
for (std::size_t second = first + 1; second < dccRanges.size(); ++second) {
const auto& a = dccRanges[first];
const auto& b = dccRanges[second];
Require(a.address >= b.address + b.bytes || b.address >= a.address + a.bytes, "overlapping multisampled DCC color or metadata ranges are not implemented");
}
}
std::vector<std::byte> dcc(dccRanges[1].bytes);
GuestMemory::Read(use.dccAddress, dcc);
std::vector<std::byte> cmask(use.cmaskBytes);
if (!cmask.empty()) GuestMemory::Read(use.cmaskAddress, cmask);
const auto clear = MultisampledDccClear(use, dcc, cmask);
Require(GuestMemory::UnchangedSinceAll(dccRanges), "initial multisampled DCC contents changed during import");
return clear;
}
void checkDccCurrent(const ColorTarget& use) const {
if (dccRanges.empty()) {
Require(use.dccAddress == 0, "adding DCC to a resident multisampled color target is not implemented");
return;
}
Require(metadataValid->load(std::memory_order_acquire), "multisampled DCC metadata was released while its color target remains resident");
static_cast<void>(MultisampledDccBytes(use));
Require(use.address == target.address && use.dccAddress == target.dccAddress && use.dccControl == target.dccControl && use.dccAlphaOnMsb == target.dccAlphaOnMsb && use.dccPipeAligned == target.dccPipeAligned && use.cmaskAddress == target.cmaskAddress && use.cmaskBytes == target.cmaskBytes && use.tileMode == target.tileMode && use.elementBytes == target.elementBytes && use.pipeBankXor == target.pipeBankXor, "multisampled DCC target layout or metadata changed");
for (const auto& range : dccRanges) {
Require(GuestMemory::Watched(range.address, range.bytes), "multisampled DCC target reuse requires watched guest memory");
GuestMemory::CollectWritesUncached(range.address, range.bytes);
Require(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented");
if (auto* recorder = Recorder::Active(); recorder != nullptr) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "pending writes to a resident multisampled DCC target or its metadata are not implemented");
}
}
void release() noexcept {
textures.clear();
if (resolvePass) context.Function<PFN_vkDestroyRenderPass>("vkDestroyRenderPass")(context.device, resolvePass, nullptr);
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
if (storageView) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, storageView, nullptr);
destroy({image, memory, view});
for (const auto& allocation : retired) destroy(allocation);
}
};
@@ -205,6 +364,7 @@ struct GuardedRange {
std::uint64_t bytes;
std::uint32_t samples;
std::shared_ptr<std::atomic<bool>> active;
std::shared_ptr<std::atomic<bool>> metadataValid;
};
class PrimedSharedMutex : public std::shared_mutex {
@@ -238,14 +398,16 @@ void retireUnmapped(std::uintptr_t address, std::size_t bytes) {
RetireUnmappedDepth_nid_no_patch(address, bytes);
std::unique_lock lock(guardedMutex());
std::erase_if(guarded(), [&](const auto& range) {
if (range.metadataValid && address < range.address + range.bytes && range.address < address + bytes) range.metadataValid->store(false, std::memory_order_release);
if (range.address < address || range.address - address > bytes || range.bytes > bytes - (range.address - address)) return false;
range.active->store(false, std::memory_order_release);
if (!range.metadataValid) range.active->store(false, std::memory_order_release);
return true;
});
std::erase_if(guarded(), [](const auto& range) { return range.metadataValid && !range.active->load(std::memory_order_acquire); });
anyGuarded().store(!guarded().empty(), std::memory_order_release);
}
void guard(const Context& context, const ColorTarget& target, const std::shared_ptr<std::atomic<bool>>& active) {
void guard(const Context& context, const ColorTarget& target, const std::shared_ptr<std::atomic<bool>>& active, const std::shared_ptr<std::atomic<bool>>& metadataValid) {
auto bytes = std::max<std::uint64_t>(target.bytes * target.samples, static_cast<std::uint64_t>(target.extent.width) * target.extent.height * target.elementBytes * target.samples);
if (target.tileMode == ColorTileMode::RenderTarget && std::has_single_bit(target.elementBytes) && std::has_single_bit(target.samples)) {
const auto log2Samples = std::countr_zero(target.samples);
@@ -258,6 +420,10 @@ void guard(const Context& context, const ColorTarget& target, const std::shared_
}
std::unique_lock lock(guardedMutex());
guarded().push_back({context.device, target.address, bytes, target.samples, active});
if (target.dccAddress != 0) {
guarded().push_back({context.device, target.dccAddress, MultisampledDccBytes(target), target.samples, active, metadataValid});
if (target.cmaskAddress != 0) guarded().push_back({context.device, target.cmaskAddress, target.cmaskBytes, target.samples, active, metadataValid});
}
anyGuarded().store(true, std::memory_order_release);
InstallResidentImageReadGuard_nid_no_patch();
}
@@ -276,22 +442,27 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
for (const auto& target : targets()) {
if (!target->active->load(std::memory_order_acquire)) continue;
if (target->context.device != context.device || !sameTarget(target->target, use)) continue;
if (target->fromTexture && use.cmaskAddress != 0) {
target->fromTexture = false;
target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes);
}
target->cmask.CheckAddress(use.cmaskAddress);
target->SettleStorageWrites();
target->ApplyFastClear(use);
return *target;
}
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
auto& created = *targets().back();
guard(context, use, created.active);
if (use.cmaskFastClear) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast<std::uint64_t>(use.cmaskBytes) * 256u));
guard(context, use, created.active, created.metadataValid);
if (use.cmaskFastClear && use.dccAddress == 0) created.cmask.SeedKeys(CurrentDccKeys(use.cmaskAddress, static_cast<std::uint64_t>(use.cmaskBytes) * 256u));
created.ApplyFastClear(use);
return created;
}
MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format) {
MultisampleTarget& createFilled(const Context& context, const GuestTextureResource& resource, VkFormat format, const char* access) {
const auto refuse = [&](const std::string& reason) {
char text[160];
std::snprintf(text, sizeof(text), "AGC graphics: sampling the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", resource.width, resource.height, static_cast<unsigned long long>(resource.baseAddress), resource.samples);
char text[192];
std::snprintf(text, sizeof(text), "AGC graphics: %s the %ux%u multisampled texture 0x%llx of %u samples, which no draw rendered to and ", access, resource.width, resource.height, static_cast<unsigned long long>(resource.baseAddress), resource.samples);
throw std::runtime_error(text + reason + ", is not implemented");
};
ColorTarget use{};
@@ -332,8 +503,9 @@ MultisampleTarget& createFilled(const Context& context, const GuestTextureResour
if (!MultisampledFillClearColor(use, value, clear)) refuse(std::string("whose color memory ") + range + " holds " + held + ", which has no exact clear value in its format");
targets().push_back(std::make_unique<MultisampleTarget>(context, use));
auto& created = *targets().back();
guard(context, use, created.active);
guard(context, use, created.active, created.metadataValid);
created.colorFill.NoteFill(use.address, static_cast<std::size_t>(bytes), value);
created.fromTexture = true;
return created;
}
@@ -406,6 +578,45 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target) {
return ((target.extent.width + blockWidth - 1) / blockWidth) * ((target.extent.height + blockHeight - 1) / blockHeight) << blockLog2;
}
std::size_t MultisampledDccBytes(const ColorTarget& target) {
Require(target.samples == 2 || target.samples == 4 || target.samples == 8, "unsupported multisampled DCC fragment count");
Require(target.tileMode == ColorTileMode::RenderTarget && target.dccPipeAligned, "multisampled DCC requires pipe-aligned SW_64KB_R_X");
Require(std::has_single_bit(target.elementBytes) && target.elementBytes <= 16, "unsupported multisampled DCC element size");
Require(target.extent.width != 0 && target.extent.width <= 16384 && target.extent.height != 0 && target.extent.height <= 16384, "invalid multisampled DCC extent");
Require(target.mipCount == 1 && target.mip == 0 && !target.mipTail && target.depth == 1 && target.depthSlice == 0 && target.arrayLastSlice == 0 && target.arraySlice == 0 && target.layers == 1, "multisampled DCC array, volume or mip contents are not implemented");
const auto bits = 19u - static_cast<std::uint32_t>(std::countr_zero(target.elementBytes));
const std::uint64_t width = std::uint64_t{1} << ((bits + 1u) / 2u);
const std::uint64_t height = std::uint64_t{1} << (bits / 2u);
return static_cast<std::size_t>(((target.extent.width + width - 1) / width) * ((target.extent.height + height - 1) / height) * 4096u);
}
VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span<const std::byte> dcc, std::span<const std::byte> cmask) {
Require(target.dccAddress != 0 && (target.dccControl & 0x3ff80u) == 0 && (target.pipeBankXor & ~0xcf00u) == 0, "multisampled DCC address, color transform, lossy mode or XOR is not implemented");
Require(dcc.size() == MultisampledDccBytes(target), "multisampled DCC metadata snapshot size mismatch");
Require(std::ranges::all_of(dcc, [&](std::byte key) { return key == dcc.front(); }), "mixed multisampled DCC metadata is not implemented");
Require(target.cmaskAddress == 0 ? cmask.empty() && target.cmaskBytes == 0 : cmask.size() == target.cmaskBytes && target.cmaskBytes == CmaskBytes(target.extent.width, target.extent.height), "multisampled DCC CMASK snapshot size mismatch");
if (!cmask.empty()) {
const auto code = std::to_integer<std::uint32_t>(cmask.front());
const auto expanded = target.samples == 2 ? 0xddu : target.samples == 4 ? 0xeeu : 0xffu;
Require(std::ranges::all_of(cmask, [&](std::byte key) { return key == cmask.front(); }) && (code == 0xccu || code == expanded), "multisampled DCC CMASK compression or register clear is not implemented");
}
DccKeys keys;
switch (std::to_integer<std::uint32_t>(dcc.front())) {
case 0x00u: keys = DccKeys::Clear0000; break;
case 0x40u: keys = DccKeys::Clear0001; break;
case 0x80u: keys = DccKeys::Clear1110; break;
case 0xc0u: keys = DccKeys::Clear1111; break;
default: throw std::runtime_error("initial multisampled DCC contents are not a supported uniform fast clear");
}
std::array<std::byte, 16> texels{};
Require(FillDccClear(target.format, keys, target.dccAlphaOnMsb, texels), "multisampled DCC clear format is not implemented");
std::array<std::uint32_t, 4> pattern{};
std::memcpy(pattern.data(), texels.data(), texels.size());
VkClearColorValue clear{};
Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC fast clear has no exact host value");
return clear;
}
bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear) {
const std::array<std::uint32_t, 4> pattern{fill, fill, fill, fill};
VkFormat unorm = VK_FORMAT_UNDEFINED;
@@ -465,6 +676,25 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar
return acquire(context, target).view;
}
namespace {
bool redBlueSwapped(VkFormat rendered, VkFormat sampled) {
const auto pair = [&](VkFormat a, VkFormat b) { return (rendered == a && sampled == b) || (rendered == b && sampled == a); };
return pair(VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM) || pair(VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8A8_SRGB) || pair(VK_FORMAT_B8G8R8A8_SNORM, VK_FORMAT_R8G8B8A8_SNORM) || pair(VK_FORMAT_B8G8R8A8_UINT, VK_FORMAT_R8G8B8A8_UINT) || pair(VK_FORMAT_B8G8R8A8_SINT, VK_FORMAT_R8G8B8A8_SINT);
}
VkComponentMapping swapRedBlue(VkComponentMapping components) {
const auto swapped = [](VkComponentSwizzle swizzle, VkComponentSwizzle identity) {
if (swizzle == VK_COMPONENT_SWIZZLE_IDENTITY) swizzle = identity;
if (swizzle == VK_COMPONENT_SWIZZLE_R) return VK_COMPONENT_SWIZZLE_B;
if (swizzle == VK_COMPONENT_SWIZZLE_B) return VK_COMPONENT_SWIZZLE_R;
return swizzle;
};
return {swapped(components.r, VK_COMPONENT_SWIZZLE_R), swapped(components.g, VK_COMPONENT_SWIZZLE_G), swapped(components.b, VK_COMPONENT_SWIZZLE_B), swapped(components.a, VK_COMPONENT_SWIZZLE_A)};
}
}
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) {
std::lock_guard lock(targetsMutex());
const auto format = ResolveTextureFormat(resource.format);
@@ -473,12 +703,14 @@ std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const
if (!target->active->load(std::memory_order_acquire)) continue;
if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue;
const auto& rendered = target->target;
if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Sampled(components);
const bool sameShape = rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0;
if (sameShape && rendered.format == format) return target->Sampled(components);
if (sameShape && redBlueSwapped(rendered.format, format)) return target->Sampled(swapRedBlue(components));
other = target.get();
}
if (other == nullptr) {
Require(Recorder::Active() != nullptr, "a multisampled texture is only sampled by recorded draws and dispatches");
return createFilled(context, resource, format).Sampled(components);
return createFilled(context, resource, format, "sampling").Sampled(components);
}
char text[320];
std::snprintf(text, sizeof(text), "AGC graphics: sampling multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast<unsigned long long>(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast<int>(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast<int>(format));
@@ -490,6 +722,33 @@ void MaterializeMultisampleTargetClear(const Context& context, const ColorTarget
acquire(context, target);
}
VkImageView MultisampleTargetStorageView(const Context& context, const GuestTextureResource& resource) {
std::lock_guard lock(targetsMutex());
const auto format = ResolveTextureFormat(resource.format);
const MultisampleTarget* other = nullptr;
for (const auto& target : targets()) {
if (!target->active->load(std::memory_order_acquire)) continue;
if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue;
const auto& rendered = target->target;
if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Storage(StorageFormatForGuest(context, resource.format));
other = target.get();
}
if (other == nullptr) {
Require(Recorder::Active() != nullptr, "a multisampled color target is only written by recorded dispatches");
return createFilled(context, resource, format, "writing").Storage(StorageFormatForGuest(context, resource.format));
}
char text[320];
std::snprintf(text, sizeof(text), "AGC graphics: storage image access to multisampled color target 0x%llx (%ux%u, %u samples, vk format %d) as a %ux%u texture of %u samples and guest format %u (vk %d) is not implemented", static_cast<unsigned long long>(resource.baseAddress), other->target.extent.width, other->target.extent.height, other->target.samples, static_cast<int>(other->target.format), resource.width, resource.height, resource.samples, resource.format, static_cast<int>(format));
throw std::runtime_error(text);
}
bool MultisampleTargetStorable(const Context& context, const ColorTarget& target) {
std::lock_guard lock(targetsMutex());
return std::any_of(targets().begin(), targets().end(), [&](const auto& candidate) {
return candidate->active->load(std::memory_order_acquire) && candidate->context.device == context.device && sameTarget(candidate->target, target) && candidate->Storable();
});
}
void ResolveMultisampleTarget(const Context& context, const ColorTarget& source, StorageTexture& destination, VkImageView destinationView) {
std::lock_guard lock(targetsMutex());
auto& target = acquire(context, source);
Loaded 100 of 861 files, more files were not shown because too many files have changed in this diff. Show more