Merge remote-tracking branch 'origin/main' into fix/kernel-raise-exception-ntcontinue

Keeps this branch's CONTEXT_FULL | CONTEXT_SEGMENTS read, context-first
order and bounded retry in RaiseOn, and adds #1478's ymm save fields to
both deliveries. The test keeps both phase sets; the vectors phase runs
after the continuing, selfing and looping phases and counts their
deliveries. The TechnicalDebt entry about ymm upper halves is dropped,
since #1478 preserves them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
popcoder565 2026-10-09 16:04:14 -04:00
commit 570e7affdb
704 changed files with 33612 additions and 4324 deletions

View File

@ -4,13 +4,20 @@ on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
workflow_dispatch:
inputs:
pr:
description: Pull request number to build
required: true
type: string
permissions:
contents: read
concurrency:
group: build-${{ github.ref }}
group: build-${{ github.event_name }}-${{ github.event.pull_request.number || inputs.pr || github.ref }}
cancel-in-progress: true
env:
@ -20,29 +27,189 @@ env:
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
jobs:
eligibility:
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
pull-requests: read
outputs:
sha: ${{ steps.gate.outputs.sha }}
pr: ${{ steps.gate.outputs.pr }}
head: ${{ steps.gate.outputs.head }}
relinker: ${{ steps.gate.outputs.relinker }}
full: ${{ steps.gate.outputs.full }}
steps:
- uses: actions/github-script@v8
id: gate
env:
PR_NUMBER: ${{ inputs.pr }}
with:
script: |
if (context.eventName === 'push') {
if (context.ref !== 'refs/heads/main') {
throw new Error('Automatic full builds only run on main');
}
core.setOutput('sha', context.sha);
core.setOutput('pr', '');
core.setOutput('head', '');
core.setOutput('relinker', true);
core.setOutput('full', true);
await core.summary.addRaw(`Building main at ${context.sha}: all builds and tests enabled`).write();
return;
}
const { owner, repo } = context.repo;
const manual = context.eventName === 'workflow_dispatch';
if (manual) {
for (const username of new Set([context.actor, process.env.GITHUB_TRIGGERING_ACTOR])) {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username });
if (!['write', 'maintain', 'admin'].includes(data.permission)) {
throw new Error('Only collaborators with write access can start or rerun builds');
}
}
if (!/^[1-9][0-9]*$/.test(process.env.PR_NUMBER)) {
throw new Error('Enter a positive pull request number');
}
}
const pull_number = manual ? Number(process.env.PR_NUMBER) : context.payload.pull_request.number;
if (!Number.isSafeInteger(pull_number) || pull_number < 1) {
throw new Error('Invalid pull request number');
}
let { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
for (let attempt = 0; pr.mergeable === null && attempt < 10; ++attempt) {
await new Promise((resolve) => setTimeout(resolve, 3000));
({ data: pr } = await github.rest.pulls.get({ owner, repo, pull_number }));
}
if (pr.state !== 'open' || pr.base.ref !== 'main') {
throw new Error('The pull request must be open and target main');
}
if (!manual && pr.head.sha !== context.payload.pull_request.head.sha) {
throw new Error('The pull request has newer commits; use the latest run');
}
if (pr.mergeable === false) {
throw new Error('The pull request does not merge cleanly into main; rebase it first');
}
let relinker = manual;
if (!manual) {
const { data: commits } = await github.rest.repos.listCommits({
owner, repo, sha: 'main', author: pr.user.login, per_page: 1
});
relinker = commits.length > 0;
}
const { data: main } = await github.rest.repos.getBranch({ owner, repo, branch: 'main' });
core.setOutput('sha', main.commit.sha);
core.setOutput('pr', String(pull_number));
core.setOutput('head', pr.head.sha);
core.setOutput('relinker', relinker);
core.setOutput('full', manual);
core.info(`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}`);
await core.summary.addRaw(
`PR #${pull_number} at ${pr.head.sha} merged into main at ${main.commit.sha}: full builds ${manual ? 'enabled' : 'disabled'}, ` +
`relinker ${relinker ? 'enabled' : 'skipped because the author has no commit in main'}`
).write();
relinker:
needs: eligibility
if: ${{ needs.eligibility.outputs.relinker == 'true' }}
runs-on: ${{ matrix.os }}
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
compiler: g++
- os: windows-latest
compiler: g++
- os: macos-latest
compiler: clang++
env:
WINLIBS_RELEASE: 15.2.0posix-14.0.0-ucrt-r7
WINLIBS_ZIP: winlibs-x86_64-posix-seh-gcc-15.2.0-mingw-w64ucrt-14.0.0-r7.zip
defaults:
run:
shell: bash
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: |
git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head"
if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi
- if: runner.os == 'Windows'
id: toolchain
uses: actions/cache@v6
with:
path: C:\winlibs
key: ${{ env.WINLIBS_ZIP }}
- if: runner.os == 'Windows' && steps.toolchain.outputs.cache-hit != 'true'
run: |
curl -sSfL -o "$RUNNER_TEMP/winlibs.zip" "https://github.com/brechtsanders/winlibs_mingw/releases/download/$WINLIBS_RELEASE/$WINLIBS_ZIP"
7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /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
- run: cmake --build build-relinker --parallel 3
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120
linux:
needs: eligibility
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
submodules: true
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
shell: bash
env:
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
PR_HEAD: ${{ needs.eligibility.outputs.head }}
run: |
git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head"
if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi
git submodule update --init --recursive
- uses: hendrikmuhs/ccache-action@v1.2.24
with:
key: linux
save: ${{ github.event_name == 'push' }}
save: false
evict-old-files: job
- name: Install lavapipe for the GPU execution tests
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libvulkan1 mesa-vulkan-drivers
- run: cmake -S . -B build $CONFIGURE
- run: cmake -S . -B build $CONFIGURE -DANYPS5_ENABLE_SPIRV_TOOLS=ON
- name: Check that every test executable is registered with ctest
run: python3 tools/check_test_registration.py build
- run: cmake --build build
- run: cmake --build build --target libs
- run: ctest --test-dir build --output-on-failure --timeout 120 --repeat until-fail:3
- run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log
env:
ANYPS5_REQUIRE_VULKAN: 1
GALLIUM_OVERRIDE_CPU_CAPS: avx
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
shell: bash
run: |
[ -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
windows:
needs: eligibility
if: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && needs.eligibility.outputs.full == 'true' }}
runs-on: windows-latest
timeout-minutes: 45
env:
@ -55,7 +222,20 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.eligibility.outputs.sha }}
persist-credentials: false
submodules: true
- name: Merge the pull request into main
if: ${{ needs.eligibility.outputs.head != '' }}
shell: bash
env:
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
PR_HEAD: ${{ needs.eligibility.outputs.head }}
run: |
git fetch --no-tags --unshallow origin "refs/pull/$PR_NUMBER/head"
if [ "$(git rev-parse FETCH_HEAD)" != "$PR_HEAD" ]; then echo "::error::The pull request has newer commits; start a new build"; exit 1; fi
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit FETCH_HEAD; then echo "::error::The pull request does not merge cleanly into main; rebase it first"; exit 1; fi
git submodule update --init --recursive
- id: toolchain
uses: actions/cache@v6
with:
@ -69,13 +249,23 @@ jobs:
- uses: hendrikmuhs/ccache-action@v1.2.24
with:
key: windows
save: ${{ github.event_name == 'push' }}
save: false
evict-old-files: job
- shell: cmd
run: cmake -S . -B build %CONFIGURE%
- name: Check that every test executable is registered with ctest
shell: cmd
run: python tools/check_test_registration.py build
- shell: cmd
run: cmake --build build
- shell: cmd
run: cmake --build build --target libs
- shell: cmd
run: ctest --test-dir build --output-on-failure --timeout 120 --repeat until-fail:3
run: ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 --output-log ctest.log
- name: Report tests slower than 30 s
if: ${{ !cancelled() }}
shell: bash
run: |
[ -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

View File

@ -1,14 +1,14 @@
name: Conventions
on:
pull_request:
pull_request_target:
types: [opened, synchronize, reopened, edited]
permissions:
contents: read
concurrency:
group: conventions-${{ github.ref }}
group: conventions-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
@ -18,5 +18,15 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event.pull_request.base.sha }}
fetch-depth: 0
- run: python3 tools/check_conventions.py --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Fetch pull request objects without checking out its code
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: git fetch --no-tags origin "$HEAD_SHA"
- name: Check conventions with the trusted base script
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: python3 -I tools/check_conventions.py --base "$BASE_SHA" --head "$HEAD_SHA"

View File

@ -23,6 +23,7 @@ jobs:
force_label() { gh label create "$1" --color "$2" --description "$3" --force; }
force_label 'needs rebase' b60205 'Conflicts with main, rebase needed'
force_label 'intake paused' d93f0b 'Closed while PR intake is limited to authors with at least 20 PRs merged into main'
force_label ci 5319e7 '.github'
force_label agc 1d76db 'libSceAgc*'
force_label shader 1d76db 'core/shader'

68
.github/workflows/pr-intake.yml vendored Normal file
View File

@ -0,0 +1,68 @@
name: PR intake
on:
pull_request_target:
branches: [main]
types: [opened]
permissions: {}
concurrency:
group: pr-intake-${{ github.event.pull_request.number }}
jobs:
intake:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
const pull_number = context.payload.pull_request.number;
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number });
if (pr.state !== 'open' || pr.base.ref !== 'main') {
return;
}
const minimumMergedPRs = 20;
const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:pr is:merged base:main author:${pr.user.login}`,
per_page: 1
});
if (mergedPRs.incomplete_results || !Number.isSafeInteger(mergedPRs.total_count) || mergedPRs.total_count < 0) {
throw new Error('Could not reliably count merged pull requests');
}
if (mergedPRs.total_count >= minimumMergedPRs) {
return;
}
const name = 'intake paused';
try {
await github.rest.issues.getLabel({ owner, repo, name });
} catch (error) {
if (error.status !== 404) {
throw error;
}
try {
await github.rest.issues.createLabel({
owner, repo, name, color: 'd93f0b',
description: 'Closed while PR intake is limited to authors with at least 20 PRs merged into main'
});
} catch (createError) {
if (createError.status !== 422) {
throw createError;
}
await github.rest.issues.getLabel({ owner, repo, name });
}
}
await github.rest.issues.addLabels({ owner, repo, issue_number: pull_number, labels: [name] });
const body = 'Thank you for your interest in the project! We are sorry, but we cannot accept more PRs while we work through the existing backlog. This PR has been automatically closed and labeled "intake paused". This is not a judgment of the quality of your changes.';
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pull_number, per_page: 100
});
if (!comments.some((comment) => comment.user.login === 'github-actions[bot]' && comment.body === body)) {
await github.rest.issues.createComment({ owner, repo, issue_number: pull_number, body });
}
await github.rest.pulls.update({ owner, repo, pull_number, state: 'closed' });

View File

@ -2,6 +2,10 @@ name: Progress report
on:
pull_request:
paths:
- 'core/libs/prx/**'
- 'tools/progress.py'
- '.github/workflows/progress-report.yml'
permissions:
contents: read

View File

@ -9,6 +9,7 @@ function(add_test_executable target)
endfunction()
option(BUILD_TESTING "Build and register relinker tests" OFF)
option(ANYPS5_RELINKER_ONLY "Build the relinker without system libraries or third-party dependencies" OFF)
if(BUILD_TESTING)
enable_testing()
if(MINGW)
@ -23,13 +24,34 @@ if(BUILD_TESTING)
endif()
option(ANYPS5_ENABLE_SPIRV_TOOLS "Enable SPIRV-Tools validation and optimization" OFF)
option(APS5_ENABLE_TIMING_LOG "Enable frame timing logging" OFF)
option(APS5_ENABLE_TIMING_LOG "Write frame timings asynchronously to frame-timing.log" OFF)
add_compile_definitions(APS5_ENABLE_TIMING_LOG=$<BOOL:${APS5_ENABLE_TIMING_LOG}>)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++")
if(NOT APPLE)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static -static-libgcc -static-libstdc++")
endif()
if(BUILD_TESTING)
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME pr_overlap_encoding COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_encoding.py)
add_test(NAME progress_report COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_progress.py)
add_test(NAME import_audit COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
add_test(NAME hardware_oracle COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py)
endif()
endif()
if(ANYPS5_RELINKER_ONLY)
add_subdirectory(core/relinker)
return()
endif()
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
set(SDL_STATIC ON CACHE BOOL "" FORCE)

View File

@ -13,10 +13,13 @@
- The relinker uses only the C++20 standard library.
- Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system.
- Don't add tests that only check that a symbol is exported: a missing export already fails at startup.
- Each test finishes in 30 s on the CI runners, Vulkan tests included (Linux runs them on lavapipe), counting the first run with no shader cache. ctest runs in parallel there, so a slow test competes for the cores and times out on some runs and not others. Split or shrink a test that gets close instead of raising its timeout. CI reports every test over 30 s as a warning on the pull request.
## Build and test
Toolchains are listed in the [README](README.md#build).
Toolchains are listed in the [build instructions](docs/dev/BUILD.md).
For relinker changes, use the [relinker-only build](docs/dev/BUILD.md#relinker-only) to build and test without third-party submodules, system libraries or a GPU. It also works on macOS. Changes to system libraries or shaders still need the full build and relevant runtime tests.
```
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
@ -26,7 +29,11 @@ ctest --test-dir build --output-on-failure
Python 3 is optional; without it some relinker tests are not registered.
The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [TechnicalDebt](docs/dev/TechnicalDebt.md), and only UTF-8 text files. Run it locally before pushing:
Every push to `main`, including a merged pull request, automatically runs full Linux and Windows builds and tests and the relinker matrix on Linux, Windows and macOS, using the pushed commit without contributor checks.
For pull requests, full Linux and Windows builds and tests run only when a collaborator with write access selects Actions > Build > Run workflow on `main` and enters an open pull request number targeting `main`. This manual run also builds and tests the relinker on Linux, Windows and macOS, regardless of the author's previous contributions or the changed files. The relinker matrix also runs automatically when a pull request targeting `main` is opened, updated or reopened, if its author already has a commit in `main`; full builds never run automatically for pull requests. All build jobs build the selected pull request merged into the current `main` (GitHub's merge commit), so a pull request that no longer merges cleanly must be rebased first. New commits require another manual run for full builds.
The Conventions check runs automatically for every contributor, including first-time contributors. It uses the base branch checker and reads pull request Git objects without executing pull request code. The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [TechnicalDebt](docs/dev/TechnicalDebt.md), and only UTF-8 text files. Run it locally before pushing:
```
python3 tools/check_conventions.py --base origin/main

View File

@ -131,6 +131,22 @@ endforeach()
add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS})
if(BUILD_TESTING)
add_test_executable(patched_libraries_tests tests/PatchedLibraries.cpp)
add_dependencies(patched_libraries_tests libs)
if(MINGW)
target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables)
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
endif()
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp)
target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
configure_windows_unwind(guest_offline_imports_tests)
add_test(NAME guest_offline_imports COMMAND guest_offline_imports_tests)
if(WIN32)
set_tests_properties(guest_offline_imports PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
endif()
add_test_executable(savedata_memory_metadata_tests tests/SaveDataMemoryMetadata.cpp)
target_include_directories(savedata_memory_metadata_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(savedata_memory_metadata_tests PRIVATE libSceSaveData.native)

View File

@ -1219,6 +1219,9 @@ using NetCtlCallback = void (*)(int, void*);
struct HttpEpoll {};
using HttpEpollHandle = HttpEpoll*;
using HttpsCallback = int (*)(int, unsigned int, void* const*, int, void*);
using HttpRedirectCallback = int (*)(int, std::int32_t, std::int32_t*, const char*, void*);
using HttpCookieRecvCallback = int (*)(int, const char*, const char*, std::uint64_t, void*);
using HttpAuthInfoCallback = int (*)(int, int, const char*, char*, char*, int, std::uint8_t**, std::uint64_t*, int*, void*);
struct HttpNBEvent { std::uint8_t opaque[64]; };
@ -1243,6 +1246,14 @@ struct Http2AsyncResult {
void* reserved;
};
struct Http2AsyncOption {
KernelEqueue equeue;
int user_event_id;
std::uint8_t padding[4];
void* user_data;
void* reserved;
};
struct NpTitleId { char data[13]; char pad[3]; };
struct NpTitleSecret { std::uint8_t data[128]; };
struct NpContentRestriction { std::uint8_t opaque[128]; };
@ -1266,6 +1277,10 @@ struct NpEntitlementAccessAddcontEntitlementInfo {
std::uint32_t download_status;
};
struct NpEntitlementAccessEntitlementKey {
std::uint8_t data[16];
};
struct NpUniversalDataSystemInitParam {
@ -1375,7 +1390,7 @@ struct SaveDataMountInfo {
};
struct SceSaveDataTitleId { char data[10]; char pad[2]; };
struct SceSaveDataDirName { char data[33]; char pad[3]; };
struct SceSaveDataDirName { char data[32]; };
struct SaveDataSearchInfo { std::uint8_t opaque[128]; };
struct SaveDataMemoryData { void* buf; std::size_t buf_size; std::size_t offset; };
@ -1838,12 +1853,6 @@ struct LibcHeapInfo {
using Info = LibcHeapInfo;
#define VA_ARGS \
std::uint64_t rdi, std::uint64_t rsi, std::uint64_t rdx, std::uint64_t rcx, \
std::uint64_t r8, std::uint64_t r9, std::uint64_t overflow_arg_area, \
__m128 xmm0, __m128 xmm1, __m128 xmm2, __m128 xmm3, \
__m128 xmm4, __m128 xmm5, __m128 xmm6, __m128 xmm7, ...
struct Packet {
std::uint32_t* addr;
std::uint32_t dw_num;

View File

@ -91,6 +91,7 @@ inline bool IsDynamicNameTag(std::int64_t tag) {
return tag == kDtNeeded || tag == kDtSoname || tag == kDtRpath || tag == kDtRunpath;
}
constexpr std::uint32_t kShtDynamic = 6u;
constexpr std::uint32_t kShtGnuVerdef = 0x6ffffffdu;
constexpr std::uint32_t kShtGnuVerneed = 0x6ffffffeu;
constexpr std::uint32_t kShtDynsym = 11u;
constexpr std::uint32_t kShtGnuHash = 0x6ffffff6u;

View File

@ -337,6 +337,8 @@ void ElfNidPatcher::PatchNids(std::vector<std::uint8_t>& elf, const std::string&
}
if (shdr.sh_type == kShtGnuVersym)
versymOffset = static_cast<std::size_t>(shdr.sh_offset);
if (shdr.sh_type == kShtGnuVerdef)
throw std::runtime_error("unsupported .gnu.version_d section: symbol version definitions cannot be preserved");
}
if (dynSymOffset == 0u) throw std::runtime_error("no .dynsym section");

View File

@ -0,0 +1,209 @@
import re
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
SHT_NULL = 0
SHT_STRTAB = 3
SHT_DYNSYM = 11
SHT_GNU_HASH = 0x6FFFFFF6
SHT_GNU_VERDEF = 0x6FFFFFFD
SHT_GNU_VERSYM = 0x6FFFFFFF
SHN_UNDEF = 0
SHN_ABS = 0xFFF1
STB_GLOBAL = 1
STT_FUNC = 2
LIBRARY_NAME = "libsymbols"
DYNSYM_OFFSET = 0x200
DYNSTR_OFFSET = 0x300
HASH_OFFSET = 0x500
VERSYM_OFFSET = 0x580
VERDEF_OFFSET = 0x600
SHOFF = 0x800
BUCKETS = 4
HASHED_FROM = 3
NID_NAME = re.compile(r"^[A-Za-z0-9+-]{11}$")
SONAME = "libsymbols.so"
VERSIONS = [(1, [SONAME]), (2, ["VERS_ONE"]), (3, ["VERS_TWO", "VERS_ONE"])]
MARKERS = [("VERS_ONE", 2, 0x1100), ("VERS_TWO", 3, 0x1101)]
DEFINITIONS = [
("sceAlphaComputeLength", 2, 0x1200),
("sceAlphaCompareStrings", 2, 0x1220),
("sceAlphaFindCharacter", 2, 0x1240),
("sceBetaCopyString", 3, 0x1260),
("sceBetaConcatenateStrings", 3, 0x1280),
("sceBetaSearchCharacter", 3, 0x12A0),
]
IMPORTS = ["jeJEvW13vxk", "e0m0E5Sq2N8"]
def gnu_hash(name):
value = 5381
for byte in name.encode("utf-8"):
value = (value * 33 + byte) & 0xFFFFFFFF
return value
def symbols(versioned):
entries = [(None, 1, SHN_UNDEF, 0, 0)]
entries += [(name, 1, SHN_UNDEF, 0, 0) for name in IMPORTS]
if versioned:
entries += [(name, version, SHN_ABS, 0, value) for name, version, value in MARKERS]
entries += [(name, version, 9, STT_FUNC, value) for name, version, value in DEFINITIONS]
else:
entries += [(name, 1, 9, STT_FUNC, value) for name, _, value in DEFINITIONS]
return entries
def string_table(names):
offsets = {}
table = bytearray(b"\x00")
for name in names:
offsets.setdefault(name, len(table))
table += name.encode("utf-8") + b"\x00"
return table, offsets
def build_image(entries, versioned):
image = bytearray(0x1000)
image[:16] = b"\x7fELF\x02\x01\x01" + bytes(9)
names = [entry[0] for entry in entries if entry[0]]
if versioned:
names += [name for _, version_names in VERSIONS for name in version_names]
strings, offsets = string_table(names)
for index, (name, version, shndx, symbol_type, value) in enumerate(entries):
info = (STB_GLOBAL << 4) | symbol_type
struct.pack_into("<IBBHQQ", image, DYNSYM_OFFSET + index * 24,
offsets[name] if name else 0, info, 0, shndx, value, 0x10 if symbol_type else 0)
image[DYNSTR_OFFSET:DYNSTR_OFFSET + len(strings)] = strings
chain_count = len(entries) - HASHED_FROM
bucket_offset = HASH_OFFSET + 16 + 8
chain_offset = bucket_offset + BUCKETS * 4
struct.pack_into("<IIII", image, HASH_OFFSET, BUCKETS, HASHED_FROM, 1, 6)
for index in range(HASHED_FROM, len(entries)):
name = entries[index][0]
bucket = gnu_hash(name) % BUCKETS
if struct.unpack_from("<I", image, bucket_offset + bucket * 4)[0] == 0:
struct.pack_into("<I", image, bucket_offset + bucket * 4, index)
chain = gnu_hash(name)
if index + 1 == len(entries) or gnu_hash(entries[index + 1][0]) % BUCKETS != bucket:
chain |= 1
struct.pack_into("<I", image, chain_offset + (index - HASHED_FROM) * 4, chain)
verdef_end = 0
if versioned:
for index, (_, version, _, _, _) in enumerate(entries):
struct.pack_into("<H", image, VERSYM_OFFSET + index * 2, version)
position = VERDEF_OFFSET
for version_index, version_names in VERSIONS:
entry_size = 20 + 8 * len(version_names)
struct.pack_into("<HHHHIII", image, position, 1, 1 if version_index == 1 else 0, version_index,
len(version_names), gnu_hash(version_names[0]), 20,
0 if version_index == len(VERSIONS) else entry_size)
aux = position + 20
for name_index, name in enumerate(version_names):
struct.pack_into("<II", image, aux, offsets[name], 0 if name_index + 1 == len(version_names) else 8)
aux += 8
position += entry_size
verdef_end = position
sections = [
(0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0),
(0, SHT_DYNSYM, 2, 0, DYNSYM_OFFSET, len(entries) * 24, 2, 1, 8, 24),
(0, SHT_STRTAB, 2, 0, DYNSTR_OFFSET, len(strings), 0, 0, 1, 0),
(0, SHT_GNU_HASH, 2, 0, HASH_OFFSET, chain_offset - HASH_OFFSET + chain_count * 4, 1, 0, 8, 0),
]
if versioned:
sections.append((0, SHT_GNU_VERSYM, 2, 0, VERSYM_OFFSET, len(entries) * 2, 1, 0, 2, 2))
sections.append((0, SHT_GNU_VERDEF, 2, 0, VERDEF_OFFSET, verdef_end - VERDEF_OFFSET, 2, len(VERSIONS), 4, 0))
struct.pack_into("<HHIQQQIHHHHHH", image, 16, 3, 62, 1, 0, 0, SHOFF, 0, 64, 0, 0, 64, len(sections), 0)
for index, section in enumerate(sections):
struct.pack_into("<IIQQQQIIQQ", image, SHOFF + index * 64, *section)
return image
def sections(image):
shoff, = struct.unpack_from("<Q", image, 0x28)
shentsize, shnum = struct.unpack_from("<HH", image, 0x3A)
return [struct.unpack_from("<IIQQQQIIQQ", image, shoff + index * shentsize) for index in range(shnum)]
def read_dynsym(image):
headers = sections(image)
dynsym = next(section for section in headers if section[1] == SHT_DYNSYM)
strings = headers[dynsym[6]]
entries = []
for index in range(dynsym[5] // 24):
name_offset, _, _, shndx, value, _ = struct.unpack_from("<IBBHQQ", image, dynsym[4] + index * 24)
name = None
if name_offset:
end = image.index(0, strings[4] + name_offset)
name = image[strings[4] + name_offset:end].decode("utf-8")
entries.append((name, shndx, value))
return entries
def patch(patcher, image, filename):
with tempfile.TemporaryDirectory(prefix="anyps5-symbol-versions-") as directory:
path = Path(directory) / filename
path.write_bytes(image)
result = subprocess.run([patcher, LIBRARY_NAME, str(path)], capture_output=True)
return result, path.read_bytes()
def test_versioned_library_is_rejected(patcher):
image = bytes(build_image(symbols(True), True))
assert any(section[1] == SHT_GNU_VERDEF for section in sections(image))
result, on_disk = patch(patcher, image, SONAME)
assert result.returncode == 2, (result.returncode, result.stderr)
message = result.stderr.decode("utf-8", errors="replace")
assert message.startswith("FAIL: "), message
assert ".gnu.version_d" in message, message
assert result.stdout == b"", result.stdout
assert on_disk == image, "the rejected library was written back modified"
def test_unversioned_library_is_still_patched(patcher):
image = bytes(build_image(symbols(False), False))
assert not any(section[1] == SHT_GNU_VERDEF for section in sections(image))
before = read_dynsym(image)
result, patched = patch(patcher, image, SONAME)
assert result.returncode == 0, result.stderr.decode("utf-8", errors="replace")
assert result.stdout.decode("utf-8", errors="replace").startswith("OK: "), result.stdout
after = read_dynsym(patched)
assert len(after) == len(before), (before, after)
assert [entry[0] for entry in after] != [entry[0] for entry in before], "no export was renamed"
for name, _, value in DEFINITIONS:
found = [entry for entry in after if entry[2] == value]
assert len(found) == 1, (name, found)
renamed, shndx, _ = found[0]
assert renamed != name, (name, found)
assert NID_NAME.match(renamed), (name, found)
assert shndx == 9, (name, found)
for name in IMPORTS:
found = [entry for entry in after if entry[0] == name]
assert found == [(name, SHN_UNDEF, 0)], (name, found)
assert len({entry[0] for entry in after if entry[0]}) == len([entry for entry in after if entry[0]])
def main():
patcher = Path(sys.argv[1]).resolve()
test_versioned_library_is_rejected(patcher)
test_unversioned_library_is_still_patched(patcher)
print("NID patcher symbol version tests passed")
if __name__ == "__main__":
main()

View File

@ -12,8 +12,7 @@
extern "C" {
// unknown signature
APS5_EXPORT("gQkqkLttcpw", sceAgcAcb_gQkqkLttcpw);
void* APS5_VABI sceAgcAcb_gQkqkLttcpw (void) {
void* APS5_VABI sceAgcAcbAtomicGds_0900(void) {
NotImplemented_nid_no_patch(__func__);
return nullptr;
}

View File

@ -209,14 +209,15 @@ if(BUILD_TESTING)
endif()
add_test_executable(agc_shader_fusion_tests EXCLUDE_FROM_ALL tests/ShaderFusion.cpp)
target_include_directories(agc_shader_fusion_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_fusion_tests PRIVATE libSceAgc libc)
target_include_directories(agc_shader_fusion_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include ${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include)
target_link_libraries(agc_shader_fusion_tests PRIVATE libSceAgc libSceAgcDriver libc)
set_target_properties(agc_shader_fusion_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_fusion_tests)
if(BUILD_TESTING)
set_target_properties(agc_shader_fusion_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_shader_fusion COMMAND agc_shader_fusion_tests)
set_tests_properties(agc_shader_fusion PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_shader_fusion PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
@ -236,9 +237,13 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_interpolant_mapping_tests EXCLUDE_FROM_ALL tests/InterpolantMapping.cpp)
add_test_executable(agc_interpolant_mapping_tests EXCLUDE_FROM_ALL
tests/InterpolantMapping.cpp
Shader/src/InterpolantMapping.cpp
Shader/src/ShaderUtils.cpp
)
target_include_directories(agc_interpolant_mapping_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_interpolant_mapping_tests PRIVATE libSceAgc libc)
target_link_libraries(agc_interpolant_mapping_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_interpolant_mapping_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_interpolant_mapping_tests)
@ -252,9 +257,15 @@ if(BUILD_TESTING)
endif()
if(BUILD_TESTING)
add_test_executable(guest_shader_link_tests ${LIBS_INCLUDE_DIR}/tests/GuestShaderLink.cpp)
add_test_executable(guest_shader_link_tests
${LIBS_INCLUDE_DIR}/tests/GuestShaderLink.cpp
Shader/src/LinkShaders.cpp
Shader/src/PrimState.cpp
Shader/src/InterpolantMapping.cpp
Shader/src/ShaderUtils.cpp
)
target_include_directories(guest_shader_link_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(guest_shader_link_tests PRIVATE libSceAgc libc)
target_link_libraries(guest_shader_link_tests PRIVATE libSceAgc libSceAgcDriver libc)
configure_windows_unwind(guest_shader_link_tests)
add_test(NAME guest_shader_link COMMAND guest_shader_link_tests)
if(WIN32)
@ -358,3 +369,22 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_shader_helpers_tests EXCLUDE_FROM_ALL
tests/ShaderHelpers.cpp
Shader/src/LinkShaders.cpp
Shader/src/PrimState.cpp
Shader/src/InterpolantMapping.cpp
Shader/src/ShaderUtils.cpp
)
target_include_directories(agc_shader_helpers_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_helpers_tests PRIVATE libc libSceAgcDriver)
configure_windows_unwind(agc_shader_helpers_tests)
if(BUILD_TESTING)
set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests)
if(WIN32)
set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()

View File

@ -40,7 +40,7 @@ std::uint32_t available(const CommandBuffer& buffer, const char* function) {
const auto bottom = reinterpret_cast<std::uintptr_t>(buffer.bottom);
const auto top = reinterpret_cast<std::uintptr_t>(buffer.top);
const auto up = reinterpret_cast<std::uintptr_t>(buffer.cursor_up);
const auto down = reinterpret_cast<std::uintptr_t>(buffer.cursor_down);
const auto down = buffer.cursor_down != nullptr ? reinterpret_cast<std::uintptr_t>(buffer.cursor_down) : top;
Require(((bottom | top | up | down) & 3u) == 0, function, "misaligned command buffer");
Require(bottom <= up && up <= down && down <= top, function, "invalid command buffer cursors");
Require(bottom != 0 || top == 0, function, "null command buffer storage");

View File

@ -35,8 +35,7 @@ std::uint32_t APS5_VABI sceAgcDcbSetIndexSizeGetSize() {
return 12;
}
APS5_EXPORT("-KRzWekV120", sceAgcUnknown__MKRzWekV120);
std::uint32_t* APS5_VABI sceAgcUnknown__MKRzWekV120(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy, std::uint8_t perInstanceObjectId) {
std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize_0600(CommandBuffer* buf, std::uint8_t indexSize, std::uint8_t cachePolicy, std::uint8_t perInstanceObjectId) {
Agc::Command::CheckBits(perInstanceObjectId, 1, __func__);
auto* packet = sceAgcDcbSetIndexSize(buf, indexSize, cachePolicy);
packet[2] |= static_cast<std::uint32_t>(perInstanceObjectId) << 14u;

View File

@ -26,8 +26,7 @@ uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(uint32_t operation) {
return 27 * sizeof(std::uint32_t);
}
APS5_EXPORT("qj7QZpgr9Uw", sceAgcDcbContextStateAnotherOp);
uint32_t* APS5_VABI sceAgcDcbContextStateAnotherOp(CommandBuffer* buf, uint32_t operation) {
uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, uint32_t operation) {
Agc::Command::Require(buf != nullptr, __func__, "null command buffer");
Agc::Command::Require(operation <= 3, __func__, "invalid context state operation");
const auto* function = __func__;

View File

@ -9,8 +9,7 @@
extern "C" {
APS5_EXPORT("23LRUSvYu1M", sceAgcUnknownInitState);
int APS5_VABI sceAgcUnknownInitState(std::uint32_t* state, std::uint32_t version) {
int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version) {
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(state), alignof(std::uint32_t), __func__);
Agc::Command::Require(version < 14, __func__, "unsupported AGC initialization version");
return 0;

View File

@ -10,7 +10,7 @@ struct SizeAlign {
std::size_t m_align;
};
extern "C" int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem);
extern "C" int APS5_VABI sceAgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front, const Shader* back);
extern "C" int APS5_VABI sceAgcFuseShaderHalves_0200(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem);
extern "C" int APS5_VABI sceAgcGetFusedShaderSize_0080(SizeAlign* dst, const Shader* front, const Shader* back);
#endif

View File

@ -17,8 +17,7 @@ uint32_t APS5_VABI sceAgcGetPacketSize(uint32_t* packet) {
return ((packet[0] >> 16u) & 0x3fffu) + 2u;
}
APS5_EXPORT("V++UgBtQhn0", sceAgcGetDataPacketPayloadAddressUnk);
int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(uint32_t** addr, uint32_t* cmd, int type) {
int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(uint32_t** addr, uint32_t* cmd, int type) {
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(addr), alignof(uint32_t*), __func__);
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(cmd), alignof(uint32_t), __func__);
if (type != 0) {

View File

@ -28,6 +28,7 @@ constexpr std::uint32_t SPI_SHADER_PGM_RSRC2_HS = 0x10Bu;
constexpr std::uint32_t SCodeEnd = 0xbf9f0000u;
constexpr std::uint32_t SNop = 0xbf800000u;
constexpr std::uint32_t SSetpcS6 = 0xbe802006u;
constexpr std::uint32_t SSwappcNullS6 = 0xbefd2106u;
constexpr std::uint8_t FusedCodeAlignmentLog2 = 8;
constexpr std::size_t FusedCodeAlignment = std::size_t{1} << FusedCodeAlignmentLog2;
constexpr char TrailerMagic[8] = {'b', 'a', 'r', 'e', 'f', 'o', 'o', 't'};
@ -51,7 +52,7 @@ ShaderRegister& FindRegister(ShaderRegister* regs, std::uint32_t count, std::uin
for (std::uint32_t i = 0; regs != nullptr && i < count; ++i) {
if (regs[i].offset == offset && occurrence-- == 0) return regs[i];
}
throw std::runtime_error("sceAgcUnknownFuseShaderHalves: shader half lacks register " + std::to_string(offset));
throw std::runtime_error("sceAgcFuseShaderHalves_0200: shader half lacks register " + std::to_string(offset));
}
void MergeMax(ShaderRegister& dst, const ShaderRegister& src, std::uint32_t shift, std::uint32_t mask) {
@ -114,7 +115,7 @@ std::uint32_t FrontProgramBytes(const char* function, const Shader* front) {
return value;
};
while (end > 0 && word(end - 1) == SCodeEnd) --end;
if (end == 0 || word(end - 1) != SSetpcS6) Fail(function, "front half does not end with s_setpc_b64 s[6:7]");
if (end == 0 || (word(end - 1) != SSetpcS6 && word(end - 1) != SSwappcNullS6)) Fail(function, "front half does not end with s_setpc_b64 s[6:7] or s_swappc_b64 null, s[6:7]");
return static_cast<std::uint32_t>((end - 1) * 4);
}
@ -187,11 +188,11 @@ void SetProgramAddress(std::vector<ShaderRegister>& regs, std::uint32_t loOffset
regs[i + 1].value = (regs[i + 1].value & 0xFFFFFF00u) | static_cast<std::uint32_t>((address >> 40u) & 0xFFu);
return;
}
throw std::runtime_error("sceAgcUnknownFuseShaderHalves: back half has no program address register pair");
throw std::runtime_error("sceAgcFuseShaderHalves_0200: back half has no program address register pair");
}
int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) {
constexpr auto fn = "sceAgcUnknownFuseShaderHalves";
constexpr auto fn = "sceAgcFuseShaderHalves_0200";
if (scratch_mem == nullptr) Fail(fn, "geometry halves need fused shader memory");
auto layout = ComputeGeometryLayout(fn, front, back);
const auto scratch = reinterpret_cast<std::uintptr_t>(scratch_mem);
@ -251,8 +252,7 @@ int FuseGeometryHalves(Shader* fused_result, const Shader* front, const Shader*
extern "C" {
APS5_EXPORT("fd5Bp5tGTgo", sceAgcUnknownFuseShaderHalves);
int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) {
int APS5_VABI sceAgcFuseShaderHalves_0200(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) {
if (fused_result == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX;
if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES;
const bool isGs = GeometryHalves(front);
@ -298,15 +298,14 @@ int APS5_VABI sceAgcUnknownFuseShaderHalves(Shader* fused_result, const Shader*
}
int APS5_VABI sceAgcFuseShaderHalves_nid_postfix(Shader* fused_result, const Shader* front, const Shader* back, void* scratch_mem) {
return sceAgcUnknownFuseShaderHalves(fused_result, front, back, scratch_mem);
return sceAgcFuseShaderHalves_0200(fused_result, front, back, scratch_mem);
}
APS5_EXPORT("dolOmWH+huQ", sceAgcUnknownGetFusedShaderSize);
int APS5_VABI sceAgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* front, const Shader* back) {
int APS5_VABI sceAgcGetFusedShaderSize_0080(SizeAlign* dst, const Shader* front, const Shader* back) {
if (dst == nullptr || front == nullptr || back == nullptr) APS5_INVALID_ARG_EX;
if (!ValidHalves(front, back)) return GRAPHICS5_ERROR_INVALID_SHADER_HALVES;
if (GeometryHalves(front)) {
const auto layout = ComputeGeometryLayout("sceAgcUnknownGetFusedShaderSize", front, back);
const auto layout = ComputeGeometryLayout("sceAgcGetFusedShaderSize_0080", front, back);
dst->m_size = layout.totalBytes + FusedCodeAlignment - 1;
dst->m_align = FusedCodeAlignmentLog2;
return 0;
@ -316,8 +315,7 @@ int APS5_VABI sceAgcUnknownGetFusedShaderSize(SizeAlign* dst, const Shader* fron
return 0;
}
APS5_EXPORT("k0E7vkgqAuE", sceAgcCreateInterpolantMappingVsPs);
int APS5_VABI sceAgcCreateInterpolantMappingVsPs(ShaderRegister* regs, const Shader* vs, const Shader* ps) {
int APS5_VABI sceAgcUpdateInterpolantMapping_0100(ShaderRegister* regs, const Shader* vs, const Shader* ps) {
(void)regs;
(void)vs;
(void)ps;

View File

@ -28,8 +28,7 @@ int APS5_VABI sceAgcJumpPatchSetTarget(uint32_t* cmd, const volatile uint32_t* t
return 0;
}
APS5_EXPORT("Ikfdt-rIqCE", sceAgcUnknown_Ikfdt_MrIqCE);
int APS5_VABI sceAgcUnknown_Ikfdt_MrIqCE(uint32_t* cmd, uint64_t cache_policy, const volatile uint32_t* target, uint32_t size_in_dwords) {
int APS5_VABI sceAgcJumpPatchSetTarget_0300(uint32_t* cmd, uint64_t cache_policy, const volatile uint32_t* target, uint32_t size_in_dwords) {
Agc::Command::CheckBits(cache_policy, 3u, __func__);
PatchJump(cmd, target, size_in_dwords, __func__);
cmd[3] = (cmd[3] & ~(3u << 28u)) | (static_cast<std::uint32_t>(cache_policy) << 28u);

View File

@ -1,10 +1,14 @@
#include "prx/libSceAgc/Shader/include/InterpolantMapping.hpp"
#include <cstdio>
#include <array>
#include <algorithm>
#include <stdexcept>
#include <prx/libc/include/General.hpp>
#include "SceShaders.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgc/Shader/include/ShaderUtils.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
@ -53,9 +57,22 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
if (regs == nullptr) {
throw std::runtime_error(std::string(fn) + ": regs is null");
}
if (ps != nullptr && ps->num_input_semantics > 32) {
throw std::runtime_error(std::string(fn) + ": input semantic count exceeds 32");
}
AgcDriver::ShaderPreparationScope transaction;
std::array<ShaderRegister, 32> values{};
auto* output = regs;
regs = values.data();
if (ps == nullptr || ps->num_input_semantics == 0) {
FillIdentityInterpolants(regs, 0);
if (ps != nullptr) {
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {});
if (gs != nullptr) AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0);
}
transaction.Commit();
std::copy(values.begin(), values.end(), output);
return 0;
}
@ -82,6 +99,10 @@ int CreateInterpolantMapping(const char* fn, ShaderRegister* regs, const Shader*
}
FillIdentityInterpolants(regs, ps->num_input_semantics);
AgcDriverResolveShaderAbi_nid_postfix(ps, {regs, 32}, {});
AgcDriverResolveGraphicsAbi_nid_postfix(gs, ps, 0);
transaction.Commit();
std::copy(values.begin(), values.end(), output);
return 0;
}

View File

@ -1,4 +1,6 @@
#include "SceShaders.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include <array>
@ -20,10 +22,13 @@ extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister* context, ShaderRegist
// The simple vertex/pixel pipeline combines two primitive context registers
// and 32 interpolant registers. Publish only after both helpers succeed.
AgcDriver::ShaderPreparationScope transaction;
std::array<ShaderRegister, 34> contextValues{};
std::array<ShaderRegister, 3> primitiveValues{};
sceAgcCreatePrimState(contextValues.data(), primitiveValues.data(), nullptr, vertex, primitiveType);
sceAgcCreateInterpolantMapping(contextValues.data() + 2, vertex, pixel);
AgcDriverResolveGraphicsAbi_nid_postfix(vertex, pixel, primitiveType);
transaction.Commit();
std::copy(contextValues.begin(), contextValues.end(), context);
std::copy(primitiveValues.begin(), primitiveValues.end(), primitive);
return 0;

View File

@ -1,10 +1,13 @@
#include "prx/libSceAgc/Shader/include/PrimState.hpp"
#include <cstdio>
#include <array>
#include <algorithm>
#include <stdexcept>
#include <prx/libc/include/General.hpp>
#include "SceShaders.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgc/Shader/include/ShaderUtils.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
@ -22,44 +25,26 @@ int APS5_VABI sceAgcCreatePrimState(ShaderRegister* cx_regs, ShaderRegister* uc_
throw std::runtime_error(std::string(__func__) + ": invalid shader type or missing special registers");
}
(void)GraphicsPrimTypeToGsOut(prim_type);
if (cx_regs != nullptr) {
const auto valid = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN && shader->specials->vgt_gs_out_prim_type.offset == ShaderRegs::VGT_GS_OUT_PRIM_TYPE; };
if (!valid(gs) || (hs != nullptr && !valid(hs))) {
throw std::runtime_error(std::string(__func__) + ": invalid context register offsets");
}
const auto valid = [](const Shader* shader) { return shader->specials->vgt_shader_stages_en.offset == ShaderRegs::VGT_SHADER_STAGES_EN && shader->specials->vgt_gs_out_prim_type.offset == ShaderRegs::VGT_GS_OUT_PRIM_TYPE; };
if (!valid(gs) || (hs != nullptr && !valid(hs))) {
throw std::runtime_error(std::string(__func__) + ": invalid context register offsets");
}
if (uc_regs != nullptr && (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN))) {
if (gs->specials->ge_cntl.offset != ShaderRegs::GE_CNTL || gs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN || (hs != nullptr && hs->specials->ge_user_vgpr_en.offset != ShaderRegs::GE_USER_VGPR_EN)) {
throw std::runtime_error(std::string(__func__) + ": invalid user configuration register offsets");
}
if (cx_regs != nullptr) {
cx_regs[0] = gs->specials->vgt_shader_stages_en;
if ((cx_regs[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) != 0) {
cx_regs[1] = gs->specials->vgt_gs_out_prim_type;
} else {
cx_regs[1].offset = ShaderRegs::VGT_GS_OUT_PRIM_TYPE;
cx_regs[1].value = GraphicsPrimTypeToGsOut(prim_type);
}
if (hs != nullptr) {
cx_regs[0].value |= hs->specials->vgt_shader_stages_en.value;
if ((cx_regs[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) {
cx_regs[1] = hs->specials->vgt_gs_out_prim_type;
}
}
std::array<ShaderRegister, 2> contextValues{gs->specials->vgt_shader_stages_en, gs->specials->vgt_gs_out_prim_type};
if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) {
contextValues[1] = {ShaderRegs::VGT_GS_OUT_PRIM_TYPE, GraphicsPrimTypeToGsOut(prim_type)};
}
if (uc_regs != nullptr) {
uc_regs[0] = gs->specials->ge_cntl;
uc_regs[1] = gs->specials->ge_user_vgpr_en;
uc_regs[2].offset = ShaderRegs::VGT_PRIMITIVE_TYPE;
uc_regs[2].value = prim_type;
if (hs != nullptr) {
uc_regs[1] = hs->specials->ge_user_vgpr_en;
}
if (hs != nullptr) {
contextValues[0].value |= hs->specials->vgt_shader_stages_en.value;
if ((contextValues[0].value & ShaderRegs::VGT_SHADER_STAGES_GS_BIT) == 0) contextValues[1] = hs->specials->vgt_gs_out_prim_type;
}
std::array<ShaderRegister, 3> primitiveValues{gs->specials->ge_cntl, hs != nullptr ? hs->specials->ge_user_vgpr_en : gs->specials->ge_user_vgpr_en, ShaderRegister{ShaderRegs::VGT_PRIMITIVE_TYPE, prim_type}};
const std::array<const Shader*, 2> stages{hs, gs};
AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span(stages).subspan(hs == nullptr ? 1u : 0u), contextValues, primitiveValues);
if (cx_regs != nullptr) std::copy(contextValues.begin(), contextValues.end(), cx_regs);
if (uc_regs != nullptr) std::copy(primitiveValues.begin(), primitiveValues.end(), uc_regs);
return 0;
}

View File

@ -80,8 +80,7 @@ int APS5_VABI sceAgcCbMemsetExclusive() {
return 0;
}
APS5_EXPORT("7Wa3aeJgeVU", sceAgcUnknown_7Wa3aeJgeVU);
int APS5_VABI sceAgcUnknown_7Wa3aeJgeVU() {
int APS5_VABI sceAgcBranchPatchSetThenTarget_0300() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
@ -101,8 +100,7 @@ int APS5_VABI sceAgcGetShaderInstrumentation() {
return 0;
}
APS5_EXPORT("rP5xLdOf26k", sceAgcUnknown_rP5xLdOf26k);
int APS5_VABI sceAgcUnknown_rP5xLdOf26k() {
int APS5_VABI sceAgcBranchPatchSetElseTarget_0300() {
NotImplemented_nid_no_patch(__func__);
return 0;
}

View File

@ -18,15 +18,15 @@ 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 sceAgcUnknownInitState(std::uint32_t* state, std::uint32_t version);
extern "C" int APS5_VABI sceAgcInit_0090(std::uint32_t* state, std::uint32_t version);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirectMulti(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint32_t countIndirect, std::uint32_t maxCountOrCount, const volatile void* countAddress, std::uint32_t strideInBytes, std::uint64_t modifier);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference);
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateAnotherOp(CommandBuffer* buf, std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbPushMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbPopMarker(CommandBuffer* buf);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
@ -101,6 +101,12 @@ void testPackets() {
exhausted.buffer.cursor_down = exhausted.words.data() + 2;
expectFailure([&] { Agc::Command::WriteNop(&exhausted.buffer, 3, __func__); });
check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed allocation advanced cursor");
std::array<std::uint32_t, 8> scratch{};
CommandBuffer noDown{scratch.data(), scratch.data() + scratch.size(), scratch.data(), nullptr, nullptr, nullptr, 0};
Agc::Command::WriteNop(&noDown, 6, __func__);
check(noDown.cursor_up == scratch.data() + 6, "buffer without a down cursor did not use its top as the limit");
expectFailure([&] { Agc::Command::WriteNop(&noDown, 3, __func__); });
check(noDown.cursor_up == scratch.data() + 6, "allocation past the top of a buffer without a down cursor advanced its cursor");
}
void testClearState() {
@ -222,7 +228,7 @@ void testContextState() {
source.buffer.reserved_dw = 2;
source.buffer.callback = growContext;
source.buffer.user_data = &growth;
auto* first = sceAgcDcbContextStateAnotherOp(&source.buffer, operation);
auto* first = sceAgcDcbContextStateOp_0100(&source.buffer, operation);
check(first == (split == 0 ? growth.destination.words.data() : source.words.data()), "incorrect first context packet address");
check(growth.calls == (requested == 0 ? 0u : 1u), "incorrect context callback count");
auto* end = requested == 0 ? source.words.data() + totals[operation] : growth.destination.words.data() + totals[operation] - split;
@ -243,24 +249,24 @@ void testContextState() {
}
}
}
expectFailure([] { sceAgcDcbContextStateAnotherOp(nullptr, 0); });
expectFailure([] { sceAgcDcbContextStateOp_0100(nullptr, 0); });
Storage invalid;
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 4); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 4); });
check(invalid.buffer.cursor_up == invalid.words.data(), "invalid context operation advanced cursor");
invalid.buffer.cursor_up = invalid.words.data() + 1;
invalid.buffer.cursor_down = invalid.words.data();
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 0); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 0); });
invalid.buffer.cursor_up = invalid.words.data();
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 1); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 1); });
ContextGrowth growth{{}, invalid.words.data(), 22};
invalid.buffer.callback = growContext;
invalid.buffer.user_data = &growth;
growth.success = false;
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 1); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 1); });
growth.calls = 0;
growth.success = true;
growth.destination.buffer.cursor_down = growth.destination.words.data() + 21;
expectFailure([&] { sceAgcDcbContextStateAnotherOp(&invalid.buffer, 1); });
expectFailure([&] { sceAgcDcbContextStateOp_0100(&invalid.buffer, 1); });
check(invalid.buffer.cursor_up == growth.destination.words.data(), "failed reservation advanced cursor");
}
@ -322,19 +328,19 @@ void testPacketPayloadAddress() {
Storage storage;
auto* packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, nullptr, 4);
std::uint32_t* payload = nullptr;
check(sceAgcGetDataPacketPayloadAddressUnk(&payload, packet, 1) == 0 && payload == packet + 2, "incorrect register packet payload address");
check(sceAgcGetDataPacketPayloadAddress_0090(&payload, packet, 1) == 0 && payload == packet + 2, "incorrect register packet payload address");
const std::array<std::uint32_t, 4> values{11, 22, 33, 44};
std::copy(values.begin(), values.end(), payload);
const std::array<std::uint32_t, 6> expected{0xc0047600u, 0x8c, 11, 22, 33, 44};
check(std::equal(expected.begin(), expected.end(), packet), "payload write corrupted register packet");
check(sceAgcGetDataPacketPayloadAddressUnk(&payload, packet, 0) == 0 && payload == packet + 1, "incorrect generic packet payload address");
check(sceAgcGetDataPacketPayloadAddress_0090(&payload, packet, 0) == 0 && payload == packet + 1, "incorrect generic packet payload address");
packet[0] = 0xffff1000u;
check(sceAgcGetDataPacketPayloadAddressUnk(&payload, packet, 0) == 0 && payload == nullptr, "empty payload marker was not recognized");
check(sceAgcGetDataPacketPayloadAddressUnk(&payload, packet, -1) == 0 && payload == packet + 2, "nonzero payload type did not skip two words");
expectFailure([&] { sceAgcGetDataPacketPayloadAddressUnk(nullptr, packet, 1); });
expectFailure([&] { sceAgcGetDataPacketPayloadAddressUnk(&payload, nullptr, 1); });
check(sceAgcGetDataPacketPayloadAddress_0090(&payload, packet, 0) == 0 && payload == nullptr, "empty payload marker was not recognized");
check(sceAgcGetDataPacketPayloadAddress_0090(&payload, packet, -1) == 0 && payload == packet + 2, "nonzero payload type did not skip two words");
expectFailure([&] { sceAgcGetDataPacketPayloadAddress_0090(nullptr, packet, 1); });
expectFailure([&] { sceAgcGetDataPacketPayloadAddress_0090(&payload, nullptr, 1); });
auto* misaligned = reinterpret_cast<std::uint32_t*>(reinterpret_cast<unsigned char*>(packet) + 1);
expectFailure([&] { sceAgcGetDataPacketPayloadAddressUnk(&payload, misaligned, 0); });
expectFailure([&] { sceAgcGetDataPacketPayloadAddress_0090(&payload, misaligned, 0); });
check(payload == packet + 2, "invalid packet changed output address");
}
@ -367,10 +373,10 @@ void testMemory() {
void testDefaults() {
std::uint32_t state = 0x12345678;
check(sceAgcUnknownInitState(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state");
check(sceAgcUnknownInitState(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state");
expectFailure([] { sceAgcUnknownInitState(nullptr, 8); });
expectFailure([&] { sceAgcUnknownInitState(&state, 14); });
check(sceAgcInit_0090(&state, 8) == 0 && state == 0x12345678, "AGC initialization failed or modified caller state");
check(sceAgcInit_0090(&state, 13) == 0 && state == 0x12345678, "AGC version 13 initialization changed caller state");
expectFailure([] { sceAgcInit_0090(nullptr, 8); });
expectFailure([&] { sceAgcInit_0090(&state, 14); });
check(sceAgcInit(8) == 0, "AGC version initialization failed");
expectFailure([] { sceAgcInit(14); });
expectFailure([] { sceAgcInit(0xffffffffu); });

View File

@ -8,7 +8,7 @@
#include <stdexcept>
extern "C" std::uint64_t APS5_VABI sceAgcDcbContextStateOpGetSize(std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateAnotherOp(CommandBuffer* buf, std::uint32_t operation);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateOp_0100(CommandBuffer* buf, std::uint32_t operation);
namespace {
@ -34,7 +34,7 @@ void testSizes() {
std::array<std::uint32_t, 64> words{};
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(),
nullptr, nullptr, 0};
check(sceAgcDcbContextStateAnotherOp(&buffer, operation) == words.data(), "context state op did not start at the cursor");
check(sceAgcDcbContextStateOp_0100(&buffer, operation) == words.data(), "context state op did not start at the cursor");
const auto written = static_cast<std::uint64_t>(buffer.cursor_up - words.data()) * sizeof(std::uint32_t);
check(written == sizes[operation], "context state size differs from the written packets");
}

View File

@ -4,10 +4,12 @@
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include <array>
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <stdexcept>
#include <span>
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* regs, const Shader* gs, const Shader* ps);
@ -15,6 +17,10 @@ extern "C" int APS5_VABI sceAgcUnknownCreateInterpolantMapping(ShaderRegister* r
namespace {
using Registers = std::array<ShaderRegister, 32>;
const Shader* mappedPixel = nullptr;
Registers preparedMapping{};
unsigned mappings = 0;
unsigned links = 0;
void check(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
@ -82,6 +88,7 @@ void testMapping() {
auto split = filled();
check(sceAgcUnknownCreateInterpolantMapping(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");
}
@ -94,6 +101,7 @@ void testMapping() {
auto regular = filled();
check(sceAgcCreateInterpolantMapping(regular.data(), &gs, &ps) == 0, "mapping failed");
check(std::memcmp(regular.data(), preparedMapping.data(), sizeof(regular)) == 0, "prepared mapping differs from published registers");
check(regular[3].value == split[3].value && regular[4].value == split[4].value, "mappings differ outside the high f16 half mode");
checkIdentity(regular, static_cast<std::uint32_t>(inputs.size()), "unused interpolants are not the identity");
}
@ -117,11 +125,24 @@ void testRejections() {
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
check(shader != nullptr && context.size() == preparedMapping.size() && primitive.empty(), "invalid mapping preparation call");
mappedPixel = shader;
std::copy(context.begin(), context.end(), preparedMapping.begin());
++mappings;
}
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
check(vertex != nullptr && pixel == mappedPixel && primitiveType == 0u, "invalid mapping link call");
++links;
}
int main() {
try {
testIdentity();
testMapping();
testRejections();
check(mappings == 3u && links == 2u, "mapping preparation call count changed");
LibcRunShutdown_nid_postfix();
std::puts("AGC interpolant mapping tests passed");
return 0;

View File

@ -9,7 +9,7 @@
#include <tuple>
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, std::uint32_t* cmd, int type);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type);
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddress_0090(std::uint32_t** addr, std::uint32_t* cmd, int type);
namespace {
@ -42,7 +42,7 @@ void testRanges() {
check(range.size == size, "payload range size mismatch");
check(words[0] == header && words[1] == 0x12345678u, "payload range query modified the packet");
std::uint32_t* address = nullptr;
check(sceAgcGetDataPacketPayloadAddressUnk(&address, words.data(), type) == 0 && address == range.base,
check(sceAgcGetDataPacketPayloadAddress_0090(&address, words.data(), type) == 0 && address == range.base,
"payload range base differs from the payload address");
}
}

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgc/Misc/include/ShaderFusion.hpp"
#include "prx/libSceAgcDriver/tests/execution/VulkanTestDevice.hpp"
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
#include "prx/libc/include/Shutdown.hpp"
@ -15,6 +16,7 @@ namespace {
constexpr std::uint32_t SCodeEnd = 0xbf9f0000u;
constexpr std::uint32_t SNop = 0xbf800000u;
constexpr std::uint32_t SSetpcS6 = 0xbe802006u;
constexpr std::uint32_t SSwappcNullS6 = 0xbefd2106u;
constexpr std::uint32_t SMovS0 = 0xbe800380u;
constexpr std::uint32_t VMovV0 = 0x7e000280u;
constexpr std::uint32_t SEndpgm = 0xbf810000u;
@ -67,8 +69,8 @@ struct Halves {
Shader front{};
Shader back{};
Halves() {
frontCode = {SMovS0, VMovV0, SSetpcS6, SCodeEnd, SCodeEnd};
explicit Halves(std::uint32_t terminator = SSetpcS6) {
frontCode = {SMovS0, VMovV0, terminator, SCodeEnd, SCodeEnd};
const auto codeBytes = static_cast<std::uint32_t>(frontCode.size() * 4);
std::array<std::uint32_t, 2> magic{};
std::memcpy(magic.data(), "barefoot", 8);
@ -123,16 +125,16 @@ std::uint8_t* aligned(std::vector<std::uint8_t>& storage, std::size_t alignment)
return storage.data() + ((alignment - address % alignment) % alignment);
}
void testFusion(std::size_t misalignment) {
Halves halves;
void testFusion(std::size_t misalignment, std::uint32_t terminator = SSetpcS6) {
Halves halves(terminator);
SizeAlign size{};
check(sceAgcUnknownGetFusedShaderSize(&size, &halves.front, &halves.back) == 0, "fused size query failed");
check(sceAgcGetFusedShaderSize_0080(&size, &halves.front, &halves.back) == 0, "fused size query failed");
check(size.m_align == 8, "fused shaders are not 256-byte aligned");
check(size.m_size > 256 + 255 + halves.back.shader_size + sizeof(Shader), "fused size does not cover the alignment padding, code and header");
std::vector<std::uint8_t> storage(size.m_size + 512, 0xcd);
auto* scratch = aligned(storage, 256) + misalignment;
Shader fused{};
check(sceAgcUnknownFuseShaderHalves(&fused, &halves.front, &halves.back, scratch) == 0, "fusion failed");
check(sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, scratch) == 0, "fusion failed");
for (std::size_t i = 0; i < 16; ++i) check(scratch[size.m_size + i] == 0xcd, "fusion wrote past the reported size");
auto* memory = scratch + (256 - misalignment) % 256;
const auto base = reinterpret_cast<std::uintptr_t>(memory);
@ -173,38 +175,38 @@ void testRejections() {
Halves halves;
SizeAlign size{};
Shader fused{};
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(nullptr, &halves.front, &halves.back); });
expectFailure([&] { sceAgcUnknownFuseShaderHalves(&fused, &halves.front, &halves.back, nullptr); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(nullptr, &halves.front, &halves.back); });
expectFailure([&] { sceAgcFuseShaderHalves_0200(&fused, &halves.front, &halves.back, nullptr); });
{
Halves swapped;
check(sceAgcUnknownGetFusedShaderSize(&size, &swapped.back, &swapped.front) == InvalidShaderHalves, "swapped halves were accepted by the size query");
check(sceAgcUnknownFuseShaderHalves(&fused, &swapped.back, &swapped.front, nullptr) == InvalidShaderHalves, "swapped halves were fused");
check(sceAgcGetFusedShaderSize_0080(&size, &swapped.back, &swapped.front) == InvalidShaderHalves, "swapped halves were accepted by the size query");
check(sceAgcFuseShaderHalves_0200(&fused, &swapped.back, &swapped.front, nullptr) == InvalidShaderHalves, "swapped halves were fused");
}
{
Halves stages;
stages.backSpecials.vgt_shader_stages_en.value = 0;
std::vector<std::uint8_t> storage(4096);
check(sceAgcUnknownFuseShaderHalves(&fused, &stages.front, &stages.back, storage.data()) == InvalidShaderHalves, "halves with different GS stages were fused");
check(sceAgcFuseShaderHalves_0200(&fused, &stages.front, &stages.back, storage.data()) == InvalidShaderHalves, "halves with different GS stages were fused");
}
{
Halves noJump;
noJump.frontCode[2] = SNop;
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(&size, &noJump.front, &noJump.back); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noJump.front, &noJump.back); });
}
{
Halves noTrailer;
noTrailer.frontCode[5] = 0;
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(&size, &noTrailer.front, &noTrailer.back); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &noTrailer.front, &noTrailer.back); });
}
{
Halves hardware;
hardware.frontSh[2].value |= 1u << 16u;
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(&size, &hardware.front, &hardware.back); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &hardware.front, &hardware.back); });
}
{
Halves constants;
constants.back.embedded_constant_buffer_size_dqw = 1;
expectFailure([&] { sceAgcUnknownGetFusedShaderSize(&size, &constants.front, &constants.back); });
expectFailure([&] { sceAgcGetFusedShaderSize_0080(&size, &constants.front, &constants.back); });
}
}
@ -228,10 +230,10 @@ void testHullHalves() {
back.specials = &specials;
back.type = static_cast<std::uint8_t>(ShaderRegs::ShaderBinaryType::HsBack);
SizeAlign size{};
check(sceAgcUnknownGetFusedShaderSize(&size, &front, &back) == 0 && size.m_size == backSh.size() * sizeof(ShaderRegister) && size.m_align == 4, "hull size query changed");
check(sceAgcGetFusedShaderSize_0080(&size, &front, &back) == 0 && size.m_size == backSh.size() * sizeof(ShaderRegister) && size.m_align == 4, "hull size query changed");
std::vector<ShaderRegister> scratch(backSh.size());
Shader fused{};
check(sceAgcUnknownFuseShaderHalves(&fused, &front, &back, scratch.data()) == 0, "hull fusion failed");
check(sceAgcFuseShaderHalves_0200(&fused, &front, &back, scratch.data()) == 0, "hull fusion failed");
check(fused.type == static_cast<std::uint8_t>(ShaderRegs::ShaderBinaryType::Hs) && fused.code == back.code && fused.sh_registers == scratch.data() && fused.user_data == nullptr, "hull halves were not fused at the register level");
const auto address = reinterpret_cast<std::uint64_t>(front.code);
check(scratch[0].value == 0x11u && scratch[1].value == 0x22u, "hull checksums were not taken from the front half");
@ -244,9 +246,12 @@ void testHullHalves() {
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
testFusion(0);
testFusion(0xa0);
testFusion(4);
testFusion(0, SSwappcNullS6);
testRejections();
testHullHalves();
LibcRunShutdown_nid_postfix();

View File

@ -0,0 +1,129 @@
#include "SceShaders.hpp"
#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/general/VabiMacros.hpp"
#include <algorithm>
#include <array>
#include <span>
#include <stdexcept>
#include <iostream>
extern "C" int APS5_VABI sceAgcLinkShaders(ShaderRegister*, ShaderRegister*, const void*, const Shader*, const Shader*, std::uint32_t);
extern "C" int APS5_VABI sceAgcCreatePrimState(ShaderRegister*, ShaderRegister*, const Shader*, const Shader*, std::uint32_t);
extern "C" int APS5_VABI sceAgcCreateInterpolantMapping(ShaderRegister*, const Shader*, const Shader*);
namespace {
std::array<ShaderRegister, 2> preparedContext;
std::array<ShaderRegister, 3> preparedPrimitive;
std::size_t stageCount = 0;
unsigned preparations = 0;
unsigned mappings = 0;
unsigned links = 0;
bool rejectLink = false;
const Shader* mappedPixel = nullptr;
void Require(bool condition) {
if (!condition) throw std::runtime_error("shader helper preparation mismatch");
}
}
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
Require(context.size() == 2 && primitive.size() == 3);
std::copy(context.begin(), context.end(), preparedContext.begin());
std::copy(primitive.begin(), primitive.end(), preparedPrimitive.begin());
stageCount = stages.size();
++preparations;
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
Require(context.size() == 32 && primitive.empty());
mappedPixel = shader;
++mappings;
}
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
if (rejectLink) throw std::runtime_error("injected link failure");
Require(vertex != nullptr && pixel == mappedPixel && (primitiveType == 0 || primitiveType == 7));
++links;
}
int main() {
try {
using namespace ShaderRegs;
ShaderSpecialRegs special{};
special.vgt_shader_stages_en = {VGT_SHADER_STAGES_EN, 0};
special.vgt_gs_out_prim_type = {VGT_GS_OUT_PRIM_TYPE, 2};
special.ge_cntl = {GE_CNTL, 0x123};
special.ge_user_vgpr_en = {GE_USER_VGPR_EN, 7};
Shader vertex{};
vertex.type = static_cast<std::uint8_t>(ShaderBinaryType::Gs);
vertex.specials = &special;
Shader hull{};
hull.type = static_cast<std::uint8_t>(ShaderBinaryType::Hs);
hull.specials = &special;
Shader pixel{};
pixel.type = static_cast<std::uint8_t>(ShaderBinaryType::Ps);
ShaderSemantic input{};
input.semantic = 9;
pixel.input_semantics = &input;
std::array<ShaderRegister, 32> interpolants{};
for (const auto count : {0u, 1u}) {
pixel.num_input_semantics = count;
for (const bool primitiveFirst : {false, true}) {
for (unsigned outputs = 1; outputs <= 3; ++outputs) {
for (const auto* hs : {static_cast<const Shader*>(nullptr), static_cast<const Shader*>(&hull)}) {
preparations = mappings = links = 0;
std::array<ShaderRegister, 3> context{};
std::array<ShaderRegister, 4> primitive{};
context.back().value = primitive.back().value = 0xdeadbeef;
const auto prepare = [&] { Require(sceAgcCreatePrimState(outputs & 1 ? context.data() : nullptr, outputs & 2 ? primitive.data() : nullptr, hs, &vertex, 7) == 0); };
if (primitiveFirst) prepare();
Require(sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel) == 0);
if (!primitiveFirst) prepare();
Require(preparations == 1 && mappings == 1 && links == 1 && stageCount == (hs ? 2u : 1u));
Require(preparedContext[0].offset == VGT_SHADER_STAGES_EN && preparedContext[1].offset == VGT_GS_OUT_PRIM_TYPE);
Require(preparedPrimitive[0].value == 0x123 && preparedPrimitive[1].value == 7 && preparedPrimitive[2].value == 7);
Require(context.back().value == 0xdeadbeef && primitive.back().value == 0xdeadbeef);
for (unsigned i = 0; i < 2; ++i) Require(context[i].value == (outputs & 1 ? preparedContext[i].value : 0));
for (unsigned i = 0; i < 3; ++i) Require(primitive[i].value == (outputs & 2 ? preparedPrimitive[i].value : 0));
}
}
}
}
std::array<ShaderRegister, 34> linkedContext{};
std::array<ShaderRegister, 3> linkedPrimitive{};
for (auto& value : linkedContext) value.value = 0xdeadbeef;
for (auto& value : linkedPrimitive) value.value = 0xdeadbeef;
for (auto& value : interpolants) value.value = 0xdeadbeef;
rejectLink = true;
bool rejected = false;
try { sceAgcLinkShaders(linkedContext.data(), linkedPrimitive.data(), nullptr, &vertex, &pixel, 7); }
catch (const std::runtime_error&) { rejected = true; }
Require(rejected);
for (const auto value : linkedContext) Require(value.value == 0xdeadbeef);
for (const auto value : linkedPrimitive) Require(value.value == 0xdeadbeef);
rejected = false;
try { sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel); }
catch (const std::runtime_error&) { rejected = true; }
Require(rejected);
for (const auto value : interpolants) Require(value.value == 0xdeadbeef);
rejectLink = false;
pixel.num_input_semantics = 33;
rejected = false;
try { sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, &pixel); }
catch (const std::runtime_error&) { rejected = true; }
Require(rejected);
for (const auto value : interpolants) Require(value.value == 0xdeadbeef);
pixel.num_input_semantics = 1;
Require(sceAgcLinkShaders(linkedContext.data(), linkedPrimitive.data(), nullptr, &vertex, &pixel, 7) == 0);
preparations = mappings = links = 0;
Require(sceAgcCreatePrimState(nullptr, nullptr, nullptr, nullptr, 7) == 0);
Require(sceAgcCreateInterpolantMapping(interpolants.data(), &vertex, nullptr) == 0);
Require(preparations == 0 && mappings == 0 && links == 0);
std::cout << "shader helper preparation tests passed\n";
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}

View File

@ -126,6 +126,8 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderMemory.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/BdaFeatures.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/SubgroupClock.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ShaderDeviceProfile.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Pm4.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/VulkanDevice.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/AspectFit.cpp
@ -180,12 +182,12 @@ target_include_directories(libSceAgcDriver PRIVATE
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include
)
file(GLOB_RECURSE agcShaderCacheGuardSources CONFIGURE_DEPENDS
file(GLOB_RECURSE agcShaderCacheGuardSources CONFIGURE_DEPENDS RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/*.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/*.hpp"
"${CMAKE_CURRENT_SOURCE_DIR}/*.h"
)
list(FILTER agcShaderCacheGuardSources EXCLUDE REGEX "^${CMAKE_CURRENT_SOURCE_DIR}/(tests|tools)/")
list(FILTER agcShaderCacheGuardSources EXCLUDE REGEX "^(tests|tools)/")
add_shader_recompiler(shader_recompiler_agc_driver libc
GUARD_SOURCES
${agcShaderCacheGuardSources}
@ -202,6 +204,16 @@ set_target_properties(libSceAgcDriver PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(libSceAgcDriver)
if(BUILD_TESTING)
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)
configure_windows_unwind(agc_write_watch_coverage_tests)
add_test(NAME agc_write_watch_coverage COMMAND agc_write_watch_coverage_tests)
if(WIN32)
set_tests_properties(agc_write_watch_coverage PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(agc_profile_output_tests tests/ProfileOutput.cpp)
target_include_directories(agc_profile_output_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_profile_output_tests PRIVATE libc)
@ -229,6 +241,8 @@ add_test_executable(agc_driver_tests EXCLUDE_FROM_ALL
target_include_directories(agc_driver_tests PRIVATE
${LIBS_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/core/shader/recompiler
${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include
${CMAKE_SOURCE_DIR}/3rdparty/SPIRV-Headers/include
)
target_link_libraries(agc_driver_tests PRIVATE
@ -273,11 +287,35 @@ target_link_libraries(agc_structurizer_tests PRIVATE shader_recompiler_agc_drive
set_target_properties(agc_structurizer_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_structurizer_tests)
add_test_executable(agc_subgroup_clock_tests EXCLUDE_FROM_ALL
tests/SubgroupClock.cpp
Execution/src/SubgroupClock.cpp
)
target_include_directories(agc_subgroup_clock_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
set_target_properties(agc_subgroup_clock_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_subgroup_clock_tests)
if(BUILD_TESTING)
set_target_properties(agc_subgroup_clock_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_subgroup_clock COMMAND agc_subgroup_clock_tests)
endif()
add_test_executable(agc_spirv_module_tests EXCLUDE_FROM_ALL tests/SpirvModule.cpp)
target_link_libraries(agc_spirv_module_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_spirv_module_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_spirv_module_tests)
add_test_executable(agc_spirv_narrow_constant_store_tests EXCLUDE_FROM_ALL tests/SpirvNarrowConstantStore.cpp)
target_link_libraries(agc_spirv_narrow_constant_store_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_spirv_narrow_constant_store_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_spirv_narrow_constant_store_tests)
if(ANYPS5_ENABLE_SPIRV_TOOLS)
target_link_libraries(agc_spirv_narrow_constant_store_tests PRIVATE SPIRV-Tools-static)
add_test_executable(agc_spirv_optimizer_tests EXCLUDE_FROM_ALL tests/SpirvOptimizer.cpp)
target_link_libraries(agc_spirv_optimizer_tests PRIVATE shader_recompiler_agc_driver libc SPIRV-Tools-static)
set_target_properties(agc_spirv_optimizer_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_spirv_optimizer_tests)
endif()
add_test_executable(agc_dpp_write_tests EXCLUDE_FROM_ALL tests/DppWrite.cpp)
target_link_libraries(agc_dpp_write_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_dpp_write_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -288,6 +326,20 @@ target_link_libraries(agc_masked_select_tests PRIVATE shader_recompiler_agc_driv
set_target_properties(agc_masked_select_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_masked_select_tests)
add_test_executable(agc_wave_control_decode_tests EXCLUDE_FROM_ALL tests/WaveControlDecode.cpp)
target_include_directories(agc_wave_control_decode_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_wave_control_decode_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_wave_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_wave_control_decode_tests)
if(BUILD_TESTING)
set_target_properties(agc_wave_control_decode_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_wave_control_decode COMMAND agc_wave_control_decode_tests)
if(WIN32)
set_tests_properties(agc_wave_control_decode PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1ControlDecode.cpp)
target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF)
@ -307,6 +359,20 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_buffer_pool_tests EXCLUDE_FROM_ALL tests/BufferPool.cpp)
target_include_directories(agc_buffer_pool_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
target_link_libraries(agc_buffer_pool_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_buffer_pool_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_buffer_pool_tests)
if(BUILD_TESTING)
set_target_properties(agc_buffer_pool_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_buffer_pool COMMAND agc_buffer_pool_tests)
if(WIN32)
set_tests_properties(agc_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_display_buffer_dcc_tests EXCLUDE_FROM_ALL tests/DisplayBufferDcc.cpp)
target_include_directories(agc_display_buffer_dcc_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
target_link_libraries(agc_display_buffer_dcc_tests PRIVATE libSceAgcDriver libc)
@ -332,6 +398,12 @@ target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_
set_target_properties(agc_shader_disk_cache_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_disk_cache_tests)
add_test_executable(agc_shader_device_profile_tests EXCLUDE_FROM_ALL tests/ShaderDeviceProfile.cpp Execution/src/ShaderDeviceProfile.cpp)
target_include_directories(agc_shader_device_profile_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
target_link_libraries(agc_shader_device_profile_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_shader_device_profile_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_shader_device_profile_tests)
add_test_executable(agc_shader_control_flow_tests EXCLUDE_FROM_ALL
tests/ControlFlow.cpp
)
@ -370,10 +442,24 @@ add_custom_command(
VERBATIM
)
set(agcSampleArrayShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/SampleArray.comp)
set(agcSampleArrayShaderSpv ${agcTestShaderGeneratedRoot}/SampleArray.spv)
set(agcSampleArrayShaderHeader ${agcTestShaderGeneratedRoot}/SampleArray_spv.h)
add_custom_command(
OUTPUT "${agcSampleArrayShaderHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 -o "${agcSampleArrayShaderSpv}" "${agcSampleArrayShaderSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleArrayShaderSpv} -DOUTPUT=${agcSampleArrayShaderHeader}
-DSYMBOL=SAMPLE_ARRAY_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcSampleArrayShaderSource}" glslang-standalone
VERBATIM
)
add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
tests/Recorder.cpp
tests/ResidentPresent.cpp
${agcSampleLodShaderHeader}
${agcSampleArrayShaderHeader}
)
target_include_directories(agc_driver_recorder_tests PRIVATE
@ -392,6 +478,20 @@ target_link_libraries(agc_driver_recorder_tests PRIVATE
set_target_properties(agc_driver_recorder_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_recorder_tests)
add_test_executable(agc_recorder_kept_bytes_tests EXCLUDE_FROM_ALL tests/RecorderKeptBytes.cpp)
target_include_directories(agc_recorder_kept_bytes_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include)
target_link_libraries(agc_recorder_kept_bytes_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_recorder_kept_bytes_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_recorder_kept_bytes_tests)
if(BUILD_TESTING)
set_target_properties(agc_recorder_kept_bytes_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_recorder_kept_bytes COMMAND agc_recorder_kept_bytes_tests)
if(WIN32)
set_tests_properties(agc_recorder_kept_bytes PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_driver_pm4_tests EXCLUDE_FROM_ALL
tests/Pm4.cpp
)
@ -515,6 +615,7 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
${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(${agcExecutionTest} PRIVATE libSceAgcDriver SDL2-static libc)
set_target_properties(${agcExecutionTest} PROPERTIES CXX_EXTENSIONS OFF)
@ -522,17 +623,103 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
list(APPEND agcExecutionTests ${agcExecutionTest})
endforeach()
target_include_directories(agc_driver_prepared_exports_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>)
target_link_libraries(agc_driver_interpolation_modes_tests PRIVATE shader_recompiler_agc_driver)
target_include_directories(agc_driver_prepared_shaders_tests PRIVATE
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
)
if(BUILD_TESTING)
foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests ${agcExecutionTests})
add_test(NAME agc_shader_bindless COMMAND agc_shader_memory_tests --bindless)
set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_shader_bindless PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests ${agcExecutionTests})
if(agcTest STREQUAL "agc_driver_constant_store_tests")
continue()
endif()
string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}")
set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME ${agcTestName} COMMAND ${agcTest})
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE)
add_test(NAME agc_driver_sampled_cache_budget_memory COMMAND agc_driver_sampled_cache_budget_tests memory)
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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)
if(WIN32)
set_tests_properties(agc_driver_pm4_failure PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_flip_compute COMMAND agc_driver_flip_tests compute)
set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_flip_compute PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests)
if(NOT TARGET ${agcTest})
continue()
endif()
set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF)
string(REGEX REPLACE "_tests$" "" agcTestBaseName "${agcTest}")
foreach(agcSpirvMode IN ITEMS default full none)
if(agcSpirvMode STREQUAL "default")
set(agcTestName "${agcTestBaseName}")
set(agcSpirvEnvironment "--unset=APS5_SPIRV_OPT")
else()
set(agcTestName "${agcTestBaseName}_${agcSpirvMode}")
set(agcSpirvEnvironment "APS5_SPIRV_OPT=${agcSpirvMode}")
endif()
add_test(NAME ${agcTestName} COMMAND ${CMAKE_COMMAND} -E env
${agcSpirvEnvironment} "$<TARGET_FILE:${agcTest}>")
set_tests_properties(${agcTestName} PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 60 SKIP_RETURN_CODE 77)
if(agcTest STREQUAL "agc_driver_constant_store_tests")
set_tests_properties(${agcTestName} PROPERTIES RUN_SERIAL TRUE)
endif()
if(WIN32)
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endforeach()
foreach(preparedMode IN ITEMS indirect uncached debug debug_failure)
set(preparedTest "agc_driver_prepared_shaders_${preparedMode}")
if(preparedMode STREQUAL "indirect")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --indirect)
elseif(preparedMode STREQUAL "debug_failure")
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests --fail-before-registration)
set_tests_properties(${preparedTest} PROPERTIES WILL_FAIL TRUE TIMEOUT 30)
else()
add_test(NAME ${preparedTest} COMMAND agc_driver_prepared_shaders_tests)
endif()
set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(preparedMode STREQUAL "uncached")
set_property(TEST ${preparedTest} APPEND PROPERTY ENVIRONMENT "APS5_NO_DISPATCH_CACHE=1")
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()
if(WIN32)
set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endif()
option(AGC_BUILD_VISUAL_TEST "Enable the standalone AGC SPIR-V visual test" OFF)
@ -570,6 +757,8 @@ if(AGC_BUILD_VISUAL_TEST)
Execution/src/PresentationScaler.cpp
Execution/src/GuestMemory.cpp
Execution/src/BdaFeatures.cpp
Execution/src/SubgroupClock.cpp
Execution/src/ShaderDeviceProfile.cpp
Graphics/src/Draw.cpp
Graphics/src/DepthSurface.cpp
Graphics/src/ColorTargetLayout.cpp
@ -771,6 +960,14 @@ if(BUILD_TESTING)
set_tests_properties(scalar_saveexec PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(scalar_swappc_tests ${LIBS_INCLUDE_DIR}/tests/ScalarSwappc.cpp)
target_link_libraries(scalar_swappc_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(scalar_swappc_tests)
add_test(NAME scalar_swappc COMMAND scalar_swappc_tests)
if(WIN32)
set_tests_properties(scalar_swappc PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(scalar_bitcount64_tests ${LIBS_INCLUDE_DIR}/tests/ScalarBitCount64.cpp)
target_link_libraries(scalar_bitcount64_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(scalar_bitcount64_tests)
@ -787,6 +984,14 @@ if(BUILD_TESTING)
set_tests_properties(scalar_scratch_load PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(scalar_call_tests ${LIBS_INCLUDE_DIR}/tests/ScalarCall.cpp)
target_link_libraries(scalar_call_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(scalar_call_tests)
add_test(NAME scalar_call COMMAND scalar_call_tests)
if(WIN32)
set_tests_properties(scalar_call PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(image_sample_gather_tests ${LIBS_INCLUDE_DIR}/tests/ImageSampleGather.cpp)
target_link_libraries(image_sample_gather_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(image_sample_gather_tests)
@ -855,6 +1060,23 @@ if(BUILD_TESTING)
endif()
if(BUILD_TESTING)
add_test_executable(image_by_tests ${LIBS_INCLUDE_DIR}/tests/ImageBy.cpp)
target_link_libraries(image_by_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(image_by_tests)
add_test(NAME image_by COMMAND image_by_tests)
if(WIN32)
set_tests_properties(image_by PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(image_pck_n_tests ${LIBS_INCLUDE_DIR}/tests/ImagePckN.cpp)
target_link_libraries(image_pck_n_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(image_pck_n_tests)
add_test(NAME image_pck_n COMMAND image_pck_n_tests)
if(WIN32)
set_tests_properties(image_pck_n PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test_executable(image_msaa_load_tests ${LIBS_INCLUDE_DIR}/tests/ImageMsaaLoad.cpp)
target_link_libraries(image_msaa_load_tests PRIVATE shader_recompiler_agc_driver libc)
configure_windows_unwind(image_msaa_load_tests)

View File

@ -28,6 +28,8 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer);
std::vector<std::byte> DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span<const std::byte> source);
std::vector<std::byte> ReadDisplayBuffer(const DisplayBuffer& buffer);
std::array<std::byte, 4> DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys);
std::size_t DisplayBufferKeyBytes(const DisplayBuffer& buffer);
Graphics::DccKeys DisplayBufferKeys(const DisplayBuffer& buffer);
}

View File

@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_HPP
#include <span>
#include "SceTypes.hpp"
#include "SceShaders.hpp"
@ -16,5 +17,8 @@ extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix();
extern "C" void AgcDriverShutdown_nid_postfix();
extern "C" void AgcDriverSuspendPoint_nid_postfix();
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader);
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType);
#endif

View File

@ -82,6 +82,8 @@ struct DataMask {
enum class EntryOutcome { Equal, EqualData, Differing, Inaccessible, QueuedLabel, FlushingImage, PublishMoved, PendingMoved, ForgetMoved };
bool CacheableResult(const ShaderRecompiler::RecompileResult& compiled);
}
#endif

View File

@ -32,6 +32,15 @@ struct DrawDecode {
std::vector<ShaderRecompiler::ProgramRole> roles;
};
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment);
struct PreparedGraphicsStage {
std::shared_ptr<const ShaderSnapshot> snapshot;
PreparedShaders::Entry entry;
};
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target);
struct DrawRecipeRecord {
std::vector<std::weak_ptr<const DispatchVariant>> stages;
std::shared_ptr<const DrawRecipe> recipe;

View File

@ -53,9 +53,13 @@ public:
void SuspendPoint();
void RegisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output);
void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOutput>& output);
void AttachWindow(const PresentationWindow& window);
void Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context);
void ReleaseWindow(void* window);
void RegisterShader(const Shader* shader);
void ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
void ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive);
void ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType);
private:
friend class SampledReadScope;
@ -68,6 +72,7 @@ private:
static bool& onWorkerThread();
static void copyCommands(Submission& submission, const std::uint32_t* guest, std::size_t words);
static bool copySegment(Submission& submission, const std::uint32_t* guest, std::size_t words, std::size_t& budget);
static void readRegisterLists(Submission& submission);
void waitForFlipRoom(const Submission& submission);
void reserveOutputs(Submission& submission);
void executeRewindTail(const Submission& stalled);
@ -93,7 +98,7 @@ private:
void lookupDispatch(std::uint64_t address, const Submission& submission, std::uint64_t key, bool noDispatchCache, bool traceCache, bool profile, std::span<const ShaderRecompiler::MemoryRegion> memory, DispatchPhaseTiming& phaseTiming, std::array<double, DriverPhaseCount>& phaseMs, std::shared_ptr<const ShaderRecompiler::RecompileResult>& compiledResult, std::shared_ptr<DispatchVariant>& keepVariant, std::vector<ShaderRecompiler::MemoryRegion>& captured, std::vector<std::uint32_t>& liveWords, bool& dataHit, bool& cached, bool& validated, std::shared_ptr<DispatchEntry>& missedEntry, bool& missedDiffering);
void insertDispatch(std::uint64_t address, std::uint64_t key, bool noDispatchCache, bool profile, const std::shared_ptr<const ShaderSnapshot>& registeredShader, std::uint64_t forgetAtCapture, std::span<const ShaderRecompiler::MemoryRegion> memory, const std::shared_ptr<ShaderMemory>& shaderMemory, const std::vector<ShaderRecompiler::MemoryRegion>& captured, const std::shared_ptr<const ShaderRecompiler::ResourceCapture>& capture, const std::shared_ptr<const ShaderRecompiler::RecompileResult>& compiledResult, const std::shared_ptr<DispatchEntry>& missedEntry, bool missedDiffering, std::shared_ptr<DispatchVariant>& attachVariant, DispatchPhaseTiming& phaseTiming);
void dispatch(QueueState& queue, std::span<const std::uint32_t> packet, const Submission& submission, std::uint64_t indirectArguments = 0);
void verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, std::uint64_t deviceSerial, ShaderRecompiler::RecompileRequest request, std::span<const ShaderRecompiler::MemoryRegion> memory, std::uint64_t address, const DispatchVariant& variant, std::span<const std::uint32_t> liveWords, const ShaderRecompiler::RecompileResult& patched);
void verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, ShaderRecompiler::RecompileRequest request, std::span<const ShaderRecompiler::MemoryRegion> memory, std::uint64_t address, const DispatchVariant& variant, std::span<const std::uint32_t> liveWords, const ShaderRecompiler::RecompileResult& patched);
static bool drawEntries();
static bool verifyDrawEntries();
static bool registerKeyEnabled();
@ -110,7 +115,7 @@ private:
std::shared_ptr<DrawDecode> decodeDraw(const QueueState& queue, const Submission& submission);
void resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile);
void lookupDraw(const Submission& submission, const std::shared_ptr<VulkanDevice>& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr<DrawEntry>& entry, std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs);
ShaderRecompiler::RecompileResult compileDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected);
ShaderRecompiler::RecompileResult materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected);
void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming);
static bool drawPrecheck();
std::optional<DrawVerdict> precheckDraw(const QueueState& queue, const Submission& submission, std::span<const std::uint32_t> packet, const Pm4::DrawParameters& drawParameters, std::string& rejected, bool& traceIndirect);
@ -215,8 +220,8 @@ private:
void dumpSampleCounters(std::uint64_t address);
template <typename TWork>
static void timed(double WorkerProfile::*bucket, TWork&& work);
template <typename TWork>
void tolerate(const char* kind, TWork&& work);
FrameTiming* frameTiming();
FrameTiming* includeTimingSubmission(const Submission& submission, bool firstSegment);
void execute(const Submission& submission);
void markCompleted(std::uint64_t serial);
static const std::atomic<std::uint64_t>*& workerQueued();
@ -229,6 +234,7 @@ private:
std::map<std::uint32_t, QueueWorker> workers;
std::uint64_t frameSerial = 0;
std::shared_ptr<FrameTiming> pendingFrameTiming;
std::atomic<std::uint64_t> flipsCounted{0};
std::atomic<std::uint64_t> flipSerial{0};

View File

@ -26,13 +26,20 @@ struct Submission {
std::map<std::size_t, std::shared_ptr<IFlipRequest>> flips;
std::map<std::size_t, std::shared_ptr<IRenderingWait>> renderingWaits;
std::map<std::size_t, std::size_t> conditionalEnds;
std::map<std::size_t, std::vector<std::uint32_t>> registerLists;
bool suspend = false;
bool waitFree = false;
std::uint64_t received = 0;
std::vector<std::uint64_t> labelWrites;
std::set<std::size_t> heldAtSubmit;
std::chrono::steady_clock::time_point receivedAt{};
std::chrono::steady_clock::time_point copiedAt{};
std::chrono::steady_clock::time_point validatedAt{};
std::chrono::steady_clock::time_point roomReadyAt{};
std::chrono::steady_clock::time_point enqueuedAt{};
std::chrono::steady_clock::time_point dequeuedAt{};
std::chrono::steady_clock::time_point orderedAt{};
const std::uint32_t* rewindTail = nullptr;
std::size_t rewindWords = 0;
};

View File

@ -2,7 +2,9 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_SHADERREGISTRY_HPP
#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <array>
#include <optional>
#include <atomic>
#include <cstddef>
#include <cstdint>
@ -14,16 +16,45 @@
namespace AgcDriver::DriverDetail {
struct HandleMemos {
struct ShaderSnapshot;
using ShaderRegistry = std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>>;
struct PreparedShaderState {
struct Entry {
std::uint64_t key = 0;
std::size_t codeOffset;
std::shared_ptr<const ShaderRecompiler::SourceHandle> handle;
std::shared_ptr<const std::string> failure;
};
struct Rectangle {
std::uint64_t vertexId;
std::uint64_t fragmentId;
std::shared_ptr<const ShaderRecompiler::RectListShaders> shaders;
};
struct RectangleProgress {
std::weak_ptr<const ShaderSnapshot> fragment;
std::size_t vertexCount;
std::size_t fragmentCount;
};
struct GraphicsAbi {
std::vector<std::uint64_t> key;
std::weak_ptr<const ShaderRegistry> registry;
std::vector<std::weak_ptr<const ShaderSnapshot>> stages;
};
std::vector<Entry> entries;
std::vector<std::vector<std::uint64_t>> registeredAbis;
std::vector<GraphicsAbi> graphicsAbis;
std::vector<Rectangle> rectangles;
std::vector<RectangleProgress> rectangleProgress;
std::vector<std::weak_ptr<const ShaderSnapshot>> fragments;
bool rectangleRequested = false;
};
struct PreparedShaders : PreparedShaderState {
std::mutex mutex;
std::array<Entry, 8> entries;
std::size_t next = 0;
std::atomic<std::uint32_t> poisoned{0};
};
struct RegisteredShaderState {
Registers shader;
Registers context;
Registers userConfig;
};
struct ShaderSnapshot {
@ -32,17 +63,23 @@ struct ShaderSnapshot {
std::uint8_t type;
std::vector<std::uint32_t> code;
std::vector<std::byte> header;
std::unique_ptr<HandleMemos> handles = std::make_unique<HandleMemos>();
std::shared_ptr<PreparedShaders> prepared = std::make_shared<PreparedShaders>();
std::shared_ptr<const RegisteredShaderState> registeredState;
};
using ShaderRegistry = std::map<std::uint64_t, std::shared_ptr<const ShaderSnapshot>>;
bool FailureMemo();
std::shared_ptr<const ShaderSnapshot> ReadRawComputeShader(std::uint64_t address);
std::uint64_t NullPixelProgramAddress();
std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const ShaderSnapshot& snapshot);
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot);
std::shared_ptr<const ShaderRecompiler::SourceHandle> SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, std::uint64_t deviceSerial, const ShaderRecompiler::RecompileRequest& request, bool bypass, const std::string** poisoned = nullptr);
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target);
ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapshot, std::uint64_t vertexId, std::uint64_t fragmentId);
std::shared_ptr<const ShaderRecompiler::SourceHandle> SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request);
ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request);
}

View File

@ -92,6 +92,7 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati
// for every collect: nothing is reused without an ordering point. APS5_PACKET_EPOCH=1 makes the
// workers bump before every packet as before. CollectEpochBumps counts the bumps ([guestmem] line).
void BumpCollectEpoch();
std::uint64_t CollectEpoch();
std::uint64_t CollectEpochBumps();
std::uint64_t CollectWritesUncached(std::uint64_t address, std::size_t bytes);
// The tracker's current generation (every collect and MarkWritten bumps it): a stamp taken after

View File

@ -5,17 +5,15 @@
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <functional>
#include <iomanip>
#include <locale>
#include <map>
#include <memory>
#include <mutex>
#include <sstream>
#include <ostream>
#include <stdexcept>
#include <string_view>
#include <utility>
#include "prx/libc/include/general/LogMacros.hpp"
namespace AgcDriver {
@ -30,23 +28,68 @@ public:
std::uint64_t bytes = 0;
};
explicit FrameTiming(std::uint64_t id) : id(id) {}
explicit FrameTiming(std::uint64_t id, bool localMetrics = false) : localMetrics(localMetrics), id(id), workerStart(Clock::now()) {}
static FrameTiming* Preparation() {
if (!APS5_ENABLE_TIMING_LOG) return nullptr;
static FrameTiming timing(0);
return &timing;
}
static FrameTiming* Async() {
if (!APS5_ENABLE_TIMING_LOG) return nullptr;
static FrameTiming timing(0);
return &timing;
}
void CollectBackground() {
const auto collect = [&](FrameTiming* source, auto& destination) {
if (source == nullptr) return;
std::scoped_lock lock(mutex, source->mutex);
for (auto& [key, metric] : source->metrics) {
if (metric.count == 0) continue;
auto& target = destination[key];
target.total += metric.total;
target.maximum = std::max(target.maximum, metric.maximum);
target.count += metric.count;
target.bytes += metric.bytes;
metric = {};
}
};
collect(Preparation(), metrics);
collect(Async(), asyncMetrics);
}
Metric* Get(const char* scope, const char* stage) {
std::lock_guard lock(mutex);
std::unique_lock lock(mutex, std::defer_lock);
if (!localMetrics) lock.lock();
return &metrics[{scope, stage}];
}
void Add(Metric* metric, Clock::duration elapsed, std::uint64_t bytes = 0) {
std::lock_guard lock(mutex);
std::unique_lock lock(mutex, std::defer_lock);
if (!localMetrics) lock.lock();
metric->total += elapsed;
metric->maximum = std::max(metric->maximum, elapsed);
++metric->count;
metric->bytes += bytes;
}
void NotePacket(std::uint64_t serial, std::size_t offset, std::uint32_t opcode, std::size_t words, Clock::duration elapsed) {
std::unique_lock lock(mutex, std::defer_lock);
if (!localMetrics) lock.lock();
++packetCount;
packetWords += words;
if (elapsed > slowPacketTime) {
slowPacketTime = elapsed;
slowPacketSerial = serial;
slowPacketOffset = offset;
slowPacketOpcode = opcode;
}
}
void IncludeSubmission(std::uint64_t serial, Clock::time_point received, Clock::time_point enqueued, Clock::time_point dequeued, bool firstSegment) {
if (serial == 0 || received > enqueued || enqueued > dequeued) throw std::runtime_error("Frame timing: invalid submission timestamps");
if (serial == 0 || received == Clock::time_point{} || received > enqueued || enqueued > dequeued) throw std::runtime_error("Frame timing: invalid submission timestamps");
if (firstSerial == 0) {
firstSerial = serial;
start = received;
@ -66,6 +109,7 @@ public:
flipOffset = offset;
flipReceived = received;
flipReached = reached;
localMetrics = false;
}
Clock::time_point FlipReached() {
@ -107,39 +151,52 @@ public:
gpuGapMs += gapMs;
}
void Print(std::uint32_t outputHandle, std::int32_t buffer, std::int64_t argument, Clock::time_point finished, Clock::duration interval) {
std::function<void(std::ostream&)> Capture(std::uint32_t outputHandle, std::int32_t buffer, std::int64_t argument, Clock::time_point finished, Clock::duration interval) {
std::lock_guard lock(mutex);
if (firstSerial == 0 || flipSerial == 0) throw std::runtime_error("Frame timing: incomplete submission lineage");
std::ostringstream output;
output.imbue(std::locale::classic());
output << std::fixed << std::setprecision(3);
output << "[FrameTiming] frame=" << id << " submissions=" << firstSerial << ':' << lastSerial;
output << " output=" << outputHandle;
output << " flip=" << flipSerial << ':' << flipOffset << " buffer=" << buffer << " argument=" << argument;
output << " endpoint=flip_complete display_confirmed=0";
output << " first_submit_to_flip_ms=" << milliseconds(finished - start);
output << " flip_submit_to_complete_ms=" << milliseconds(finished - flipReceived);
output << " first_submit_to_execute_ms=" << milliseconds(executionStart - start);
output << " execute_to_flip_packet_ms=" << milliseconds(flipReached - executionStart);
output << " flip_packet_to_complete_ms=" << milliseconds(finished - flipReached);
auto accounted = Clock::duration::zero();
for (const auto scope : {"Driver.Packet", "Driver.Suspend", "Driver.Worker", "Driver.Completion"}) {
const auto it = metrics.find({scope, "total"});
if (it != metrics.end()) accounted += it->second.total;
}
output << " worker_unattributed_ms=" << milliseconds(flipReached - executionStart - accounted);
if (interval != Clock::duration::zero()) output << " flip_interval_ms=" << milliseconds(interval);
output << " batches_at_flip=" << batchesAtFlip << " unsignaled_at_flip=" << unsignaledAtFlip << " batches_at_blit=" << batchesAtBlit << " batches_after_flip=" << batchesAfterFlip;
output << " gpu_busy_ms=" << gpuBusyMs << " gpu_gap_ms=" << gpuGapMs;
output << " last_submit_after_flip_ms=" << milliseconds(lastSubmitAfterFlip) << " blit_submit_after_flip_ms=" << milliseconds(blitSubmitAfterFlip);
output << " metrics=inclusive(count,sum_ms,max_ms[,bytes])";
for (const auto& [key, metric] : metrics) {
if (metric.count == 0) continue;
output << ' ' << key.first << '.' << key.second << "=(" << metric.count << ',' << milliseconds(metric.total) << ',' << milliseconds(metric.maximum);
if (metric.bytes != 0) output << ',' << metric.bytes;
output << ')';
}
APS5_LOG_TIMING("%s", output.str().c_str());
return [asyncMetrics = asyncMetrics, metrics = metrics, id = id, firstSerial = firstSerial, lastSerial = lastSerial, flipSerial = flipSerial, flipOffset = flipOffset, workerStart = workerStart, start = start, executionStart = executionStart, flipReceived = flipReceived, flipReached = flipReached, batchesAtFlip = batchesAtFlip, unsignaledAtFlip = unsignaledAtFlip, batchesAtBlit = batchesAtBlit, batchesAfterFlip = batchesAfterFlip, gpuBusyMs = gpuBusyMs, gpuGapMs = gpuGapMs, lastSubmitAfterFlip = lastSubmitAfterFlip, blitSubmitAfterFlip = blitSubmitAfterFlip, packetCount = packetCount, packetWords = packetWords, slowPacketTime = slowPacketTime, slowPacketSerial = slowPacketSerial, slowPacketOffset = slowPacketOffset, slowPacketOpcode = slowPacketOpcode, outputHandle, buffer, argument, finished, interval](std::ostream& output) {
output << std::fixed << std::setprecision(3);
output << "[FrameTiming] frame=" << id << " submissions=" << firstSerial << ':' << lastSerial;
output << " output=" << outputHandle;
output << " flip=" << flipSerial << ':' << flipOffset << " buffer=" << buffer << " argument=" << argument;
output << " endpoint=flip_complete display_confirmed=0 worker_queue=0 background_metrics=completed_since_previous_flip";
output << " first_submit_at_ms=" << milliseconds(start.time_since_epoch());
output << " first_execute_at_ms=" << milliseconds(executionStart.time_since_epoch());
output << " flip_packet_at_ms=" << milliseconds(flipReached.time_since_epoch());
output << " flip_complete_at_ms=" << milliseconds(finished.time_since_epoch());
output << " first_submit_to_flip_ms=" << milliseconds(finished - start);
output << " flip_submit_to_complete_ms=" << milliseconds(finished - flipReceived);
output << " first_submit_to_execute_ms=" << milliseconds(executionStart - start);
output << " execute_to_flip_packet_ms=" << milliseconds(flipReached - executionStart);
output << " flip_packet_to_complete_ms=" << milliseconds(finished - flipReached);
auto accounted = Clock::duration::zero();
for (const auto scope : {"Driver.Packet", "Driver.Suspend", "Driver.WorkerWait", "Driver.Completion", "Driver.EndSubmission", "Driver.Rewind"}) {
const auto it = metrics.find({scope, "total"});
if (it != metrics.end()) accounted += it->second.total;
}
output << " worker_begin_at_ms=" << milliseconds(workerStart.time_since_epoch());
output << " worker_span_ms=" << milliseconds(flipReached - workerStart);
output << " worker_accounted_ms=" << milliseconds(accounted);
output << " worker_unattributed_ms=" << milliseconds(flipReached - workerStart - accounted);
if (interval != Clock::duration::zero()) output << " flip_interval_ms=" << milliseconds(interval);
output << " batches_at_flip=" << batchesAtFlip << " unsignaled_at_flip=" << unsignaledAtFlip << " batches_at_blit=" << batchesAtBlit << " batches_after_flip=" << batchesAfterFlip;
output << " gpu_busy_ms=" << gpuBusyMs << " gpu_gap_ms=" << gpuGapMs;
output << " last_submit_after_flip_ms=" << milliseconds(lastSubmitAfterFlip) << " blit_submit_after_flip_ms=" << milliseconds(blitSubmitAfterFlip);
output << " packet_count=" << packetCount << " packet_dwords=" << packetWords;
output << " slow_packet_submission=" << slowPacketSerial << " slow_packet_offset=" << slowPacketOffset << " slow_packet_opcode=" << slowPacketOpcode << " slow_packet_ms=" << milliseconds(slowPacketTime);
output << " metrics=inclusive(count,sum_ms,max_ms[,bytes])";
const auto writeMetrics = [&](const auto& entries, const char* prefix) {
for (const auto& [key, metric] : entries) {
if (metric.count == 0) continue;
output << ' ' << prefix << key.first << '.' << key.second << "=(" << metric.count << ',' << milliseconds(metric.total) << ',' << milliseconds(metric.maximum);
if (metric.bytes != 0) output << ',' << metric.bytes;
output << ')';
}
};
writeMetrics(metrics, "");
writeMetrics(asyncMetrics, "Async.");
output << '\n';
};
}
private:
@ -148,12 +205,21 @@ private:
}
std::mutex mutex;
bool localMetrics = false;
std::map<std::pair<std::string_view, std::string_view>, Metric> metrics;
std::map<std::pair<std::string_view, std::string_view>, Metric> asyncMetrics;
std::uint64_t packetCount = 0;
std::uint64_t packetWords = 0;
Clock::duration slowPacketTime{};
std::uint64_t slowPacketSerial = 0;
std::size_t slowPacketOffset = 0;
std::uint32_t slowPacketOpcode = 0;
std::uint64_t id;
std::uint64_t firstSerial = 0;
std::uint64_t lastSerial = 0;
std::uint64_t flipSerial = 0;
std::size_t flipOffset = 0;
Clock::time_point workerStart{};
Clock::time_point start{};
Clock::time_point executionStart{};
Clock::time_point flipReceived{};
@ -196,7 +262,7 @@ public:
if (frame != nullptr) {
total = frame->Get(scope, "total");
}
start = Clock::now();
if (frame != nullptr) start = Clock::now();
previous = start;
}
@ -204,10 +270,20 @@ public:
PerformanceTimer& operator=(const PerformanceTimer&) = delete;
~PerformanceTimer() {
Finish();
}
void Finish() {
if (frame == nullptr) return;
const auto end = Clock::now();
if (tail != nullptr) frame->Add(tail, end - previous);
frame->Add(total, end - start);
frame = nullptr;
}
void FinishPacket(std::uint64_t serial, std::size_t offset, std::uint32_t opcode, std::size_t words) {
if (frame != nullptr) frame->NotePacket(serial, offset, opcode, words, Clock::now() - start);
Finish();
}
void Mark(const char* stage, std::uint64_t bytes = 0) {
@ -225,8 +301,8 @@ private:
const char* scope;
FrameTiming::Metric* total = nullptr;
FrameTiming::Metric* tail = nullptr;
Clock::time_point start;
Clock::time_point previous;
Clock::time_point start{};
Clock::time_point previous{};
};
}

View File

@ -86,6 +86,9 @@ inline std::size_t PacketWords(std::uint32_t header) { return FillerPacket(heade
std::string_view UnsupportedReason(std::uint32_t header);
void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue);
void Execute(std::span<const std::uint32_t> packet, QueueState& queue);
inline bool IndirectRegisterOpcode(std::uint32_t opcode) { return opcode == 0x63 || opcode == 0x64 || opcode == 0x9f; }
std::vector<std::uint32_t> ReadIndirectRegisters(std::span<const std::uint32_t> packet);
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue);
bool AccessesMemory(std::uint32_t header);
// Whether an ACQUIRE_MEM packet asks only for GPU cache actions (no CPU-visible memory
// synchronization): such a packet needs a pipeline barrier, not a device drain.

View File

@ -25,6 +25,7 @@ struct PresentationWindow {
}
extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationWindow& window);
extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context);
extern "C" void AgcDriverPresentBuffer_nid_postfix(const AgcDriver::PresentationWindow& window, const AgcDriver::DisplayBuffer& buffer, void (*gpuReady)(void*), void* context);
extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window);

View File

@ -1,6 +1,7 @@
#pragma once
#include <array>
#include <atomic>
#include <condition_variable>
#include <cstdio>
#include <deque>
@ -24,12 +25,17 @@ public:
}
~ProfileOutput() {
Stop();
}
void Stop() {
std::lock_guard stopLock(stopMutex);
{
std::lock_guard lock(mutex);
stopping = true;
}
changed.notify_one();
worker.join();
if (worker.joinable()) worker.join();
}
bool Write(std::string text) {
@ -117,6 +123,7 @@ private:
std::function<void(std::string_view)> writer;
const std::size_t capacity;
std::mutex stopMutex;
std::mutex mutex;
std::condition_variable changed;
std::condition_variable idle;
@ -129,14 +136,21 @@ private:
std::thread worker;
};
inline std::atomic<ProfileOutput*> createdProfileOutput{nullptr};
inline ProfileOutput& ProfileOutput_nid_no_patch() {
static ProfileOutput output([](std::string_view text) {
if (std::fwrite(text.data(), 1, text.size(), stderr) != text.size() || std::fflush(stderr) != 0)
throw std::runtime_error("profile output write failed");
});
createdProfileOutput.store(&output, std::memory_order_release);
return output;
}
inline void StopProfileOutput_nid_no_patch() {
if (auto* output = createdProfileOutput.load(std::memory_order_acquire)) output->Stop();
}
template<typename... TArgs>
void ProfilePrint_nid_no_patch(const char* format, TArgs... args) {
ProfileOutput_nid_no_patch().Print(format, args...);

View File

@ -148,15 +148,18 @@ inline Registers InitialContextRegisters() {
{0xc, 0}, {0xd, 0x40004000}, {0x81, 0x80000000}, {0x82, 0x40004000},
{0x90, 0x80000000}, {0x91, 0x40004000},
{0x105, 0}, {0x106, 0}, {0x107, 0}, {0x108, 0},
{0x1b1, 0}, {0x1b6, 0}, {0x1c3, 0}, {0x1c4, 0}, {0x1c5, 0},
{0x1b1, 0}, {0x1b3, 0}, {0x1b4, 0}, {0x1b6, 0}, {0x1c3, 0}, {0x1c4, 0}, {0x1c5, 0},
{0x1ff, 0}, {0x292, 2}, {0x293, 0}, {0x29b, 0},
{0x2ce, 0}, {0x2d3, 0}, {0x2d5, 0}, {0x2d6, 0}, {0x2db, 0},
{0x2dc, 0xaa00}, {0x2e4, 0}, {0x2f8, 0}, {0x2f9, 0x2d},
{0x2dc, 0xaa00}, {0x2df, 0}, {0x2e4, 0}, {0x2f8, 0}, {0x2f9, 0x2d},
{0x30e, 0xffffffff}, {0x30f, 0xffffffff}, {0x313, 0x6000},
{0x318, 0}, {0x31b, 0}, {0x31c, 0}, {0x31d, 0},
{0x390, 0}, {0x3b0, 0}, {0x3b8, 0}
};
for (std::uint32_t i = 0; i < 8; ++i) result.emplace(0x1e0 + i, 0x20010001);
for (std::uint32_t i = 0; i < 8; ++i) {
result.emplace(0x1e0 + i, 0x20010001);
result.emplace(0x31c + 0xfu * i, 0);
}
for (std::uint32_t i = 0; i < 16; ++i) {
result.emplace(0x94 + 2 * i, 0x80000000);
result.emplace(0x95 + 2 * i, 0x40004000);

View File

@ -0,0 +1,31 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERDEVICEPROFILE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERDEVICEPROFILE_HPP
#include "Recompiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <string>
#include <vector>
namespace AgcDriver {
class ShaderDeviceProfile {
public:
ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& target, const VkDeviceCreateInfo& deviceInfo, const VkPhysicalDeviceLimits& limits);
ShaderDeviceProfile(const ShaderDeviceProfile&) = delete;
ShaderDeviceProfile& operator=(const ShaderDeviceProfile&) = delete;
ShaderRecompiler::SpirvTarget Target() const { return target; }
const VkPhysicalDeviceLimits& Limits() const { return limits; }
bool NullDescriptors() const { return nullDescriptors; }
private:
ShaderRecompiler::SpirvTarget target;
const VkPhysicalDeviceLimits limits;
std::vector<std::uint32_t> capabilities;
std::vector<std::string> extensions;
std::vector<std::string_view> extensionViews;
bool nullDescriptors = false;
};
}
#endif

View File

@ -14,6 +14,7 @@
namespace ShaderRecompiler {
struct SourceHandle;
class PreparedShaderInvocation;
}
namespace AgcDriver {
@ -61,6 +62,7 @@ public:
// (the driver's memoized ShaderRecompiler::ResolveSource result) the capture skips the source
// resolution.
std::shared_ptr<const ShaderRecompiler::ResourceCapture> Capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle = nullptr);
std::shared_ptr<const ShaderRecompiler::ResourceCapture> Capture(const ShaderRecompiler::PreparedShaderInvocation& invocation);
[[nodiscard]] std::vector<ShaderRecompiler::MemoryRegion> Regions() const;
// The page regions read since the previous call (or construction), a word read again
// included, the initial regions excluded: one stage's own reads on the draw path's shared
@ -70,6 +72,7 @@ public:
static void CountHandleMemo(bool hit);
private:
std::shared_ptr<const ShaderRecompiler::ResourceCapture> capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation);
static constexpr std::size_t PageBytes = 4096;
static constexpr std::size_t PageWords = PageBytes / sizeof(std::uint32_t);
@ -82,6 +85,7 @@ private:
};
static bool read(void* context, std::uint64_t address, std::uint32_t* value);
static bool accessible(void* context, std::uint64_t address, std::uint64_t bytes);
Page& page(std::uint64_t base);
// Regions given at construction (the registered shader's code and header), referenced as given:

View File

@ -0,0 +1,30 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATION_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATION_HPP
#include <memory>
namespace AgcDriver::DriverDetail {
struct ShaderSnapshot;
struct PreparedShaderState;
class ShaderPreparationTransaction {
public:
ShaderPreparationTransaction();
~ShaderPreparationTransaction();
ShaderPreparationTransaction(const ShaderPreparationTransaction&) = delete;
ShaderPreparationTransaction& operator=(const ShaderPreparationTransaction&) = delete;
PreparedShaderState& Edit(const ShaderSnapshot& snapshot);
const PreparedShaderState& Read(const ShaderSnapshot& snapshot) const;
void Commit();
private:
struct State;
std::unique_ptr<State> state;
State* root = nullptr;
bool committed = false;
};
}
#endif

View File

@ -0,0 +1,28 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SHADERPREPARATIONSCOPE_HPP
namespace AgcDriver::DriverDetail {
class ShaderPreparationTransaction;
}
extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix();
extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction);
extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept;
namespace AgcDriver {
class ShaderPreparationScope {
public:
ShaderPreparationScope() : transaction(AgcDriverBeginShaderPreparation_nid_postfix()) {}
~ShaderPreparationScope() { AgcDriverEndShaderPreparation_nid_postfix(transaction); }
ShaderPreparationScope(const ShaderPreparationScope&) = delete;
ShaderPreparationScope& operator=(const ShaderPreparationScope&) = delete;
void Commit() { AgcDriverCommitShaderPreparation_nid_postfix(transaction); }
private:
DriverDetail::ShaderPreparationTransaction* transaction;
};
}
#endif

View File

@ -0,0 +1,13 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPCLOCK_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPCLOCK_HPP
#include <vulkan/vulkan.h>
#include <string_view>
namespace AgcDriver {
bool NarrowSubgroupClock(VkDriverId driver, std::string_view deviceName);
}
#endif

View File

@ -161,6 +161,7 @@ public:
bool Presentable() const;
bool PrimitiveListRestart() const;
bool SamplerFilterMinmax() const;
bool ConservativeRasterization() const;
// A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it
// touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command
// buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires
@ -299,6 +300,7 @@ 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;
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);

View File

@ -0,0 +1,85 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WRITEWATCHCOVERAGE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_WRITEWATCHCOVERAGE_HPP
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <vector>
#include <limits>
#include <unordered_map>
namespace AgcDriver::GuestMemory {
class WriteWatchCoverage {
public:
void Initialize(std::uint64_t address, std::size_t bytes) {
base = address;
size = bytes;
excluded.assign(bytes / BlockBytes + (bytes % BlockBytes != 0), 0);
excludedGenerations.clear();
}
bool Covers(std::uint64_t address, std::size_t bytes) const {
if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false;
const auto first = (address - base) / BlockBytes;
const auto last = (address - base + bytes - 1) / BlockBytes;
return std::none_of(excluded.begin() + first, excluded.begin() + last + 1, [](auto generation) { return generation != 0; });
}
bool Exclude(std::uint64_t address, std::size_t bytes, std::uint64_t generation = 1) {
if (bytes == 0 || size == 0) return false;
if (address < base) {
if (base - address >= bytes) return false;
bytes -= base - address;
address = base;
}
const auto offset = address - base;
if (offset >= size) return false;
bytes = static_cast<std::size_t>(std::min<std::uint64_t>(bytes, size - offset));
const auto first = offset / BlockBytes;
const auto last = (offset + bytes - 1) / BlockBytes;
bool changed = false;
for (auto block = first; block <= last; ++block) {
changed = changed || excluded[block] == 0;
auto& stamp = excludedGenerations[block];
stamp = std::max(stamp, std::max<std::uint64_t>(generation, 1));
excluded[block] = 1;
}
return changed;
}
bool Restore(std::uint64_t address, std::size_t bytes, std::uint64_t generation = std::numeric_limits<std::uint64_t>::max()) {
if (bytes == 0 || size == 0) return false;
if (address < base) {
if (base - address >= bytes) return false;
bytes -= base - address;
address = base;
}
const auto offset = address - base;
if (offset >= size) return false;
bytes = static_cast<std::size_t>(std::min<std::uint64_t>(bytes, size - offset));
const auto first = offset / BlockBytes + (offset % BlockBytes != 0);
const auto stop = offset + bytes;
const auto end = stop / BlockBytes + (stop == size && stop % BlockBytes != 0);
bool changed = false;
for (auto block = first; block < end; ++block) {
if (excluded[block] != 0 && excludedGenerations.at(block) < generation) {
changed = true;
excluded[block] = 0;
excludedGenerations.erase(block);
}
}
return changed;
}
private:
static constexpr std::size_t BlockBytes = 65536;
std::uint64_t base = 0;
std::size_t size = 0;
std::vector<std::uint8_t> excluded;
std::unordered_map<std::uint64_t, std::uint64_t> excludedGenerations;
};
}
#endif

View File

@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include <algorithm>
#include <array>
#include <chrono>
#include <cstdio>
@ -10,6 +11,7 @@
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
namespace AgcDriver {
namespace {
@ -50,6 +52,40 @@ void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBi
pixel[3] = texel[3];
}
constexpr std::uint32_t MetaBlockEdge = 512;
constexpr std::size_t MetaBlockKeys = 4096;
constexpr std::uint32_t KeyEdge = 8;
std::array<std::uint32_t, 2> displayableKeyPixel(const DisplayBuffer& buffer, std::size_t key) {
const auto pitch = (buffer.width + MetaBlockEdge - 1u) / MetaBlockEdge;
const auto block = static_cast<std::uint32_t>(key / MetaBlockKeys);
const auto offset = static_cast<std::uint32_t>(key % MetaBlockKeys);
std::uint32_t x = 0;
std::uint32_t y = 0;
for (std::uint32_t bit = 0; bit < 6; ++bit) {
x |= ((offset >> (2u * bit)) & 1u) << bit;
y |= ((offset >> (2u * bit + 1u)) & 1u) << bit;
}
return {block % pitch * MetaBlockEdge + x * KeyEdge, block / pitch * MetaBlockEdge + y * KeyEdge};
}
void traceDisplayKeys(const DisplayBuffer& buffer, const std::uint8_t* keys, std::size_t count) {
static const bool trace = std::getenv("APS5_TRACE_DCC_KEYS") != nullptr;
if (!trace) return;
constexpr std::size_t MaxRuns = 256;
std::string runs;
std::size_t begin = 0;
for (std::size_t run = 0; begin < count && run < MaxRuns; ++run) {
const auto end = static_cast<std::size_t>(std::find_if(keys + begin, keys + count, [&](std::uint8_t key) { return key != keys[begin]; }) - keys);
char text[32];
std::snprintf(text, sizeof(text), "%s%02x x%zu", run == 0 ? "" : ", ", keys[begin], end - begin);
runs += text;
begin = end;
}
if (begin < count) runs += ", ...";
std::fprintf(stderr, "[dcc-keys] display keys 0x%llx+0x%zx: %s\n", static_cast<unsigned long long>(buffer.dccAddress), count, runs.c_str());
}
}
std::size_t DisplayBufferSize(const DisplayBuffer& buffer) {
@ -117,6 +153,35 @@ std::array<std::byte, 4> DisplayBufferClearPixel(const DisplayBuffer& buffer, Gr
return pixel;
}
std::size_t DisplayBufferKeyBytes(const DisplayBuffer& buffer) {
const auto bytes = DisplayBufferSize(buffer);
require(buffer.tilingMode == 0, "VideoOut: a linear display buffer has no DCC keys");
return Graphics::DccKeyCount(Graphics::TextureTileMode::kR64KBX, 4, buffer.width, buffer.height, bytes);
}
Graphics::DccKeys DisplayBufferKeys(const DisplayBuffer& buffer) {
const auto count = DisplayBufferKeyBytes(buffer);
require(buffer.dccAddress != 0, "VideoOut: a display buffer without DCC metadata has no keys");
const auto keys = Graphics::CurrentDccKeys(buffer.dccAddress, DisplayBufferSize(buffer), count);
if (keys == Graphics::DccKeys::Uncompressed || Graphics::IsDccClear(keys)) return keys;
char differing[128] = "";
if (keys == Graphics::DccKeys::Mixed && GuestMemory::Accessible(reinterpret_cast<const void*>(buffer.dccAddress), count)) {
const auto* bytes = reinterpret_cast<const std::uint8_t*>(buffer.dccAddress);
traceDisplayKeys(buffer, bytes, count);
const auto* other = std::find_if(bytes + 1, bytes + count, [&](std::uint8_t key) { return key != bytes[0]; });
if (other == bytes + count) {
std::snprintf(differing, sizeof(differing), ": every key is 0x%02x", bytes[0]);
} else {
const auto key = static_cast<std::size_t>(other - bytes);
const auto [x, y] = displayableKeyPixel(buffer, key);
std::snprintf(differing, sizeof(differing), ": key %zu (pixels %u..%u x %u..%u in the displayable order) is 0x%02x, key 0 is 0x%02x", key, x, x + KeyEdge - 1u, y, y + KeyEdge - 1u, *other, bytes[0]);
}
}
char message[384];
std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads %s DCC keys at 0x%llx over its 0x%zx key bytes%s: presenting DCC metadata that is not uniformly uncompressed or fast-cleared is not implemented", static_cast<unsigned long long>(buffer.address), Graphics::DccKeysName(keys), static_cast<unsigned long long>(buffer.dccAddress), count, differing);
throw std::runtime_error(message);
}
std::vector<std::byte> ReadDisplayBuffer(const DisplayBuffer& buffer) {
const auto size = DisplayBufferSize(buffer);
static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr;

View File

@ -1,22 +1,23 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <cstdlib>
#include <cstring>
namespace AgcDriver::DriverDetail {
void Driver::verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, std::uint64_t deviceSerial, ShaderRecompiler::RecompileRequest request, std::span<const ShaderRecompiler::MemoryRegion> memory, std::uint64_t address, const DispatchVariant& variant, std::span<const std::uint32_t> liveWords, const ShaderRecompiler::RecompileResult& patched) {
void Driver::verifyDataHit(const ShaderSnapshot& snapshot, std::size_t codeOffset, ShaderRecompiler::RecompileRequest request, std::span<const ShaderRecompiler::MemoryRegion> memory, std::uint64_t address, const DispatchVariant& variant, std::span<const std::uint32_t> liveWords, const ShaderRecompiler::RecompileResult& patched) {
const auto fail = [&](const char* what, std::size_t position, std::size_t slot) {
std::fprintf(stderr, "[dispatch-cache] APS5_VERIFY_DATA_HITS: %s disagrees (program 0x%llx, position %zu, slot %zu)\n", what, static_cast<unsigned long long>(address), position, slot);
std::fflush(stderr);
std::abort();
};
auto verifyMemory = std::make_shared<ShaderMemory>(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter());
const auto handle = SourceHandleFor(snapshot, codeOffset, deviceSerial, request, false);
const auto capture = verifyMemory->Capture(request, handle.get());
const auto invocation = InvocationFor(snapshot, codeOffset, request);
const auto capture = verifyMemory->Capture(invocation);
const auto regions = verifyMemory->Regions();
request.context.memory = regions;
const auto result = ShaderRecompiler::Recompile(request, *capture);
const auto result = invocation.Materialize(*capture);
std::size_t position = 0, run = 0;
for (const auto& region : regions) {
const bool registered = std::any_of(memory.begin(), memory.end(), [&](const auto& known) { return region.guestAddress >= known.guestAddress && region.guestAddress < known.guestAddress + known.bytes.size(); });

View File

@ -1,4 +1,5 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
@ -11,6 +12,7 @@
namespace AgcDriver::DriverDetail {
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);
auto it = submission.shaders->upper_bound(address);
std::shared_ptr<const ShaderSnapshot> registeredShader;
@ -26,7 +28,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
for (std::uint32_t i = 0; i < userCount; ++i) {
userData.push_back(readUserData(queue.shader, 0x240 + i));
}
auto compute = Graphics::DecodeComputeStageInfo(queue.shader);
auto compute = Graphics::DecodeComputeStageInfo(queue.shader, snapshot.header);
std::vector<ShaderRecompiler::MemoryRegion> memory{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}};
if (!snapshot.header.empty()) memory.push_back({snapshot.headerAddress, snapshot.header});
@ -65,7 +67,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
}
ShaderRecompiler::RecompileRequest request{
{ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header},
{(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory},
{(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory, RegisteredFloatMode(snapshot)},
localDevice->ComputeTarget((packet[4] & 0x8000u) != 0 ? 32u : 64u),
{0, 0, 0, 128}
};
@ -98,13 +100,11 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
if (probeThis) std::fprintf(stderr, "[gpu] probing dispatch %llu of 0x%llx\n", static_cast<unsigned long long>(probeDispatch.second), static_cast<unsigned long long>(address));
}
if (FailureMemo() && snapshot.handles->poisoned.load(std::memory_order_relaxed) != 0) {
const std::string* poisoned = nullptr;
if (SourceHandleFor(snapshot, codeOffset, localDevice->Serial(), request, probeThis, &poisoned) == nullptr && poisoned != nullptr) {
pendingDispatchPhases().outcome = DispatchOutcome::SkippedMemo;
return;
}
}
struct ProbeScope {
bool active;
explicit ProbeScope(bool active) : active(active) { if (active) ShaderRecompiler::SetDebugProbeActive(true); }
~ProbeScope() { if (active) ShaderRecompiler::SetDebugProbeActive(false); }
} probeScope{probeThis};
const bool noDispatchCache = noDispatchCacheEnv || probeThis;
std::uint64_t key = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) {
@ -179,10 +179,13 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
previous.key = key;
}
if (!stampValidate()) mix(reinterpret_cast<std::uintptr_t>(registeredShader.get()));
mix(reinterpret_cast<std::uintptr_t>(registeredShader.get()));
phaseTiming.Phase(PhaseKey);
timing.Mark("decode_key_device_setup");
lookupDispatch(address, submission, key, noDispatchCache, traceCache, profile, memory, phaseTiming, phaseMs, compiledResult, keepVariant, captured, liveWords, dataHit, cached, validated, missedEntry, missedDiffering);
timing.Mark("lookup_cache");
if (cached) {
require(keepVariant != nullptr && keepVariant->shader == registeredShader, "dispatch cache belongs to another registered shader");
captureMs += phaseTiming.Elapsed();
} else {
shaderMemory = std::make_shared<ShaderMemory>(memory, &queryPendingWrite, &observePendingWrite, hookWaitCounter());
@ -190,18 +193,14 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr;
try {
const auto invocation = InvocationFor(snapshot, codeOffset, request);
timing.Mark("prepared_invocation");
struct ProbeScope {
bool active;
explicit ProbeScope(bool active) : active(active) { if (active) ShaderRecompiler::SetDebugProbeActive(true); }
~ProbeScope() { if (active) ShaderRecompiler::SetDebugProbeActive(false); }
} probeScope{probeThis};
const auto waitedBefore = traceCapSync() ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
forgetAtCapture = GuestMemory::ForgetSerial();
const auto handle = SourceHandleFor(snapshot, codeOffset, localDevice->Serial(), request, probeThis);
capture = [&] {
const SampledReadScope sampling(evidenceReads);
return shaderMemory->Capture(request, handle.get());
return shaderMemory->Capture(invocation);
}();
captured = shaderMemory->Regions();
request.context.memory = captured;
@ -211,10 +210,11 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
if (dumpShaders) static_cast<void>(dumpRequest(address, request));
const auto started = std::chrono::steady_clock::now();
static const bool reuseCapture = std::getenv("APS5_NO_CAPTURE_REUSE") == nullptr;
bool memoHit = false;
compiledResult = reuseCapture ? ShaderRecompiler::Recompile(request, *capture, &memoHit) : std::make_shared<const ShaderRecompiler::RecompileResult>(ShaderRecompiler::Recompile(request));
if (compiledResult->cacheHit || memoHit) ++cacheHits;
timing.Mark("capture_resources");
compiledResult = invocation.Materialize(*capture);
timing.Mark("materialize");
if (compiledResult->cacheHit) ++cacheHits;
const auto elapsed = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count();
static double totalMs = 0;
totalMs += elapsed;
@ -231,9 +231,9 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
}
recompileMs += phaseTiming.Elapsed();
phaseTiming.Phase(PhaseRecompile);
insertDispatch(address, key, noDispatchCache, profile, registeredShader, forgetAtCapture, memory, shaderMemory, captured, capture, compiledResult, missedEntry, missedDiffering, attachVariant, phaseTiming);
insertDispatch(address, key, noDispatchCache || !CacheableResult(*compiledResult), profile, registeredShader, forgetAtCapture, memory, shaderMemory, captured, capture, compiledResult, missedEntry, missedDiffering, attachVariant, phaseTiming);
}
if (verifyDataHits() && dataHit) verifyDataHit(snapshot, codeOffset, localDevice->Serial(), request, memory, address, *keepVariant, liveWords, *compiledResult);
if (verifyDataHits() && dataHit) verifyDataHit(snapshot, codeOffset, request, memory, address, *keepVariant, liveWords, *compiledResult);
if (recordQueuedLabelsAfterCapture(submission.queue, captured)) {
dispatch(queue, packet, submission, indirectArguments);

View File

@ -6,6 +6,10 @@
namespace AgcDriver::DriverDetail {
bool CacheableResult(const ShaderRecompiler::RecompileResult& compiled) {
return compiled.poisonedSrtReads == 0u;
}
void Driver::insertDispatch(std::uint64_t address, std::uint64_t key, bool noDispatchCache, bool profile, const std::shared_ptr<const ShaderSnapshot>& registeredShader, std::uint64_t forgetAtCapture, std::span<const ShaderRecompiler::MemoryRegion> memory, const std::shared_ptr<ShaderMemory>& shaderMemory, const std::vector<ShaderRecompiler::MemoryRegion>& captured, const std::shared_ptr<const ShaderRecompiler::ResourceCapture>& capture, const std::shared_ptr<const ShaderRecompiler::RecompileResult>& compiledResult, const std::shared_ptr<DispatchEntry>& missedEntry, bool missedDiffering, std::shared_ptr<DispatchVariant>& attachVariant, DispatchPhaseTiming& phaseTiming) {
if (!noDispatchCache) {
auto fresh = std::make_shared<DispatchVariant>();

View File

@ -91,7 +91,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (!graphics.stages.mesh) return;
auto& words = programs.front().userData;
require(programs.front().firstUserSgpr == 0 && words.size() >= ShaderRecompiler::MeshIndexBufferUserWord + 4, "mesh program lacks the hidden user words");
const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters, programs.front().binary.codeAddress);
const auto descriptor = Graphics::MeshIndexBufferDescriptor(parameters);
std::copy(descriptor.begin(), descriptor.end(), words.begin() + ShaderRecompiler::MeshIndexBufferUserWord);
};
if (!drawParameters.indirect) setMeshIndexBuffer(drawParameters);
@ -204,7 +204,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
memory.insert(memory.end(), matchedRegions[i].begin(), matchedRegions[i].end());
} else {
resultIndex[i] = results.size();
results.push_back(compileDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected));
results.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[i] = &results.back();
}
@ -234,7 +234,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
const auto buildRectList = [&] {
phaseTiming.Phase(DrawRowVectors);
require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs");
auto rectangle = ShaderRecompiler::BuildRectListShaders(*programResults[0], *programResults[1], localDevice->Target());
auto rectangle = PreparedRectangle(*programs[0].snapshot, programResults[0]->variantId, programResults[1]->variantId);
if (rectListBuilt) {
results[rectIndex] = std::move(rectangle.control);
results[rectIndex + 1] = std::move(rectangle.evaluation);
@ -329,7 +329,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
auto& result = results[resultIndex[programIndex]];
const auto pushBytes = result.pushConstants.size();
decodeVertexInfo(programIndex);
result = compileDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected);
result = materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected);
if (!rejected.empty()) return DrawVerdict::Rejected;
require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout");
}

View File

@ -1,38 +1,37 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <cstdlib>
#include <cstring>
namespace AgcDriver::DriverDetail {
ShaderRecompiler::RecompileResult Driver::compileDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected) {
ShaderRecompiler::RecompileResult Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected) {
PerformanceTimer timing("Shader.DrawStage");
using Stage = ShaderRecompiler::ShaderStage;
phaseTiming.Phase(DrawRowVectors);
const auto& program = programs[i];
const auto waveSize = program.binary.stage == Stage::Fragment ? graphics.stages.fragmentWaveSize : graphics.stages.vertexWaveSize;
ShaderRecompiler::RecompileRequest request{
program.binary,
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory},
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)},
localDevice->Target(),
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushConstantBytes) - pushOffset},
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}}
};
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
const std::string* poisoned = nullptr;
const auto handle = SourceHandleFor(*program.snapshot, program.codeOffset, localDevice->Serial(), request, false, FailureMemo() ? &poisoned : nullptr);
if (handle == nullptr && poisoned != nullptr) {
rejected = *poisoned;
return {};
}
const auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request);
timing.Mark("prepared_invocation");
auto& stageCapture = stageCaptures[i];
stageCapture.forgetSerial = GuestMemory::ForgetSerial();
stageCapture.pushOffset = pushOffset;
const auto capture = [&] {
const SampledReadScope sampling(evidenceReads);
return shaderMemory.Capture(request, handle.get());
return shaderMemory.Capture(invocation);
}();
stageCapture.regions = shaderMemory.TakeRecentRegions();
@ -73,10 +72,11 @@ ShaderRecompiler::RecompileResult Driver::compileDrawStage(std::size_t i, std::u
}
}
static const bool reuseCapture = std::getenv("APS5_NO_CAPTURE_REUSE") == nullptr;
phaseTiming.Phase(DrawRowCapture);
stageCapture.compiled = reuseCapture ? ShaderRecompiler::Recompile(request, *capture) : std::make_shared<const ShaderRecompiler::RecompileResult>(ShaderRecompiler::Recompile(request));
timing.Mark("capture_resources");
stageCapture.compiled = invocation.Materialize(*capture);
timing.Mark("materialize");
ShaderRecompiler::RecompileResult result = *stageCapture.compiled;
phaseTiming.Phase(DrawRowRecompile);
return result;
@ -88,7 +88,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP
std::uint64_t unstable = 0, mismatches = 0;
for (std::size_t i = 0; i < programs.size(); ++i) {
const auto& stageCapture = stageCaptures[i];
if (stageCapture.compiled == nullptr) continue;
if (stageCapture.compiled == nullptr || !CacheableResult(*stageCapture.compiled)) continue;
auto variant = std::make_shared<DispatchVariant>();
variant->compiled = stageCapture.compiled;
variant->shader = programs[i].snapshot;

View File

@ -5,27 +5,38 @@
namespace AgcDriver::DriverDetail {
std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Submission& submission) {
namespace {
std::uint32_t ReadGraphicsRegister(const Registers& registers, std::uint32_t offset) {
const auto found = registers.find(offset);
return found == registers.end() ? 0u : found->second;
}
}
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment) {
using Stage = ShaderRecompiler::ShaderStage;
using Role = ShaderRecompiler::ProgramRole;
const auto programAddress = [&](std::uint32_t base) {
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, base);
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, base + 1);
const auto high = readRegister(queue.shader, base + 1);
const auto high = ReadGraphicsRegister(queue.shader, base + 1);
require((high & ~0xffu) == 0, "reserved graphics program address bits are set");
return (static_cast<std::uint64_t>(readRegister(queue.shader, base)) << 8u) | (static_cast<std::uint64_t>(high) << 40u);
return (static_cast<std::uint64_t>(ReadGraphicsRegister(queue.shader, base)) << 8u) | (static_cast<std::uint64_t>(high) << 40u);
};
const bool pixelSkipped = Graphics::PixelProgramSkipped(queue);
const auto prepare = [&](std::uint64_t address, std::uint8_t type, Stage stage, std::uint32_t rsrc2, std::uint32_t userDataBase) {
const bool nullPixel = address == 0 && stage == Stage::Fragment;
const bool nullPixel = stage == Stage::Fragment && (address == 0 || pixelSkipped);
if (nullPixel) address = NullPixelProgramAddress();
auto it = submission.shaders->upper_bound(address);
require(it != submission.shaders->begin(), "graphics program does not belong to a registered shader");
auto it = registry.upper_bound(address);
require(it != registry.begin(), "graphics program does not belong to a registered shader");
--it;
const auto& snapshot = *it->second;
require(address - snapshot.codeAddress < snapshot.code.size() * sizeof(std::uint32_t), "graphics program is outside registered shader code");
require((address - snapshot.codeAddress) % sizeof(std::uint32_t) == 0, "graphics entry point is not dword aligned");
require(snapshot.type == type, "graphics program refers to an incompatible shader binary type");
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, rsrc2);
const auto resources = nullPixel && !queue.shader.contains(rsrc2) ? 0u : readRegister(queue.shader, rsrc2);
const auto resources = nullPixel && !queue.shader.contains(rsrc2) ? 0u : ReadGraphicsRegister(queue.shader, rsrc2);
const auto userCount = ((resources >> 1u) & 0x1fu) | (((resources >> 27u) & 1u) << 5u);
require(userCount <= 32, "graphics user SGPR count exceeds the register bank");
const auto codeOffset = static_cast<std::size_t>((address - snapshot.codeAddress) / sizeof(std::uint32_t));
@ -40,13 +51,12 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
};
for (std::uint32_t i = 0; i < userCount; ++i) {
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, userDataBase + i);
result.userData.push_back(readUserData(queue.shader, userDataBase + i));
result.userData.push_back((staticAbi ? ReadGraphicsRegister(queue.shader, userDataBase + i) : readUserData(queue.shader, userDataBase + i)));
}
return result;
};
{
auto product = std::make_shared<DrawDecode>();
product->state = Graphics::DecodeState(queue);
auto* product = &decoded;
auto& programs = product->programs;
auto& roles = product->roles;
programs.reserve(5);
@ -60,9 +70,10 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
program.userData.insert(program.userData.begin(), 8, 0);
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase);
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, pointerBase + 1);
if (staticAbi) return;
if (!pointerRequired && !queue.shader.contains(pointerBase) && !queue.shader.contains(pointerBase + 1)) return;
const auto low = readRegister(queue.shader, pointerBase);
const auto high = readRegister(queue.shader, pointerBase + 1);
const auto low = ReadGraphicsRegister(queue.shader, pointerBase);
const auto high = ReadGraphicsRegister(queue.shader, pointerBase + 1);
const auto address = static_cast<std::uint64_t>(low) | (static_cast<std::uint64_t>(high) << 32u);
require(address != 0 || !pointerRequired, "merged shader user-data address is null");
if (address == 0) return;
@ -78,8 +89,8 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
append(0x0c8, 2, Stage::TessellationEvaluation, 0x08b, 0x08c, Role::Domain);
} else if (graphics.stages.path == Graphics::ShaderPath::Geometry) {
const auto frontAddress = programAddress(0xc8);
auto snapshot = submission.shaders->upper_bound(frontAddress);
require(snapshot != submission.shaders->begin(), "geometry front program is not registered");
auto snapshot = registry.upper_bound(frontAddress);
require(snapshot != registry.begin(), "geometry front program is not registered");
--snapshot;
const auto type = snapshot->second->type;
require(type == 2 || type == 4, "invalid geometry front binary type");
@ -89,18 +100,25 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
} else {
append(0xc8, 2, Stage::Vertex, 0x8b, 0x8c, Role::Main);
}
const bool nullPixel = Graphics::PixelProgramUnset(queue);
if (nullPixel) {
const auto rejection = Graphics::NullPixelProgramRejection(queue);
require(rejection.empty(), rejection.c_str());
if (includeFragment) {
if (pixelSkipped) {
const auto rejection = Graphics::NullPixelProgramRejection(queue);
require(rejection.empty(), rejection.c_str());
}
append(0x008, 1, Stage::Fragment, 0x00b, 0x00c, Role::Fragment);
programs.back().firstUserSgpr = 0;
}
append(0x008, 1, Stage::Fragment, 0x00b, 0x00c, Role::Fragment);
programs.back().firstUserSgpr = 0;
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(graphics), nullPixel);
return product;
}
}
std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Submission& submission) {
auto product = std::make_shared<DrawDecode>();
product->state = Graphics::DecodeState(queue);
DecodeGraphicsPrograms(*product, queue, *submission.shaders, false, true);
product->pixel = Graphics::DecodePixelStageInfo(queue.context, Graphics::ExportMappings(product->state), Graphics::PixelProgramSkipped(queue));
return product;
}
void Driver::resolveDrawDecode(const QueueState& queue, const Submission& submission, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile) {
if (decode == nullptr || verifyDrawRecipe()) {
std::vector<Graphics::RegisterRead> readLog;

View File

@ -63,6 +63,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(pixel.inputAddr);
for (const bool flag : {pixel.wave32, pixel.hasPerspectiveCenterVgpr, pixel.perspectiveCentroid, pixel.posX, pixel.posY, pixel.posZ, pixel.posW, pixel.frontFace, pixel.ancillary, pixel.sampleShading, pixel.noPerspective, pixel.linearCentroid, pixel.pixelKillEnable, pixel.depthExportEnable, pixel.sampleMaskExportEnable, pixel.earlyZ, pixel.executeOnNoop}) mix(flag);
mix(static_cast<std::uint64_t>(pixel.conservativeZExport));
mix(pixel.orderedPixelShader);
for (const auto value : pixel.targetOutputMode) mix(value);
for (const auto value : pixel.targetExportMapping) mix(value);
std::lock_guard cacheLock(drawCacheMutex);

View File

@ -23,13 +23,15 @@ std::optional<DrawVerdict> Driver::precheckDraw(const QueueState& queue, const S
localDevice->ColorMetadataPass(*pass);
return DrawVerdict::Drawn;
}
rejected = Graphics::DepthMaintenanceRejection(queue);
if (!rejected.empty()) return DrawVerdict::Rejected;
static const bool traceIndirectEnabled = std::getenv("APS5_TRACE_INDIRECT_DRAWS") != nullptr;
traceIndirect = traceIndirectEnabled;
if (traceIndirect && drawParameters.indirect) std::fprintf(stderr, "[draw] indirect packet %s args 0x%llx count %u reached\n", Pm4::Name(packet[0]).c_str(), static_cast<unsigned long long>(drawParameters.indirect->arguments), drawParameters.indirect->count);
{
const auto targetMask = queue.context.find(0x8e);
const auto shaderMask = queue.context.find(0x8f);
const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && (targetMask->second & shaderMask->second) != 0;
const bool colorWrites = targetMask != queue.context.end() && shaderMask != queue.context.end() && Graphics::ColorWriteMask(queue.context) != 0;
if (!colorWrites && !queue.shader.contains(0x8)) {
const auto word = [&](std::uint32_t offset) { const auto it = queue.context.find(offset); return it == queue.context.end() ? 0u : it->second; };
if ((word(0x200) & 3u) == 0 || ((word(0x010) & 3u) == 0 && (word(0x011) & 1u) == 0)) return DrawVerdict::Nothing;

View File

@ -1,20 +1,37 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include <algorithm>
#include <array>
#include <cstdlib>
#include <cstring>
namespace AgcDriver::DriverDetail {
std::uint64_t Driver::drawRegisterKey(const QueueState& queue, const ShaderRegistry& registry, std::uint64_t deviceSerial) {
static const bool allUserWords = std::getenv("APS5_DRAW_KEY_ALL_USER_WORDS") != nullptr;
std::uint64_t key = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) {
key ^= value;
key *= 0x100000001b3ull;
};
const auto userEnd = [&](std::uint32_t base) {
if (allUserWords) return base + 32u;
const auto resources = queue.shader.find(base - 1);
if (resources == queue.shader.end()) return base;
const auto count = ((resources->second >> 1u) & 0x1fu) | (((resources->second >> 27u) & 1u) << 5u);
return base + std::min(count, 32u);
};
const std::array<std::pair<std::uint32_t, std::uint32_t>, 3> users{{{0x00cu, userEnd(0x00cu)}, {0x08cu, userEnd(0x08cu)}, {0x10cu, userEnd(0x10cu)}}};
const auto unread = [&](std::uint32_t offset) {
return std::any_of(users.begin(), users.end(), [&](const auto& user) { return offset >= user.second && offset < user.first + 32u; });
};
mix(deviceSerial);
for (const auto& range : Graphics::DrawKeyRegisters) {
const auto& bank = range.bank == Graphics::RegisterBank::Context ? queue.context : range.bank == Graphics::RegisterBank::Shader ? queue.shader : queue.userConfig;
mix((static_cast<std::uint64_t>(range.bank) << 32u) | range.first);
const auto end = range.first + range.count;
const bool shader = range.bank == Graphics::RegisterBank::Shader;
for (auto it = bank.lower_bound(range.first); it != bank.end() && it->first < end; ++it) {
if (shader && unread(it->first)) continue;
mix(it->first);
mix(it->second);
}
@ -77,11 +94,11 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
for (std::size_t i = 0; i < s.blends.size(); ++i) {
if (!sameBlend(s.blends[i], t.blends[i])) return false;
}
if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.cullMode != t.cullMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false;
if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.cullMode != t.cullMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false;
if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false;
const auto& p = a.pixel;
const auto& q = b.pixel;
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false;
for (std::size_t i = 0; i < a.programs.size(); ++i) {
const auto& x = a.programs[i];

View File

@ -33,6 +33,10 @@ void UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<IVideoOut
DriverDetail::Driver::Get().UnregisterVideoOutput(handle, output);
}
void AttachWindow(const PresentationWindow& window) {
DriverDetail::Driver::Get().AttachWindow(window);
}
void PresentClear(const PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) {
DriverDetail::Driver::Get().Present(window, nullptr, opaque, gpuReady, context);
}
@ -94,6 +98,10 @@ extern "C" void AgcDriverUnregisterVideoOutput_nid_postfix(std::uint32_t handle,
AgcDriver::UnregisterVideoOutput(handle, output);
}
extern "C" void AgcDriverAttachWindow_nid_postfix(const AgcDriver::PresentationWindow& window) {
AgcDriver::AttachWindow(window);
}
extern "C" void AgcDriverPresentClear_nid_postfix(const AgcDriver::PresentationWindow& window, bool opaque, void (*gpuReady)(void*), void* context) {
AgcDriver::PresentClear(window, opaque, gpuReady, context);
}
@ -109,3 +117,14 @@ extern "C" void AgcDriverReleaseWindow_nid_postfix(void* window) {
extern "C" void AgcDriverReportFailure_nid_postfix(std::exception_ptr error) {
AgcDriver::ReportFailure(error);
}
extern "C" void AgcDriverResolveShaderAbi_nid_postfix(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
AgcDriver::DriverDetail::Driver::Get().ResolveShaderAbi(shader, context, primitive);
}
extern "C" void AgcDriverResolveGraphicsAbi_nid_postfix(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsAbi(vertex, pixel, primitiveType);
}
extern "C" void AgcDriverResolveGraphicsStagesAbi_nid_postfix(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
AgcDriver::DriverDetail::Driver::Get().ResolveGraphicsStagesAbi(stages, context, primitive);
}

View File

@ -2,6 +2,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
namespace AgcDriver::DriverDetail {
@ -44,6 +45,7 @@ void Driver::stop() {
device.Reset();
replacedDevices.clear();
Graphics::ShutdownGuestBufferWorkers();
StopProfileOutput_nid_no_patch();
stopped = true;
}

View File

@ -29,17 +29,11 @@ void Driver::timed(double WorkerProfile::*bucket, TWork&& work) {
}
}
template <typename TWork>
void Driver::tolerate(const char* kind, TWork&& work) {
try {
work();
} catch (const std::exception& error) {
reportSkip(kind, error.what());
}
}
void Driver::execute(const Submission& submission) {
auto* submissionTiming = includeTimingSubmission(submission, true);
if (submission.suspend) {
PerformanceContext timingContext(submissionTiming);
PerformanceTimer timing("Driver.Suspend");
static const bool suspendDrain = std::getenv("APS5_SUSPEND_DRAIN") != nullptr;
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
@ -113,9 +107,13 @@ void Driver::execute(const Submission& submission) {
const auto packet = std::span(submission.commands).subspan(cursor, count);
const auto opcode = (header >> 8u) & 0xffu;
auto nextCursor = cursor + count;
if (APS5_ENABLE_TIMING_LOG && submission.queue == 0 && pendingFrameTiming == nullptr) includeTimingSubmission(submission, false);
PerformanceContext timingContext(submission.queue == 0 ? pendingFrameTiming.get() : FrameTiming::Async());
PerformanceTimer timing("Driver.Packet");
std::shared_lock deviceUse(deviceReplacement, std::defer_lock);
if (opcode != 0x3c && opcode != 0x93 && header != RenderingWaitPacketHeader && header != FlipPacketHeader) deviceUse.lock();
timing.Mark("device_lock");
GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue);
CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast<unsigned long long>(submission.serial), submission.queue, cursor, header, packet.size());
if (Pm4::Predicated(header) && queue.predication.operation != 0) {
@ -132,6 +130,7 @@ void Driver::execute(const Submission& submission) {
const auto flushStart = profilePackets ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
flushBetweenPackets(submission.queue, header, opcode == 0x49 || opcode == 0x37);
timing.Mark("flush_between_packets");
PacketTimer packetTimer{profilePackets, header == FlipPacketHeader ? 0xffffu : opcode, submission.queue, packetProfile, std::chrono::steady_clock::now()};
if (profilePackets) {
@ -170,6 +169,7 @@ void Driver::execute(const Submission& submission) {
bool wroteOnGpu = false, endOfPipeInterrupt = false, interruptDeferred = false, drawPacket = false, sampleDump = false;
const bool drains = preparePacketMemory(submission, queue, packet, header, opcode, wroteOnGpu, endOfPipeInterrupt, interruptDeferred, drawPacket, sampleDump);
traceLabel(packet, submission.queue);
timing.Mark("prepare_memory");
const bool waitPacket = opcode == 0x3c || opcode == 0x93 || header == RenderingWaitPacketHeader;
struct Progress {
@ -201,10 +201,14 @@ void Driver::execute(const Submission& submission) {
if (batchesAtFlip != 0) flipSerial = batchesAtFlip;
flipBatchesUnsignaled += unsignaledAtFlip;
auto frame = std::make_shared<FrameTiming>(++frameSerial);
timing.Mark("flip_flush");
timing.FinishPacket(submission.serial, cursor, 0xffffu, count);
auto frame = APS5_ENABLE_TIMING_LOG ? std::move(pendingFrameTiming) : std::make_shared<FrameTiming>(++frameSerial);
require(frame != nullptr, "flip has no frame timing");
const auto now = FrameTiming::Clock::now();
frame->IncludeSubmission(submission.serial, now, now, now, true);
frame->SetFlip(submission.serial, cursor, now, now);
if (!APS5_ENABLE_TIMING_LOG) frame->IncludeSubmission(submission.serial, now, now, now, true);
frame->SetFlip(submission.serial, cursor, APS5_ENABLE_TIMING_LOG ? submission.receivedAt : now, now);
if (APS5_ENABLE_TIMING_LOG) frame->CollectBackground();
frame->NoteFlipBatches(batchesAtFlip, unsignaledAtFlip);
CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast<unsigned long long>(frameSerial), static_cast<unsigned long long>(submission.serial), cursor, static_cast<unsigned long long>(batchesAtFlip), static_cast<unsigned long long>(unsignaledAtFlip));
submission.flips.at(cursor)->GpuReady(frame);
@ -234,7 +238,7 @@ void Driver::execute(const Submission& submission) {
}
} else if (drawPacket) {
bool drawn = false;
timed(&WorkerProfile::drawMs, [&] { tolerate("draw", [&] {
timed(&WorkerProfile::drawMs, [&] {
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
static const bool profileDraws = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto color = (static_cast<std::uint64_t>(readRegister(queue.context, 0x390)) << 40u) | (static_cast<std::uint64_t>(readRegister(queue.context, 0x318)) << 8u);
@ -243,36 +247,14 @@ void Driver::execute(const Submission& submission) {
if (profileDraws) Graphics::CountDrawSkip(kind, std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - started).count());
};
const auto skipped = [&](const std::string& what) {
if (traceDraws) std::fprintf(stderr, "[draw] target 0x%llx mask 0x%x failed: %.160s\n",static_cast<unsigned long long>(color), readRegister(queue.context, 0x8e), what.c_str());
char suffix[80];
std::snprintf(suffix, sizeof(suffix), " [%s, color target 0x%llx]", Pm4::Name(header).c_str(), static_cast<unsigned long long>(color));
static std::set<std::pair<std::uint64_t, std::string>> dumpedTargets;
const std::string reason = what.substr(0, 48);
if (dumpedTargets.insert({color, reason}).second) {
char name[64];
std::snprintf(name, sizeof(name), "draw_%llx_%08x.regs", static_cast<unsigned long long>(color), static_cast<std::uint32_t>(std::hash<std::string>{}(reason)));
if (std::FILE* file = std::fopen(name, "w")) {
std::fprintf(file, "# %s\n", what.c_str());
recent.Print(file, "# packet %s\n");
for (const auto& [offset, value] : queue.context) std::fprintf(file, "context %x %08x\n", offset, value);
for (const auto& [offset, value] : queue.userConfig) std::fprintf(file, "uconfig %x %08x\n", offset, value);
for (const auto& [offset, value] : queue.shader) std::fprintf(file, "shader %x %08x\n", offset, value);
std::fclose(file);
}
}
reportSkip("draw", what + suffix);
};
try {
std::string rejected;
const auto verdict = draw(queue, packet, submission, rejected);
drawn = verdict == DrawVerdict::Drawn;
CaptureTrace::Log("draw submission=%llu queue=%x offset=%zu target=%llx mask=%x verdict=%d reason=%.256s", static_cast<unsigned long long>(submission.serial), submission.queue, cursor, static_cast<unsigned long long>(color), readRegister(queue.context, 0x8e), static_cast<int>(verdict), rejected.c_str());
if (verdict == DrawVerdict::Rejected) {
skipped(rejected);
countSkip(Graphics::DrawSkip::Prechecked);
throw std::runtime_error(rejected);
} else if (verdict == DrawVerdict::Nothing) {
countSkip(Graphics::DrawSkip::Nothing);
} else if (traceDraws) {
@ -280,10 +262,10 @@ void Driver::execute(const Submission& submission) {
}
} catch (const std::exception& error) {
CaptureTrace::Log("draw-error submission=%llu offset=%zu reason=%.256s", static_cast<unsigned long long>(submission.serial), cursor, error.what());
skipped(error.what());
countSkip(Graphics::DrawSkip::Thrown);
throw;
}
}); });
});
finishDrawPacket(drawn);
} else if (opcode == 0x22) {
recordQueuedLabelsBeforeRead(submission.queue);
@ -294,7 +276,8 @@ void Driver::execute(const Submission& submission) {
dumpSampleCounters(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u));
} else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) {
if (!wroteOnGpu) {
Pm4::Execute(packet, queue);
if (Pm4::IndirectRegisterOpcode(opcode)) Pm4::ExecuteIndirectRegisters(packet, submission.registerLists.at(cursor), queue);
else Pm4::Execute(packet, queue);
if (opcode == 0x49 || opcode == 0x37) {
if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial);
}
@ -302,9 +285,14 @@ void Driver::execute(const Submission& submission) {
if (endOfPipeInterrupt && !interruptDeferred) AgcDriverDeliverEopInterrupt(submission.queue);
}
if (drawPacket || (sampleDump && wroteOnGpu)) Graphics::Recorder::CountRecordedWork();
timing.Mark(header == RenderingWaitPacketHeader ? "rendering_wait" : opcode == 0x3c || opcode == 0x93 ? "memory_wait" : opcode == 0x15 || opcode == 0x16 ? "dispatch" : drawPacket ? "draw" : "execute");
if (submission.queue == 0) timing.FinishPacket(submission.serial, cursor, opcode, count);
else timing.Finish();
cursor = nextCursor;
}
PerformanceContext endContext(submission.queue == 0 ? frameTiming() : FrameTiming::Async());
PerformanceTimer endTiming("Driver.EndSubmission");
static const bool submitAtEnd = std::getenv("APS5_SUBMIT_AT_END") != nullptr;
if (!deferredLabels().labels.empty() || Graphics::Recorder::PendingLabelSince().has_value() || Graphics::Recorder::RecordedWorkSinceSubmit() != 0) {
auto& costs = submissionCosts(submission.queue);
@ -320,6 +308,7 @@ void Driver::execute(const Submission& submission) {
recordDeferredLabels(localDevice.get(), submission.queue);
if (localDevice != nullptr) localDevice->SubmitRecorded(submission.queue == 0);
}
endTiming.Finish();
if (submission.rewindTail != nullptr) executeRewindTail(submission);
}

View File

@ -18,7 +18,8 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
bool orderedAlready = false;
endOfPipeInterrupt = opcode == 0x49 && ((packet[2] >> 24u) & 7u) != 0;
const auto interruptSelect = (packet[2] >> 24u) & 7u;
endOfPipeInterrupt = opcode == 0x49 && interruptSelect != 0 && interruptSelect != 3;
interruptDeferred = false;
if (!drainAll && endOfPipeInterrupt) {
const auto label = Pm4::DecodeLabelWrite(packet);

View File

@ -23,6 +23,19 @@ void Driver::UnregisterVideoOutput(std::uint32_t handle, const std::shared_ptr<I
outputs.erase(it);
}
void Driver::AttachWindow(const PresentationWindow& window) {
CheckFailure();
std::unique_lock replacing(deviceReplacement);
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present);
std::lock_guard lock(GuestMemory::GpuMutex());
if (device) {
require(device->Window() == nullptr, "a window is already attached to the device");
device->PrepareForReplacement();
replacedDevices.push_back(device);
}
device = std::make_shared<VulkanDevice>(&window);
}
void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buffer, bool opaque, void (*gpuReady)(void*), void* context) {
PerformanceContext timingContext(window.timing.get());
@ -48,19 +61,10 @@ void Driver::Present(const PresentationWindow& window, const DisplayBuffer* buff
bool trailing = false;
double waitedMs = 0;
{
std::unique_lock replacing(deviceReplacement, std::defer_lock);
if (const auto current = device.Load(); current == nullptr || current->Window() == nullptr) replacing.lock();
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Present);
std::lock_guard lock(GuestMemory::GpuMutex());
timing.Mark("gpu_mutex_wait");
if (device == nullptr || device->Window() == nullptr) {
if (device) {
device->PrepareForReplacement();
replacedDevices.push_back(device);
}
device = std::make_shared<VulkanDevice>(&window);
}
require(device->Window() == window.context, "presentation window does not match device surface");
require(device != nullptr && device->Window() == window.context, "presentation window is not attached to the device");
presenting = device;
timing.Mark("device_setup");
std::uint32_t drawableWidth = 0;

View File

@ -2,12 +2,42 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/WorkerAffinity.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/WaitMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include <cstdlib>
namespace AgcDriver::DriverDetail {
FrameTiming* Driver::frameTiming() {
if (!APS5_ENABLE_TIMING_LOG) return nullptr;
if (pendingFrameTiming == nullptr) pendingFrameTiming = std::make_shared<FrameTiming>(++frameSerial, true);
return pendingFrameTiming.get();
}
FrameTiming* Driver::includeTimingSubmission(const Submission& submission, bool firstSegment) {
if (!APS5_ENABLE_TIMING_LOG) return nullptr;
auto* frame = submission.queue == 0 ? frameTiming() : FrameTiming::Async();
require(submission.receivedAt != FrameTiming::Clock::time_point{} && submission.receivedAt <= submission.enqueuedAt && submission.enqueuedAt <= submission.dequeuedAt && submission.dequeuedAt <= submission.orderedAt, "invalid submission timing order");
if (submission.queue == 0) frame->IncludeSubmission(submission.serial, submission.receivedAt, submission.enqueuedAt, submission.orderedAt, firstSegment);
else if (firstSegment) {
frame->Add(frame->Get("Submission", "accept"), submission.enqueuedAt - submission.receivedAt);
frame->Add(frame->Get("Submission", "queue"), submission.orderedAt - submission.enqueuedAt);
}
if (firstSegment) {
frame->Add(frame->Get("Submission", "dequeue"), submission.dequeuedAt - submission.enqueuedAt);
frame->Add(frame->Get("Submission", "order_wait"), submission.orderedAt - submission.dequeuedAt);
if (!submission.suspend) {
frame->Add(frame->Get("Submission", "copy"), submission.copiedAt - submission.receivedAt, submission.commands.size() * sizeof(std::uint32_t));
frame->Add(frame->Get("Submission", "validate"), submission.validatedAt - submission.copiedAt);
frame->Add(frame->Get("Submission", "flip_room_wait"), submission.roomReadyAt - submission.validatedAt);
frame->Add(frame->Get("Submission", "reserve_enqueue"), submission.enqueuedAt - submission.roomReadyAt);
}
}
return frame;
}
void Driver::markCompleted(std::uint64_t serial) {
completedOutOfOrder.insert(serial);
while (!completedOutOfOrder.empty() && *completedOutOfOrder.begin() == completed + 1) {
@ -52,12 +82,25 @@ void Driver::run(std::uint32_t id) noexcept {
submission = Submission{};
static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr;
{
PerformanceContext timingContext(id == 0 ? frameTiming() : FrameTiming::Async());
PerformanceTimer timing("Driver.WorkerWait");
std::unique_lock lock(mutex);
timing.Mark("mutex_wait");
auto& worker = workers.at(id);
auto& pending = worker.pending;
workerQueued() = &worker.queued;
if (traceGpu && pending.empty()) std::fprintf(stderr, "[gpu] %.1f idle queue=0x%x\n", TraceMs(), id);
const auto ready = [&] { return failure || stopping || !pending.empty(); };
const auto poll = [&](const auto& done) {
#ifdef _WIN32
lock.unlock();
PollSleep();
lock.lock();
return done();
#else
return changed.wait_for(lock, std::chrono::milliseconds(1), done);
#endif
};
while (!ready()) {
if (!completionsPending() || (id != 0 && Graphics::Recorder::PendingCompletionLabels() == 0)) {
@ -66,24 +109,29 @@ void Driver::run(std::uint32_t id) noexcept {
if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed);
break;
}
if (changed.wait_for(lock, std::chrono::milliseconds(1), ready)) break;
if (poll(ready)) break;
lock.unlock();
reapCompletionLabels();
lock.lock();
}
timing.Mark("idle_and_completion_poll");
rethrowFailure();
if (stopping || shutdownToken.stop_requested() || pending.empty()) {
break;
}
submission = std::move(pending.front());
pending.pop_front();
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);
while (!failure && !stopping && !orderReleased(id, submission.received)) changed.wait_for(lock, std::chrono::milliseconds(1));
const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); };
while (!released()) poll(released);
orderHolders.fetch_sub(1, std::memory_order_acq_rel);
rethrowFailure();
}
if (APS5_ENABLE_TIMING_LOG) submission.orderedAt = std::chrono::steady_clock::now();
timing.Mark("dequeue_order");
worker.queued.fetch_sub(1, std::memory_order_acq_rel);
if (profile && submission.enqueuedAt != std::chrono::steady_clock::time_point{}) costs.dequeueNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - submission.enqueuedAt).count());
}
@ -97,6 +145,8 @@ void Driver::run(std::uint32_t id) noexcept {
}
if (traceGpu) std::fprintf(stderr, "[gpu] %.1f done serial=%llu queue=0x%x\n", TraceMs(), static_cast<unsigned long long>(submission.serial), id);
const auto completeStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
PerformanceContext completionContext(id == 0 ? frameTiming() : FrameTiming::Async());
PerformanceTimer completionTiming("Driver.Completion");
bool notify = true;
{
std::lock_guard lock(mutex);

View File

@ -1,3 +1,4 @@
#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/Diagnostics.hpp"
@ -103,6 +104,19 @@ bool Driver::copySegment(Submission& submission, const std::uint32_t* guest, std
return false;
}
void Driver::readRegisterLists(Submission& submission) {
submission.registerLists.clear();
for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) {
const auto header = submission.commands[cursor];
if ((header >> 30u) != 3u || !Pm4::IndirectRegisterOpcode((header >> 8u) & 0xffu)) continue;
try {
submission.registerLists.emplace(cursor, Pm4::ReadIndirectRegisters(std::span<const std::uint32_t>(submission.commands).subspan(cursor, Pm4::PacketWords(header))));
} catch (const std::exception& error) {
throw std::runtime_error("AGC driver: " + Pm4::Name(header) + " at DWORD " + std::to_string(cursor) + ": " + error.what());
}
}
}
void Driver::waitForFlipRoom(const Submission& submission) {
for (std::size_t cursor = 0; cursor < submission.commands.size(); cursor += Pm4::PacketWords(submission.commands[cursor])) {
if (submission.commands[cursor] != FlipPacketHeader) continue;
@ -141,6 +155,7 @@ void Driver::reserveOutputs(Submission& submission) {
}
void Driver::executeRewindTail(const Submission& stalled) {
PerformanceTimer timing("Driver.Rewind");
std::atomic_ref<std::uint32_t> control(*const_cast<std::uint32_t*>(stalled.rewindTail - 1));
if ((control.load(std::memory_order_acquire) & 0x80000000u) == 0) {
noteWaitBlocked(stalled.queue, reinterpret_cast<std::uint64_t>(stalled.rewindTail - 1), true);
@ -157,9 +172,14 @@ void Driver::executeRewindTail(const Submission& stalled) {
}
Submission tail{};
tail.queue = stalled.queue;
if (APS5_ENABLE_TIMING_LOG) tail.receivedAt = std::chrono::steady_clock::now();
copyCommands(tail, stalled.rewindTail, stalled.rewindWords);
if (APS5_ENABLE_TIMING_LOG) tail.copiedAt = std::chrono::steady_clock::now();
validate(tail, stalled.rewindTail);
readRegisterLists(tail);
if (APS5_ENABLE_TIMING_LOG) tail.validatedAt = std::chrono::steady_clock::now();
waitForFlipRoom(tail);
if (APS5_ENABLE_TIMING_LOG) tail.roomReadyAt = std::chrono::steady_clock::now();
{
std::lock_guard lock(mutex);
rethrowFailure();
@ -169,10 +189,13 @@ void Driver::executeRewindTail(const Submission& stalled) {
tail.serial = stalled.serial;
tail.received = ++eventSerial;
}
if (APS5_ENABLE_TIMING_LOG) tail.enqueuedAt = tail.dequeuedAt = tail.orderedAt = std::chrono::steady_clock::now();
timing.Finish();
execute(tail);
}
void Driver::Submit(const Packet* packet, std::uint32_t queue) {
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");
GuestMemory::CheckRange(packet, sizeof(Packet), alignof(Packet));
@ -180,6 +203,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
require(descriptor.flags == 0, "nonzero submission flags are not implemented");
Submission submission{};
submission.queue = queue;
submission.receivedAt = receivedAt;
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
auto& costs = submissionCosts(queue);
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
@ -189,8 +213,12 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
copyCommands(submission, descriptor.addr, descriptor.dw_num);
}
const auto copied = profile ? std::chrono::steady_clock::now() : start;
if (APS5_ENABLE_TIMING_LOG) submission.copiedAt = std::chrono::steady_clock::now();
validate(submission, descriptor.addr);
readRegisterLists(submission);
if (APS5_ENABLE_TIMING_LOG) submission.validatedAt = std::chrono::steady_clock::now();
waitForFlipRoom(submission);
if (APS5_ENABLE_TIMING_LOG) submission.roomReadyAt = std::chrono::steady_clock::now();
static const bool trace = std::getenv("APS5_TRACE_GPU") != nullptr;
if (trace) std::fprintf(stderr, "[gpu] %.1f submit queue=0x%x dwords=%zu at %p\n", TraceMs(), queue, submission.commands.size(), static_cast<const void*>(descriptor.addr));
const auto validated = profile ? std::chrono::steady_clock::now() : start;
@ -211,6 +239,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
costs.validateNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(validated - copied).count());
costs.copyNs += static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>((copied - start) + (now - validated)).count());
}
if (APS5_ENABLE_TIMING_LOG) submission.enqueuedAt = std::chrono::steady_clock::now();
enqueue(std::move(submission));
++accepted;
}
@ -218,6 +247,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue) {
}
void Driver::SuspendPoint() {
const auto receivedAt = APS5_ENABLE_TIMING_LOG ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
require(!onWorkerThread(), "worker cannot suspend itself");
std::unique_lock lock(mutex);
rethrowFailure();
@ -228,6 +258,7 @@ void Driver::SuspendPoint() {
boundary.suspend = true;
boundary.queue = 0;
boundary.receivedAt = receivedAt;
boundary.enqueuedAt = std::chrono::steady_clock::now();
enqueue(std::move(boundary));
++accepted;

View File

@ -10,7 +10,9 @@ std::uint64_t WorkerAffinityMask() {
if (std::getenv("APS5_NO_WORKER_AFFINITY") != nullptr) return std::uint64_t{0};
const auto requested = CpuTopology::MaskFromEnvironment("APS5_WORKER_AFFINITY_MASK");
if (requested != 0) return requested;
#ifndef __linux__
if (std::getenv("APS5_WORKER_AFFINITY") == nullptr) return std::uint64_t{0};
#endif
const auto& layout = CpuTopology::Get();
return layout.hybrid ? layout.performant : std::uint64_t{0};
}();

View File

@ -1,8 +1,15 @@
#include "prx/libc/include/general/LogMacros.hpp"
#include "ControlFlow/GraphBuilder.hpp"
#include "ControlFlow/Structurizer.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "CacheKey.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparation.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderPreparationScope.hpp"
#include "CompiledVariant.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <cstdlib>
#include <cstring>
@ -78,71 +85,475 @@ std::shared_ptr<const ShaderSnapshot> ReadRawComputeShader(std::uint64_t address
throw std::runtime_error("AGC driver: raw compute program has no reachable end within mapped code or the size limit");
}
bool FailureMemo() {
static const bool memo = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr;
return memo;
struct ShaderPreparationTransaction::State {
struct Change {
std::shared_ptr<PreparedShaders> destination;
PreparedShaderState prepared;
};
static std::mutex writers;
static thread_local State* active;
std::unique_lock<std::mutex> lock{writers};
std::map<PreparedShaders*, Change> changes;
bool failed = false;
};
std::mutex ShaderPreparationTransaction::State::writers;
thread_local ShaderPreparationTransaction::State* ShaderPreparationTransaction::State::active = nullptr;
ShaderPreparationTransaction::ShaderPreparationTransaction() {
if (State::active == nullptr) {
state = std::make_unique<State>();
State::active = state.get();
}
root = State::active;
}
std::shared_ptr<const ShaderRecompiler::SourceHandle> SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, std::uint64_t deviceSerial, const ShaderRecompiler::RecompileRequest& request, bool bypass, const std::string** poisoned) {
static const bool enabled = std::getenv("APS5_NO_SOURCE_HANDLE_CACHE") == nullptr && std::getenv("APS5_NO_CAPTURE_REUSE") == nullptr;
static const bool verify = std::getenv("APS5_VERIFY_SOURCE_HANDLE") != nullptr;
if (!enabled || bypass || ShaderRecompiler::DebugProbeActive() || !request.useCache) return nullptr;
std::uint64_t key = 0xcbf29ce484222325ull;
for (const auto value : {static_cast<std::uint64_t>(codeOffset), deviceSerial, ShaderRecompiler::RecompileCacheKey::ContextHash(request)}) {
key ^= value;
key *= 0x100000001b3ull;
ShaderPreparationTransaction::~ShaderPreparationTransaction() {
if (!committed) root->failed = true;
if (state != nullptr) State::active = nullptr;
}
PreparedShaderState& ShaderPreparationTransaction::Edit(const ShaderSnapshot& snapshot) {
require(!committed, "shader preparation transaction is already committed");
auto& changes = root->changes;
const auto found = changes.find(snapshot.prepared.get());
if (found != changes.end()) return found->second.prepared;
std::lock_guard lock(snapshot.prepared->mutex);
return changes.emplace(snapshot.prepared.get(), State::Change{snapshot.prepared, *snapshot.prepared}).first->second.prepared;
}
const PreparedShaderState& ShaderPreparationTransaction::Read(const ShaderSnapshot& snapshot) const {
require(!committed, "shader preparation transaction is already committed");
const auto found = root->changes.find(snapshot.prepared.get());
return found != root->changes.end() ? found->second.prepared : *snapshot.prepared;
}
void ShaderPreparationTransaction::Commit() {
require(!committed && !root->failed, "shader preparation transaction was aborted");
if (state != nullptr) {
std::vector<std::unique_lock<std::mutex>> locks;
locks.reserve(state->changes.size());
for (const auto& [key, change] : state->changes) locks.emplace_back(change.destination->mutex);
for (auto& [key, change] : state->changes) {
auto& destination = *change.destination;
destination.entries.swap(change.prepared.entries);
destination.registeredAbis.swap(change.prepared.registeredAbis);
destination.graphicsAbis.swap(change.prepared.graphicsAbis);
destination.rectangles.swap(change.prepared.rectangles);
destination.rectangleProgress.swap(change.prepared.rectangleProgress);
destination.fragments.swap(change.prepared.fragments);
std::swap(destination.rectangleRequested, change.prepared.rectangleRequested);
}
}
if (request.graphics.has_value()) {
for (const auto& linked : request.graphics->linkedPrograms) {
for (const auto value : {static_cast<std::uint64_t>(linked.role), linked.binary.codeAddress}) {
key ^= value;
key *= 0x100000001b3ull;
committed = true;
}
void PublishRegisteredShader(std::shared_ptr<ShaderRegistry>& registry, const std::shared_ptr<const ShaderSnapshot>& snapshot) {
ShaderPreparationTransaction transaction;
if (registry != nullptr) {
const auto found = registry->find(snapshot->codeAddress);
if (found != registry->end()) {
const auto& current = *found->second;
if (current.headerAddress == snapshot->headerAddress && current.type == snapshot->type && current.code == snapshot->code && current.header == snapshot->header) {
transaction.Commit();
return;
}
}
}
auto& memos = *snapshot.handles;
auto next = registry != nullptr ? std::make_shared<ShaderRegistry>(*registry) : std::make_shared<ShaderRegistry>();
next->insert_or_assign(snapshot->codeAddress, snapshot);
registry = std::move(next);
transaction.Commit();
}
std::shared_ptr<const ShaderRecompiler::SourceHandle> SourceHandleFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request) {
struct PreparedKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, PreparedKeyStorage>();
ShaderRecompiler::BuildPreparedShaderKey(request, key);
std::lock_guard lock(snapshot.prepared->mutex);
for (const auto& entry : snapshot.prepared->entries) {
if (entry.codeOffset == codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) return entry.handle;
}
if (snapshot.header.empty()) {
if (snapshot.type != 0 || request.shader.stage != ShaderRecompiler::ShaderStage::Compute) throw std::runtime_error("AGC driver: unregistered program is not a compute shader");
APS5_LOG_ERR("Compute shader 0x%llx was not registered; preparing its artifact at dispatch", static_cast<unsigned long long>(snapshot.codeAddress));
auto handle = ShaderRecompiler::PrepareShader(request);
snapshot.prepared->entries.push_back({codeOffset, handle});
return handle;
}
std::string layouts;
for (const auto& entry : snapshot.prepared->entries) {
if (entry.codeOffset != codeOffset || entry.handle == nullptr || entry.handle->artifact == nullptr) continue;
const auto& layout = entry.handle->artifact->layout;
layouts += " [" + std::to_string(layout.pushConstantOffsetBytes) + "," + std::to_string(layout.pushConstantSizeBytes) + "]";
}
throw std::runtime_error("AGC driver: prepared shader artifact is missing for the requested static ABI: address=" + std::to_string(request.shader.codeAddress) + " stage=" + std::to_string(static_cast<std::uint32_t>(request.shader.stage)) + " wave=" + std::to_string(request.context.waveSize) + " pushOffset=" + std::to_string(request.layout.pushConstantOffsetBytes) + " pushCapacity=" + std::to_string(request.layout.pushConstantSizeBytes) + " preparedLayouts=" + layouts);
}
ShaderRecompiler::RectListShaders PreparedRectangle(const ShaderSnapshot& snapshot, std::uint64_t vertexId, std::uint64_t fragmentId) {
std::lock_guard lock(snapshot.prepared->mutex);
for (const auto& entry : snapshot.prepared->rectangles) {
if (entry.vertexId == vertexId && entry.fragmentId == fragmentId) {
require(entry.shaders != nullptr, "prepared rectangle artifact is missing");
return *entry.shaders;
}
}
throw std::runtime_error("AGC driver: prepared rectangle artifacts are missing");
}
ShaderRecompiler::PreparedShaderInvocation InvocationFor(const ShaderSnapshot& snapshot, std::size_t codeOffset, const ShaderRecompiler::RecompileRequest& request) {
require(codeOffset < snapshot.code.size(), "prepared shader code offset is outside the snapshot");
const auto code = std::span(snapshot.code).subspan(codeOffset);
require(request.shader.code.data() == code.data() && request.shader.code.size() == code.size(), "prepared invocation does not refer to registered code");
auto invocationRequest = request;
struct PreparedKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, PreparedKeyStorage>();
ShaderRecompiler::BuildPreparedShaderKey(request, key);
std::lock_guard lock(snapshot.prepared->mutex);
for (const auto& entry : snapshot.prepared->entries) {
if (entry.codeOffset != codeOffset) continue;
invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*entry.handle);
if (auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, entry.handle, key)) return std::move(*invocation);
}
if (snapshot.header.empty()) {
if (snapshot.type != 0 || request.shader.stage != ShaderRecompiler::ShaderStage::Compute) throw std::runtime_error("AGC driver: unregistered program is not a compute shader");
APS5_LOG_ERR("Compute shader 0x%llx was not registered; preparing its artifact at dispatch", static_cast<unsigned long long>(snapshot.codeAddress));
auto handle = ShaderRecompiler::PrepareShader(request);
invocationRequest = request;
invocationRequest.shader.code = ShaderRecompiler::GetPreparedCode(*handle);
auto invocation = ShaderRecompiler::PreparedShaderInvocation::TryCreate(invocationRequest, handle, key);
if (!invocation.has_value()) throw std::runtime_error("AGC driver: raw compute artifact does not match its invocation");
snapshot.prepared->entries.push_back({codeOffset, std::move(handle)});
return std::move(*invocation);
}
std::string layouts;
for (const auto& entry : snapshot.prepared->entries) {
if (entry.codeOffset != codeOffset || entry.handle == nullptr || entry.handle->artifact == nullptr) continue;
const auto& layout = entry.handle->artifact->layout;
layouts += " [" + std::to_string(layout.pushConstantOffsetBytes) + "," + std::to_string(layout.pushConstantSizeBytes) + "]";
}
throw std::runtime_error("AGC driver: prepared shader artifact is missing for the requested static ABI: address=" + std::to_string(request.shader.codeAddress) + " stage=" + std::to_string(static_cast<std::uint32_t>(request.shader.stage)) + " wave=" + std::to_string(request.context.waveSize) + " pushOffset=" + std::to_string(request.layout.pushConstantOffsetBytes) + " pushCapacity=" + std::to_string(request.layout.pushConstantSizeBytes) + " preparedLayouts=" + layouts);
}
std::optional<ShaderRecompiler::ShaderFloatMode> RegisteredFloatMode(const ShaderSnapshot& snapshot) {
if (snapshot.registeredState == nullptr) return std::nullopt;
std::uint32_t rsrc1;
std::uint32_t fp16OverflowBit;
switch (snapshot.type) {
case 0: rsrc1 = 0x212; fp16OverflowBit = 26; break;
case 1: rsrc1 = 0x00a; fp16OverflowBit = 29; break;
case 2: case 4: case 6: rsrc1 = 0x08a; fp16OverflowBit = 31; break;
case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break;
default: return std::nullopt;
}
const auto found = snapshot.registeredState->shader.find(rsrc1);
if (found == snapshot.registeredState->shader.end()) return std::nullopt;
const auto value = found->second;
return ShaderRecompiler::ShaderFloatMode{(value >> 12u) & 0xffu, ((value >> 21u) & 1u) != 0u, ((value >> 23u) & 1u) != 0u, ((value >> fp16OverflowBit) & 1u) != 0u};
}
namespace {
template<typename TValue>
std::vector<TValue> ReadHeaderArray(const ShaderSnapshot& snapshot, const TValue* pointer, std::size_t count) {
if (count == 0) return {};
const auto address = reinterpret_cast<std::uintptr_t>(pointer);
if (address < snapshot.headerAddress || address - snapshot.headerAddress > snapshot.header.size()) throw std::runtime_error("AGC driver: shader metadata is outside the registered header");
const auto offset = static_cast<std::size_t>(address - snapshot.headerAddress);
if (count > (snapshot.header.size() - offset) / sizeof(TValue)) throw std::runtime_error("AGC driver: truncated shader metadata");
std::vector<TValue> result(count);
std::memcpy(result.data(), snapshot.header.data() + offset, count * sizeof(TValue));
return result;
}
Shader ReadHeader(const ShaderSnapshot& snapshot) {
Shader header;
std::memcpy(&header, snapshot.header.data(), sizeof(header));
return header;
}
RegisteredShaderState DecodeRegisteredState(const ShaderSnapshot& snapshot) {
const auto header = ReadHeader(snapshot);
RegisteredShaderState state{{}, InitialContextRegisters(), {{0x24a, 0}, {0x24b, 0}}};
for (const auto reg : ReadHeaderArray(snapshot, header.sh_registers, header.num_sh_registers)) {
state.shader.insert_or_assign(reg.offset, reg.value);
}
for (const auto reg : ReadHeaderArray(snapshot, header.cx_registers, header.num_cx_registers)) {
state.context.insert_or_assign(reg.offset, reg.value);
}
if (header.specials != nullptr) {
const auto special = ReadHeaderArray(snapshot, header.specials, 1).front();
state.context[special.vgt_shader_stages_en.offset] = special.vgt_shader_stages_en.value;
state.context[special.vgt_gs_out_prim_type.offset] = special.vgt_gs_out_prim_type.value;
state.userConfig[special.ge_cntl.offset] = special.ge_cntl.value;
state.userConfig[special.ge_user_vgpr_en.offset] = special.ge_user_vgpr_en.value;
}
return state;
}
std::uint32_t RegisterValue(const Registers& registers, std::uint32_t offset) {
const auto found = registers.find(offset);
if (found == registers.end()) throw std::runtime_error("AGC driver: missing static shader ABI register " + std::to_string(offset));
return found->second;
}
void BuildRegisteredAbiKey(const QueueState& state, const VulkanDevice& device, std::vector<std::uint64_t>& key) {
key.clear();
key.reserve(7u + state.shader.size() + state.context.size() + state.userConfig.size());
key.insert(key.end(), {device.Serial(), ShaderRecompiler::DebugProbeActive(), ShaderRecompiler::RayTracingStrict(), ShaderRecompiler::RayTracingMiss()});
for (const auto* registers : {&state.shader, &state.context, &state.userConfig}) {
key.push_back(registers->size());
for (const auto& [offset, value] : *registers) key.push_back((static_cast<std::uint64_t>(offset) << 32u) | value);
}
}
std::vector<PreparedShaders::Entry> PrepareRegistered(const ShaderSnapshot& snapshot, const VulkanDevice& device, const QueueState& state, bool registration) {
using Stage = ShaderRecompiler::ShaderStage;
const auto header = ReadHeader(snapshot);
std::uint32_t programRegister;
std::uint32_t resourceRegister;
std::uint32_t firstUser = 0;
Stage stage;
switch (snapshot.type) {
case 0: stage = Stage::Compute; programRegister = 0x20c; resourceRegister = 0x213; break;
case 1: stage = Stage::Fragment; programRegister = 0x008; resourceRegister = 0x00b; break;
case 2: stage = Stage::Vertex; programRegister = 0x0c8; resourceRegister = 0x08b; firstUser = 8; break;
case 4: stage = Stage::Mesh; programRegister = 0x0c8; resourceRegister = 0x08b; break;
case 5: stage = Stage::Local; programRegister = 0x148; resourceRegister = 0x10b; firstUser = 8; break;
case 6: stage = Stage::Mesh; programRegister = 0x088; resourceRegister = 0x08b; break;
case 7: stage = Stage::TessellationControl; programRegister = 0x108; resourceRegister = 0x10b; break;
default: throw std::runtime_error("AGC driver: unsupported registered shader type");
}
const auto high = RegisterValue(state.shader, programRegister + 1);
if ((high & ~0xffu) != 0) throw std::runtime_error("AGC driver: invalid registered program address");
const auto address = (static_cast<std::uint64_t>(RegisterValue(state.shader, programRegister)) << 8u) | (static_cast<std::uint64_t>(high) << 40u);
if (address < snapshot.codeAddress || address - snapshot.codeAddress >= snapshot.code.size() * 4u) throw std::runtime_error("AGC driver: registered entry point is outside shader code");
const auto codeOffset = static_cast<std::size_t>((address - snapshot.codeAddress) / 4u);
const auto code = std::span(snapshot.code).subspan(codeOffset);
const auto decoded = ShaderRecompiler::RdnaInstructionDecoder{}.Decode(code);
std::optional<ShaderRecompiler::ShaderComputeStageInfo> compute;
std::optional<ShaderRecompiler::ShaderPixelStageInfo> pixel;
std::optional<ShaderRecompiler::ShaderVertexStageInfo> vertex;
std::optional<ShaderRecompiler::GraphicsCompileContext> graphics;
std::uint32_t wave;
const auto resources = RegisterValue(state.shader, resourceRegister);
auto userCount = (resources >> 1u) & 0x1fu;
if (stage != Stage::Compute) userCount |= ((resources >> 27u) & 1u) << 5u;
if (userCount > 32) throw std::runtime_error("AGC driver: static user SGPR count exceeds the register bank");
if (stage == Stage::Compute) {
compute = Graphics::DecodeComputeStageInfo(state.shader, snapshot.header);
const auto special = ReadHeaderArray(snapshot, header.specials, 1).front();
wave = (special.dispatch_modifier & 0x8000u) != 0 ? 32u : 64u;
} else if (stage == Stage::Fragment) {
const auto count = RegisterValue(state.context, 0x1b6) & 0x3fu;
if (registration && count != 0) return {};
pixel = Graphics::DecodePixelStageInfo(state.context, {});
wave = pixel->wave32 ? 32u : 64u;
} else {
const auto routing = RegisterValue(state.context, 0x2d5);
wave = (routing & 0x00400000u) != 0 ? 32u : 64u;
if ((routing & 0x20u) != 0 || stage == Stage::Mesh || (routing & 4u) != 0) {
if (registration) return {};
auto stageState = state;
stageState.context[0x1b6] = 0;
const auto stages = Graphics::DecodeShaderStages(stageState);
graphics = ShaderRecompiler::GraphicsCompileContext{0, {}, stages.mesh, stages.tessellation, {}};
if (stages.mesh) { stage = Stage::Mesh; firstUser = 0; }
if (stages.tessellation && snapshot.type == 2) stage = Stage::TessellationEvaluation;
}
if (firstUser == 0) userCount += 8;
}
std::vector<std::uint32_t> userData(userCount);
if (stage != Stage::Compute && stage != Stage::Fragment && snapshot.type != 6) vertex = Graphics::DecodeVertexStageInfo(snapshot.header, snapshot.headerAddress, userData, nullptr, true);
const ShaderRecompiler::SwappcInfo swappc{vertex.has_value(), firstUser, userCount};
auto graph = ShaderRecompiler::GraphBuilder{}.Build(decoded, &swappc);
ShaderRecompiler::Structurizer{}.Structurize(graph);
const std::array<ShaderRecompiler::MemoryRegion, 2> memory{{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}, {snapshot.headerAddress, snapshot.header}}};
ShaderRecompiler::RecompileRequest request{{stage, address, code, snapshot.headerAddress, snapshot.header}, {wave, firstUser, userData, compute, pixel, vertex, memory, RegisteredFloatMode(snapshot)}, stage == Stage::Compute ? device.ComputeTarget(wave) : device.Target(), {0, 0, 0, 128}, graphics};
if (graphics && graphics->mesh) request.layout.pushConstantSizeBytes = ShaderRecompiler::MeshDrawPushOffsetBytes;
std::vector<PreparedShaders::Entry> entries;
const auto append = [&] {
PerformanceTimer timing("Shader.PrepareArtifact");
entries.push_back({codeOffset, ShaderRecompiler::PrepareShader(request)});
};
append();
if (compute) {
request.context.compute->partialThreads = {1, 1, 1};
append();
} else if (pixel) {
request.layout.pushConstantSizeBytes = 0;
append();
}
return entries;
}
}
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) {
ShaderPreparationTransaction transaction;
require(decoded.programs.size() == decoded.roles.size(), "graphics ABI program roles are incomplete");
for (const auto& program : decoded.programs) require(program.snapshot != nullptr, "graphics ABI has no registered shader snapshot");
std::vector<ShaderRecompiler::ProgramRole> expected;
if (decoded.state.stages.path == Graphics::ShaderPath::Tessellation) expected = {ShaderRecompiler::ProgramRole::Local, ShaderRecompiler::ProgramRole::Hull, ShaderRecompiler::ProgramRole::Domain};
else {
expected.push_back(ShaderRecompiler::ProgramRole::Main);
if (decoded.state.stages.path == Graphics::ShaderPath::Geometry && !decoded.programs.empty() && decoded.programs.front().snapshot->type == 4) expected.push_back(ShaderRecompiler::ProgramRole::GeometryBack);
}
if (!decoded.roles.empty() && decoded.roles.back() == ShaderRecompiler::ProgramRole::Fragment) expected.push_back(ShaderRecompiler::ProgramRole::Fragment);
require(expected == decoded.roles, "graphics ABI linked programs do not match the stage routing");
std::vector<ShaderRecompiler::LinkedProgram> linked;
std::vector<ShaderRecompiler::MemoryRegion> memory;
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
const auto& program = decoded.programs[index];
require(program.snapshot != nullptr && program.codeOffset < program.snapshot->code.size(), "graphics ABI has an invalid registered entry point");
const auto code = std::span(program.snapshot->code).subspan(program.codeOffset);
require(program.binary.code.data() == code.data() && program.binary.code.size() == code.size() && program.binary.codeAddress == program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t), "graphics ABI entry point differs from its snapshot");
linked.push_back({decoded.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData});
memory.insert(memory.end(), program.memory.begin(), program.memory.end());
}
std::vector<PreparedGraphicsStage> prepared;
std::uint32_t pushOffset = 0;
const auto capacity = decoded.state.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : Graphics::PipelinePushConstantBytes;
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
if (decoded.roles[index] == ShaderRecompiler::ProgramRole::GeometryBack) continue;
const auto& program = decoded.programs[index];
const bool fragment = program.binary.stage == ShaderRecompiler::ShaderStage::Fragment;
const auto wave = fragment ? decoded.state.stages.fragmentWaveSize : decoded.state.stages.vertexWaveSize;
std::optional<ShaderRecompiler::ShaderVertexStageInfo> vertex;
if (!fragment) vertex = Graphics::DecodeVertexStageInfo(program.binary.header, program.binary.headerAddress, program.userData, nullptr, true);
ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory, RegisteredFloatMode(*program.snapshot)}, target, {0, 0, pushOffset, capacity - pushOffset}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, decoded.state.stages.mesh, decoded.state.stages.tessellation, {}}};
std::vector<std::uint64_t> key;
ShaderRecompiler::BuildPreparedShaderKey(request, key);
std::shared_ptr<const ShaderRecompiler::SourceHandle> handle;
for (const auto& entry : transaction.Read(*program.snapshot).entries) {
if (entry.codeOffset == program.codeOffset && ShaderRecompiler::MatchesPreparedShader(request, *entry.handle, key)) {
handle = entry.handle;
break;
}
}
if (handle == nullptr) {
PerformanceTimer timing("Shader.PrepareArtifact");
handle = ShaderRecompiler::PrepareShader(request);
}
const auto bytes = handle->artifact->bindings.pushConstantSizeBytes;
require(bytes <= capacity - pushOffset, "prepared graphics stages exceed the push constant block");
prepared.push_back({program.snapshot, {program.codeOffset, handle}});
pushOffset += bytes;
}
transaction.Commit();
return prepared;
}
void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveGraphicsStagesAbi");
ShaderPreparationTransaction transaction;
CheckFailure();
require(!stages.empty(), "graphics ABI has no shader headers");
QueueState state{};
std::shared_ptr<const ShaderRegistry> registry;
std::shared_ptr<const ShaderSnapshot> owner;
{
std::lock_guard lock(memos.mutex);
for (const auto& entry : memos.entries) {
if (entry.key != key || (entry.handle == nullptr && entry.failure == nullptr)) continue;
if (entry.handle == nullptr) {
if (verify) {
bool resolved = true;
try {
static_cast<void>(ShaderRecompiler::ResolveSource(request));
} catch (const std::exception&) {
resolved = false;
}
if (resolved) throw std::runtime_error("AGC driver: source handle memo poisoned but the source resolved");
}
ShaderMemory::CountHandleMemo(true);
if (poisoned == nullptr) throw std::runtime_error(*entry.failure);
*poisoned = entry.failure.get();
return nullptr;
}
if (verify && ShaderRecompiler::ResolveSource(request)->source != entry.handle->source) throw std::runtime_error("AGC driver: source handle memo answered a different source");
ShaderMemory::CountHandleMemo(true);
return entry.handle;
std::lock_guard lock(mutex);
registry = shaders;
for (const auto* shader : stages) {
GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader));
const auto address = reinterpret_cast<std::uintptr_t>(const_cast<const void*>(shader->code));
require(registry != nullptr && registry->contains(address), "graphics ABI refers to an unregistered shader");
if (owner == nullptr) owner = registry->at(address);
const auto& snapshot = *registry->at(address);
require(snapshot.headerAddress == reinterpret_cast<std::uintptr_t>(shader), "graphics ABI refers to a replaced shader header");
require(snapshot.registeredState != nullptr, "registered shader state is missing");
const auto& registered = *snapshot.registeredState;
for (const auto& [offset, value] : registered.shader) state.shader.insert_or_assign(offset, value);
for (const auto& [offset, value] : registered.context) state.context.insert_or_assign(offset, value);
for (const auto& [offset, value] : registered.userConfig) state.userConfig.insert_or_assign(offset, value);
}
}
std::shared_ptr<const ShaderRecompiler::SourceHandle> handle;
try {
handle = ShaderRecompiler::ResolveSource(request);
} catch (const std::exception& error) {
if (FailureMemo()) {
std::lock_guard lock(memos.mutex);
memos.entries[memos.next] = {key, nullptr, std::make_shared<const std::string>(error.what())};
memos.next = (memos.next + 1) % memos.entries.size();
memos.poisoned.fetch_add(1, std::memory_order_relaxed);
for (const auto reg : context) state.context.insert_or_assign(reg.offset, reg.value);
for (const auto reg : primitive) state.userConfig.insert_or_assign(reg.offset, reg.value);
const auto localDevice = device.Load();
require(localDevice != nullptr, "graphics ABI device is missing");
struct GraphicsAbiKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, GraphicsAbiKeyStorage>();
BuildRegisteredAbiKey(state, *localDevice, key);
const auto primitiveType = state.userConfig.find(0x242u);
for (const auto& abi : transaction.Read(*owner).graphicsAbis) {
if (abi.key != key || abi.registry.lock() != registry) continue;
for (const auto& stage : abi.stages) {
const auto snapshot = stage.lock();
require(snapshot != nullptr, "prepared graphics ABI lost a registered stage");
if (snapshot->type != 1) ResolvePreparedGraphics(*snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target());
}
throw;
transaction.Commit();
return;
}
ShaderMemory::CountHandleMemo(false);
if (handle == nullptr) return nullptr;
std::lock_guard lock(memos.mutex);
memos.entries[memos.next] = {key, handle, nullptr};
memos.next = (memos.next + 1) % memos.entries.size();
return handle;
DrawDecode decoded{};
decoded.state.stages = Graphics::DecodeShaderStages(state);
DecodeGraphicsPrograms(decoded, state, *registry, true, false);
auto prepared = PrepareGraphicsStages(decoded, localDevice->Target());
for (auto& stage : prepared) {
const auto& entries = transaction.Read(*stage.snapshot).entries;
const auto duplicate = std::ranges::any_of(entries, [&](const auto& existing) { return existing.codeOffset == stage.entry.codeOffset && existing.handle->artifact == stage.entry.handle->artifact; });
if (!duplicate) transaction.Edit(*stage.snapshot).entries.push_back(std::move(stage.entry));
}
for (const auto& stage : prepared) {
if (stage.snapshot->type == 1) continue;
ResolvePreparedGraphics(*stage.snapshot, {}, primitiveType == state.userConfig.end() ? 0u : primitiveType->second, localDevice->Target());
}
PreparedShaderState::GraphicsAbi abi{key, registry, {}};
for (const auto& stage : prepared) abi.stages.push_back(stage.snapshot);
auto& graphicsAbis = transaction.Edit(*owner).graphicsAbis;
std::erase_if(graphicsAbis, [](const auto& entry) { return entry.registry.expired(); });
graphicsAbis.push_back(std::move(abi));
transaction.Commit();
}
void Driver::ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveAbi");
ShaderPreparationTransaction transaction;
CheckFailure();
GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader));
if (shader->type != 0 && shader->type != 1) {
const std::array<const Shader*, 1> stages{shader};
ResolveGraphicsStagesAbi(stages, context, primitive);
transaction.Commit();
return;
}
std::shared_ptr<const ShaderSnapshot> snapshot;
{
std::lock_guard lock(mutex);
const auto address = reinterpret_cast<std::uintptr_t>(const_cast<const void*>(shader->code));
require(shaders != nullptr && shaders->contains(address), "static ABI refers to an unregistered shader");
snapshot = shaders->at(address);
require(snapshot->headerAddress == reinterpret_cast<std::uintptr_t>(shader), "static ABI refers to a replaced shader header");
}
require(snapshot->registeredState != nullptr, "registered shader state is missing");
QueueState state{};
state.shader = snapshot->registeredState->shader;
state.context = snapshot->registeredState->context;
state.userConfig = snapshot->registeredState->userConfig;
for (const auto reg : context) state.context[reg.offset] = reg.value;
for (const auto reg : primitive) state.userConfig[reg.offset] = reg.value;
const auto localDevice = device.Load();
require(localDevice != nullptr, "shader registration device is missing");
struct RegisteredAbiKeyStorage {};
auto& key = HostThreadLocal<std::vector<std::uint64_t>, RegisteredAbiKeyStorage>();
BuildRegisteredAbiKey(state, *localDevice, key);
const auto& registeredAbis = transaction.Read(*snapshot).registeredAbis;
if (std::ranges::find(registeredAbis, key) != registeredAbis.end()) {
transaction.Commit();
return;
}
auto entries = PrepareRegistered(*snapshot, *localDevice, state, false);
auto& prepared = transaction.Edit(*snapshot);
for (auto& entry : entries) {
const auto duplicate = std::ranges::any_of(prepared.entries, [&](const auto& existing) { return existing.handle->artifact == entry.handle->artifact; });
if (!duplicate) prepared.entries.push_back(std::move(entry));
}
prepared.registeredAbis.push_back(key);
transaction.Commit();
}
alignas(256) static const std::uint32_t NullPixelCode[64] = {0xbf810000u};
@ -161,7 +572,97 @@ std::uint64_t NullPixelProgramAddress() {
return reinterpret_cast<std::uintptr_t>(NullPixelCode);
}
void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.ResolveGraphicsAbi");
CheckFailure();
if (primitiveType != 0 && primitiveType != 7 && primitiveType != 17) return;
ShaderPreparationTransaction transaction;
require(vertex != nullptr && pixel != nullptr, "rectangle ABI requires vertex and fragment shaders");
std::shared_ptr<const ShaderSnapshot> front;
std::shared_ptr<const ShaderSnapshot> fragment;
{
std::lock_guard lock(mutex);
GuestMemory::CheckRange(vertex, sizeof(Shader), alignof(Shader));
GuestMemory::CheckRange(pixel, sizeof(Shader), alignof(Shader));
const auto lookup = [&](const Shader* shader) {
const auto address = reinterpret_cast<std::uintptr_t>(const_cast<const void*>(shader->code));
require(shaders != nullptr && shaders->contains(address), "rectangle ABI refers to an unregistered shader");
const auto snapshot = shaders->at(address);
require(snapshot->headerAddress == reinterpret_cast<std::uintptr_t>(shader), "rectangle ABI refers to a replaced shader header");
return snapshot;
};
front = lookup(vertex);
fragment = lookup(pixel);
}
require(front != fragment, "rectangle stages refer to the same shader");
const auto localDevice = device.Load();
require(localDevice != nullptr, "shader registration device is missing");
ResolvePreparedGraphics(*front, fragment, primitiveType, localDevice->Target());
transaction.Commit();
}
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target) {
ShaderPreparationTransaction transaction;
require(fragment.get() != &front, "rectangle stages refer to the same shader");
std::vector<std::shared_ptr<const ShaderSnapshot>> fragments;
{
const auto& current = transaction.Read(front);
const bool newFragment = fragment && !std::ranges::any_of(current.fragments, [&](const auto& entry) { return entry.lock() == fragment; });
const bool newRequest = !current.rectangleRequested && (primitiveType == 7 || primitiveType == 17);
const bool expiredFragment = std::ranges::any_of(current.fragments, [](const auto& entry) { return entry.expired(); });
if (newFragment || newRequest || expiredFragment) {
auto& prepared = transaction.Edit(front);
std::erase_if(prepared.fragments, [](const auto& entry) { return entry.expired(); });
std::erase_if(prepared.rectangleProgress, [](const auto& entry) { return entry.fragment.expired(); });
if (newFragment) prepared.fragments.push_back(fragment);
prepared.rectangleRequested |= newRequest;
}
const auto& prepared = transaction.Read(front);
if (!prepared.rectangleRequested) {
transaction.Commit();
return;
}
for (const auto& entry : prepared.fragments) {
if (auto snapshot = entry.lock()) fragments.push_back(std::move(snapshot));
}
}
for (const auto& pixel : fragments) {
const auto& current = transaction.Read(front);
const auto& pixelPrepared = transaction.Read(*pixel);
require(!current.entries.empty() && !pixelPrepared.entries.empty(), "rectangle ABI has missing stage artifacts");
const auto progress = std::ranges::find_if(current.rectangleProgress, [&](const auto& entry) { return entry.fragment.lock() == pixel; });
const auto vertexCount = progress == current.rectangleProgress.end() ? 0u : progress->vertexCount;
const auto fragmentCount = progress == current.rectangleProgress.end() ? 0u : progress->fragmentCount;
if (vertexCount == current.entries.size() && fragmentCount == pixelPrepared.entries.size()) continue;
auto& prepared = transaction.Edit(front);
std::vector<PreparedShaders::Rectangle> rectangles;
for (std::size_t vertexIndex = 0; vertexIndex < prepared.entries.size(); ++vertexIndex) {
const auto& vertexEntry = prepared.entries[vertexIndex];
for (std::size_t fragmentIndex = vertexIndex < vertexCount ? fragmentCount : 0u; fragmentIndex < pixelPrepared.entries.size(); ++fragmentIndex) {
const auto& fragmentEntry = pixelPrepared.entries[fragmentIndex];
const auto& vertexArtifact = ShaderRecompiler::GetPreparedArtifact(*vertexEntry.handle);
const auto& fragmentArtifact = ShaderRecompiler::GetPreparedArtifact(*fragmentEntry.handle);
const auto exists = [&](const auto& entry) { return entry.vertexId == vertexArtifact.variantId && entry.fragmentId == fragmentArtifact.variantId; };
if (std::ranges::any_of(prepared.rectangles, exists) || std::ranges::any_of(rectangles, exists)) continue;
ShaderRecompiler::RecompileResult vertexResult;
ShaderRecompiler::RecompileResult fragmentResult;
static_cast<ShaderRecompiler::CompiledShaderArtifact&>(vertexResult) = vertexArtifact;
static_cast<ShaderRecompiler::CompiledShaderArtifact&>(fragmentResult) = fragmentArtifact;
rectangles.push_back({vertexArtifact.variantId, fragmentArtifact.variantId, std::make_shared<const ShaderRecompiler::RectListShaders>(ShaderRecompiler::BuildRectListShaders(vertexResult, fragmentResult, target))});
}
}
prepared.rectangles.insert(prepared.rectangles.end(), std::make_move_iterator(rectangles.begin()), std::make_move_iterator(rectangles.end()));
std::erase_if(prepared.rectangleProgress, [&](const auto& entry) { return entry.fragment.expired() || entry.fragment.lock() == pixel; });
prepared.rectangleProgress.push_back({pixel, prepared.entries.size(), pixelPrepared.entries.size()});
}
transaction.Commit();
}
void Driver::RegisterShader(const Shader* shader) {
PerformanceContext timingContext(FrameTiming::Preparation());
PerformanceTimer timing("Shader.Register");
ShaderPreparationTransaction transaction;
CheckFailure();
GuestMemory::CheckRange(shader, sizeof(Shader), 1);
Shader fields;
@ -178,6 +679,38 @@ void Driver::RegisterShader(const Shader* shader) {
snapshot.header.resize(fields.header_size);
std::memcpy(snapshot.header.data(), static_cast<const void*>(shader), fields.header_size);
{
std::lock_guard lock(mutex);
rethrowFailure();
if (shaders != nullptr) {
const auto found = shaders->find(snapshot.codeAddress);
if (found != shaders->end()) {
const auto& current = *found->second;
if (current.headerAddress == snapshot.headerAddress && current.type == snapshot.type && current.code == snapshot.code && current.header == snapshot.header) {
transaction.Commit();
return;
}
}
}
}
std::shared_ptr<VulkanDevice> localDevice = device.Load();
if (localDevice == nullptr) {
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (device == nullptr) device = std::make_shared<VulkanDevice>();
localDevice = device;
}
snapshot.registeredState = std::make_shared<const RegisteredShaderState>(DecodeRegisteredState(snapshot));
QueueState registered{};
registered.shader = snapshot.registeredState->shader;
registered.context = snapshot.registeredState->context;
registered.userConfig = snapshot.registeredState->userConfig;
snapshot.prepared->entries = PrepareRegistered(snapshot, *localDevice, registered, true);
if ((snapshot.type == 0 || snapshot.type == 1) && !snapshot.prepared->entries.empty()) {
std::vector<std::uint64_t> key;
BuildRegisteredAbiKey(registered, *localDevice, key);
snapshot.prepared->registeredAbis.push_back(std::move(key));
}
static const char* traceRegs = std::getenv("APS5_TRACE_SHADER_REGS");
if (traceRegs != nullptr && (std::string(traceRegs) == "all" || std::strtoull(traceRegs, nullptr, 16) == snapshot.codeAddress)) {
const auto print = [](const ShaderRegister* registers, std::uint32_t count) {
@ -195,18 +728,39 @@ void Driver::RegisterShader(const Shader* shader) {
}
std::lock_guard lock(mutex);
rethrowFailure();
const auto address = snapshot.codeAddress;
if (shaders == nullptr) shaders = std::make_shared<ShaderRegistry>();
else if (shaders.use_count() != 1) shaders = std::make_shared<ShaderRegistry>(*shaders);
shaders->insert_or_assign(address, std::make_shared<const ShaderSnapshot>(std::move(snapshot)));
PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(std::move(snapshot)));
if (shaders->find(NullPixelProgramAddress()) == shaders->end()) {
ShaderSnapshot null{NullPixelProgramAddress(), reinterpret_cast<std::uintptr_t>(&NullPixelShader), NullPixelShader.type, {}, {}};
null.code.assign(std::begin(NullPixelCode), std::end(NullPixelCode));
null.header.resize(sizeof(Shader));
std::memcpy(null.header.data(), &NullPixelShader, sizeof(Shader));
shaders->insert_or_assign(NullPixelProgramAddress(), std::make_shared<const ShaderSnapshot>(std::move(null)));
auto nullRegisteredState = DecodeRegisteredState(null);
nullRegisteredState.shader.emplace(0x008u, static_cast<std::uint32_t>(null.codeAddress >> 8u));
nullRegisteredState.shader.emplace(0x009u, static_cast<std::uint32_t>(null.codeAddress >> 40u));
nullRegisteredState.shader.emplace(0x00bu, 0u);
nullRegisteredState.context.emplace(0x1b3u, 0x2u);
nullRegisteredState.context.emplace(0x1b4u, 0x2u);
null.registeredState = std::make_shared<const RegisteredShaderState>(std::move(nullRegisteredState));
QueueState nullState{};
nullState.shader = null.registeredState->shader;
nullState.context = null.registeredState->context;
nullState.userConfig = null.registeredState->userConfig;
null.prepared->entries = PrepareRegistered(null, *localDevice, nullState, true);
PublishRegisteredShader(shaders, std::make_shared<const ShaderSnapshot>(std::move(null)));
}
transaction.Commit();
}
}
extern "C" AgcDriver::DriverDetail::ShaderPreparationTransaction* AgcDriverBeginShaderPreparation_nid_postfix() {
return new AgcDriver::DriverDetail::ShaderPreparationTransaction();
}
extern "C" void AgcDriverCommitShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) {
transaction->Commit();
}
extern "C" void AgcDriverEndShaderPreparation_nid_postfix(AgcDriver::DriverDetail::ShaderPreparationTransaction* transaction) noexcept {
delete transaction;
}

View File

@ -5,6 +5,8 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include <cstdlib>
#include <mutex>
#include <set>
#ifdef _WIN32
#include <windows.h>
#endif
@ -287,10 +289,14 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
if (!warned && std::chrono::steady_clock::now() - start > std::chrono::milliseconds(waitTimeoutMs())) {
warned = true;
++outcomes.timedOut;
static std::set<std::uint64_t> reported;
static std::uint64_t timeouts = 0;
if (++timeouts % 20 == 0) std::fprintf(stderr, "[gpu] %llu GPU waits have timed out\n", static_cast<unsigned long long>(timeouts));
if (!reported.insert(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u)).second) return;
{
static std::mutex reportedMutex;
static std::set<std::uint64_t> reported;
static std::uint64_t timeouts = 0;
const std::lock_guard lock(reportedMutex);
if (++timeouts % 20 == 0) std::fprintf(stderr, "[gpu] %llu GPU waits have timed out\n", static_cast<unsigned long long>(timeouts));
if (!reported.insert(packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u)).second) return;
}
const bool wide = ((packet[0] >> 8u) & 0xffu) == 0x93u;
const std::uint64_t reference = wide ? packet[4] | (static_cast<std::uint64_t>(packet[5]) << 32u) : packet[4];
const std::uint64_t mask = wide ? packet[6] | (static_cast<std::uint64_t>(packet[7]) << 32u) : packet[5];

View File

@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp"
#include "ThreadOwned.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
@ -26,9 +27,43 @@
#endif
#include <windows.h>
#else
#include <cerrno>
#include <cstdint>
#include <fcntl.h>
#include <fstream>
#include <pthread.h>
#include <sstream>
#include <sys/ioctl.h>
#include <unistd.h>
#if __has_include(<linux/fs.h>)
#include <linux/fs.h>
#endif
#if !defined(PROCMAP_QUERY)
struct procmap_query {
std::uint64_t size;
std::uint64_t query_flags;
std::uint64_t query_addr;
std::uint64_t vma_start;
std::uint64_t vma_end;
std::uint64_t vma_flags;
std::uint64_t vma_page_size;
std::uint64_t vma_offset;
std::uint64_t inode;
std::uint32_t dev_major;
std::uint32_t dev_minor;
std::uint32_t vma_name_size;
std::uint32_t build_id_size;
std::uint64_t vma_name_addr;
std::uint64_t build_id_addr;
};
static_assert(sizeof(procmap_query) == 104);
enum : std::uint64_t {
PROCMAP_QUERY_VMA_READABLE = 0x01,
PROCMAP_QUERY_VMA_WRITABLE = 0x02,
PROCMAP_QUERY_COVERING_OR_NEXT_VMA = 0x10,
};
#define PROCMAP_QUERY _IOWR('f', 17, struct procmap_query)
#endif
#endif
namespace AgcDriver::GuestMemory {
@ -478,6 +513,23 @@ struct PageRun {
bool writable;
};
#if !defined(_WIN32)
int ProcMapsQueryFd() {
static const int fd = [] {
if (std::getenv("APS5_NO_PROCMAP_QUERY") != nullptr) return -1;
const int opened = open("/proc/self/maps", O_RDONLY | O_CLOEXEC);
if (opened < 0) return -1;
procmap_query probe{};
probe.size = sizeof(probe);
probe.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA;
if (ioctl(opened, PROCMAP_QUERY, &probe) == 0 || errno == ENOENT) return opened;
close(opened);
return -1;
}();
return fd;
}
#endif
// Calls `emit` with consecutive runs of uniform accessibility covering [address, address + bytes) in
// order, stopping early when it returns false. Returns false when the address space cannot be queried.
template <class Emit>
@ -535,6 +587,31 @@ bool describePages(std::uintptr_t address, std::size_t bytes, Emit&& emit) {
if (!emit(PageRun{cursor, next, readable, writable})) return true;
cursor = next;
#else
if (const int fd = ProcMapsQueryFd(); fd >= 0) {
procmap_query query{};
query.size = sizeof(query);
query.query_flags = PROCMAP_QUERY_COVERING_OR_NEXT_VMA;
query.query_addr = cursor;
if (ioctl(fd, PROCMAP_QUERY, &query) == 0) {
if (query.vma_end <= cursor || query.vma_start >= query.vma_end) return false;
if (query.vma_start > cursor) {
const auto gapEnd = std::min<std::uintptr_t>(end, query.vma_start);
if (!emit(PageRun{cursor, gapEnd, false, false})) return true;
cursor = gapEnd;
continue;
}
const bool readable = (query.vma_flags & PROCMAP_QUERY_VMA_READABLE) != 0;
const auto next = std::min<std::uintptr_t>(end, query.vma_end);
if (!emit(PageRun{cursor, next, readable, readable && (query.vma_flags & PROCMAP_QUERY_VMA_WRITABLE) != 0})) return true;
cursor = next;
continue;
}
if (errno == ENOENT) {
static_cast<void>(emit(PageRun{cursor, end, false, false}));
return true;
}
return false;
}
std::ifstream maps("/proc/self/maps");
if (!maps.is_open()) return false;
std::string line;
@ -667,9 +744,15 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> CommittedRanges(std::uint64
namespace {
constexpr std::size_t WriteBlockBytes = 65536;
constexpr std::size_t WritePageBytes = 4096;
constexpr std::size_t WritePagesPerBlock = WriteBlockBytes / WritePageBytes;
enum class StampKind : std::uint8_t { Cpu, Driver, ImportWindow };
#ifdef _WIN32
void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation);
#endif
struct WriteTracker {
std::mutex mutex;
bool initialized = false;
@ -684,6 +767,7 @@ struct WriteTracker {
// records in the same block (the title's per-job slots are 0x20 apart) must not count.
std::vector<std::uint32_t> cpuBlocks;
std::vector<std::uint32_t> writtenBlocks;
WriteWatchCoverage coverage;
#else
static constexpr std::size_t LeafBlocks = std::size_t{1} << 16;
static constexpr std::size_t LeafCount = std::size_t{1} << 15;
@ -727,7 +811,9 @@ struct WriteTracker {
blocks.assign(size / WriteBlockBytes + 1, 0);
cpuBlocks.assign(size / WriteBlockBytes + 1, 0);
writtenBlocks.assign(size / WriteBlockBytes + 1, 0);
coverage.Initialize(base, size);
pages.resize(1u << 16);
GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping);
#else
watched = GuestWriteWatch::GuestWriteWatchAvailable_nid_postfix();
if (watched) leaves.resize(LeafCount);
@ -736,7 +822,7 @@ struct WriteTracker {
bool covers(std::uint64_t address, std::size_t bytes) const {
#ifdef _WIN32
return address >= base && address - base <= size - bytes;
return coverage.Covers(address, bytes);
#else
return address + bytes <= LeafCount * LeafBlocks * WriteBlockBytes && GuestWriteWatch::GuestWriteWatchCovers_nid_postfix(static_cast<std::uintptr_t>(address), bytes);
#endif
@ -771,6 +857,24 @@ struct WriteTracker {
};
std::unordered_map<std::uint64_t, DriverPieces> driverPieces;
struct CpuPages {
std::array<std::uint32_t, WritePagesPerBlock> pages{};
};
std::unordered_map<std::uint64_t, CpuPages> cpuPages;
void noteCpuStore(std::uint64_t begin, std::uint64_t end, std::uint32_t stampGeneration) {
for (auto block = blockOf(begin); block <= blockOf(end - 1); ++block) {
const auto blockStart = blockBegin(block);
const auto from = std::max(begin, blockStart) - blockStart;
const auto to = std::min<std::uint64_t>(end, blockStart + WriteBlockBytes) - blockStart;
auto& entry = cpuPages[block];
for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) {
auto& stamp = entry.pages[page];
stamp = std::max(stamp, 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);
@ -851,6 +955,21 @@ std::unique_lock<std::mutex> lockTracker(WriteTracker& tracker) {
return lock;
}
#ifdef _WIN32
void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_t generation) {
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
if (!tracker.watched || bytes == 0 || bytes > tracker.size || address < tracker.base || address - tracker.base > tracker.size - bytes) return;
tracker.coverage.Restore(address, bytes, generation);
++tracker.generation;
for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) {
tracker.driverPieces.erase(block);
tracker.stamp(block, tracker.generation, StampKind::Cpu);
}
unwatchSerial.fetch_add(1, std::memory_order_release);
}
#endif
// Collect epochs are per thread and globally unique: every bump takes a fresh value from one counter,
// so a memo entry (stamped with the epoch of its walk) can only match the thread that made it, and
// only until that thread's next ordering point (the queue workers bump at theirs, see the header). A
@ -894,6 +1013,7 @@ void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) {
auto& [tracker, kind] = *static_cast<StampRuns*>(context);
if (end <= begin) return;
const auto generation = tracker.generation.load(std::memory_order_relaxed);
if (kind != StampKind::Driver) tracker.noteCpuStore(begin, end, generation);
for (auto block = tracker.blockOf(begin); block <= tracker.blockOf(end - 1); ++block) tracker.stamp(block, generation, kind);
collectDirtyRuns.fetch_add(1, std::memory_order_relaxed);
}
@ -918,7 +1038,11 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop,
return false;
}
if (count != 0) dirty = true;
for (ULONG_PTR i = 0; i < count; ++i) tracker.stamp(tracker.blockOf(reinterpret_cast<std::uintptr_t>(tracker.pages[i])), tracker.generation, kind);
for (ULONG_PTR i = 0; i < count; ++i) {
const auto page = reinterpret_cast<std::uintptr_t>(tracker.pages[i]);
tracker.stamp(tracker.blockOf(page), tracker.generation, kind);
if (kind != StampKind::Driver) tracker.noteCpuStore(page, page + WritePageBytes, tracker.generation);
}
if (count < tracker.pages.size()) break;
cursor = reinterpret_cast<std::uintptr_t>(tracker.pages[count - 1]) + (granularity != 0 ? granularity : page);
}
@ -930,7 +1054,11 @@ bool walkWrites(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop,
std::size_t count = tracker.pages.size();
DWORD granularity = 4096;
if (!GuestArena::GuestArenaCollectWrites_nid_postfix(cursor, static_cast<std::size_t>(stop - cursor), tracker.pages.data(), &count, true)) return false;
for (ULONG_PTR i = 0; i < count; ++i) tracker.stamp(tracker.blockOf(reinterpret_cast<std::uintptr_t>(tracker.pages[i])), tracker.generation, kind);
for (ULONG_PTR i = 0; i < count; ++i) {
const auto page = reinterpret_cast<std::uintptr_t>(tracker.pages[i]);
tracker.stamp(tracker.blockOf(page), tracker.generation, kind);
if (kind != StampKind::Driver) tracker.noteCpuStore(page, page + WritePageBytes, tracker.generation);
}
if (count < tracker.pages.size()) break;
cursor = reinterpret_cast<std::uintptr_t>(tracker.pages[count - 1]) + (granularity != 0 ? granularity : page);
}
@ -1011,13 +1139,16 @@ std::uint64_t CollectEpochBumps() {
return collectEpochBumps.load(std::memory_order_relaxed);
}
std::uint64_t CollectEpoch() {
return collectMemoEnabled() ? threadCollectEpoch : 0;
}
namespace {
void unwatchLocked(const WriteTracker& tracker, std::uint64_t address, std::size_t bytes) {
void unwatchLocked(WriteTracker& tracker, std::uint64_t address, std::size_t bytes) {
if (!tracker.watched) return;
#ifdef _WIN32
static_cast<void>(address);
static_cast<void>(bytes);
if (tracker.coverage.Exclude(address, bytes, GuestArena::GuestArenaCommitGeneration_nid_postfix())) unwatchSerial.fetch_add(1, std::memory_order_release);
#else
if (GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(reinterpret_cast<const void*>(address), bytes)) unwatchSerial.fetch_add(1, std::memory_order_release);
#endif
@ -1098,8 +1229,9 @@ bool UnchangedSinceAll(std::span<const UnchangedQuery> queries) {
std::uint64_t storeOwn(std::uint64_t address, std::size_t bytes, const std::function<std::pair<std::uint64_t, std::uint64_t>()>& store) {
if (bytes == 0) return 0;
const auto stampStored = [](WriteTracker& tracker, std::pair<std::uint64_t, std::uint64_t> stored) -> std::uint64_t {
if (stored.second <= stored.first || !tracker.watched || !tracker.covers(stored.first, static_cast<std::size_t>(stored.second - stored.first))) return 0;
if (stored.second <= stored.first) return 0;
++tracker.generation;
if (!tracker.watched || !tracker.covers(stored.first, static_cast<std::size_t>(stored.second - stored.first))) return 0;
for (auto block = tracker.blockOf(stored.first); block <= tracker.blockOf(stored.second - 1); ++block) {
tracker.stamp(block, tracker.generation, StampKind::Driver);
tracker.noteDriverStore(block, stored.first, stored.second, tracker.generation);
@ -1133,10 +1265,11 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
tracker.initialize();
if (!tracker.watched || bytes == 0 || !tracker.covers(address, bytes)) return 0;
if (bytes == 0) return 0;
++tracker.generation;
if (!tracker.watched || !tracker.covers(address, bytes)) return 0;
const auto first = tracker.blockOf(address);
const auto last = tracker.blockOf(address + bytes - 1);
++tracker.generation;
for (auto block = first; block <= last; ++block) {
tracker.stamp(block, tracker.generation, StampKind::Driver);
tracker.noteDriverStore(block, address, address + bytes, tracker.generation);
@ -1154,7 +1287,16 @@ bool StoredOver(std::uint64_t address, std::size_t bytes, std::uint64_t generati
const auto last = tracker.blockOf(end - 1);
for (auto block = first; block <= last; ++block) {
if (tracker.writtenStampOf(block) <= generation) continue;
if (tracker.cpuStampOf(block) > generation) return true;
if (tracker.cpuStampOf(block) > generation) {
const auto begin = tracker.blockBegin(block);
const auto from = std::max(address, begin) - begin;
const auto to = std::min<std::uint64_t>(end, begin + WriteBlockBytes) - begin;
const auto found = tracker.cpuPages.find(block);
if (found == tracker.cpuPages.end()) return true;
for (auto page = from / WritePageBytes; page <= (to - 1) / WritePageBytes; ++page) {
if (found->second.pages[page] > generation) return true;
}
}
const auto found = tracker.driverPieces.find(block);
if (found == tracker.driverPieces.end()) return true;
const auto& entry = found->second;

View File

@ -130,6 +130,22 @@ void copyMemory(std::uint64_t source, std::uint64_t destination, std::size_t byt
GuestMemory::Write(destination, copySource(source, bytes, immediate));
}
constexpr std::uint32_t CopyDataGpuClockSource = 18;
constexpr std::uint32_t CopyDataCachePolicy = (3u << 13u) | (3u << 25u);
std::uint64_t gpuClockCount() {
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now().time_since_epoch()).count() / 10);
}
std::vector<std::byte> copyDataSource(std::span<const std::uint32_t> packet, std::size_t bytes) {
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
if (source != CopyDataGpuClockSource) return copySource(address(packet[2], packet[3]), bytes, source >= 10);
const auto value = gpuClockCount();
std::vector<std::byte> data(bytes);
for (std::size_t i = 0; i < bytes; ++i) data[i] = static_cast<std::byte>(value >> (i * 8));
return data;
}
}
std::string Name(std::uint32_t header) {
@ -417,12 +433,12 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
}
case 0x40: {
size(6);
require((packet[1] & ~0x40110f0fu) == 0, "COPY_DATA engine, cache or reserved fields are not implemented");
require((packet[1] & ~(0x40110f0fu | CopyDataCachePolicy)) == 0, "COPY_DATA engine or reserved fields are not implemented");
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
const auto destination = ((packet[1] >> 8u) & 0xfu) << 1u;
require(destination == 2 || destination == 4, "COPY_DATA register or GDS destination is not implemented");
require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11, "COPY_DATA register, GDS or reference-clock source is not implemented");
require(source < 10 || ((packet[1] & 0x10000u) == 0 && packet[3] == 0), "64-bit immediate COPY_DATA is not implemented");
require(source == 2 || source == 4 || source == 5 || source == 10 || source == 11 || source == CopyDataGpuClockSource, "COPY_DATA register, GDS or reference-clock source is not implemented");
require(source < 10 || source == CopyDataGpuClockSource || ((packet[1] & 0x10000u) == 0 && packet[3] == 0), "64-bit immediate COPY_DATA is not implemented");
break;
}
case 0x50:
@ -550,7 +566,7 @@ std::optional<StoreWrite> ResolveStore(std::span<const std::uint32_t> packet, co
const std::size_t bytes = (packet[1] & 0x10000u) != 0 ? 8 : 4;
const auto destination = address(packet[4], packet[5]);
if (!fits(destination, bytes)) return std::nullopt;
return StoreWrite{destination, {}, copySource(address(packet[2], packet[3]), bytes, source >= 10)};
return StoreWrite{destination, {}, copyDataSource(packet, bytes)};
}
case 0x50: {
if (packet.size() < 7 || dmaDestination(packet) == DmaSelectGds) return std::nullopt;
@ -716,6 +732,28 @@ DrawParameters ResolveDraw(std::span<const std::uint32_t> packet, const QueueSta
return {address, count, indexSize, queue.instanceCount, packet.back(), true, indexOffset->second, 0};
}
std::vector<std::uint32_t> ReadIndirectRegisters(std::span<const std::uint32_t> packet) {
require(packet.size() == 5 && IndirectRegisterOpcode((packet[0] >> 8u) & 0xffu), "expected indirect register packet");
std::vector<std::uint32_t> pairs(static_cast<std::size_t>(packet[4]) * 2);
// Named for the [hooksync] attribution (the read goes through the flush hook).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Registers);
GuestMemory::Read(address(packet[1], packet[2]), std::as_writable_bytes(std::span(pairs)), 4);
return pairs;
}
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue) {
const auto opcode = (packet[0] >> 8u) & 0xffu;
require(packet.size() == 5 && IndirectRegisterOpcode(opcode), "expected indirect register packet");
require(pairs.size() == static_cast<std::size_t>(packet[4]) * 2, "indirect register list does not match its packet");
for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]);
for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
if (queue.savedContext.has_value() && TraceContextState()) {
std::fprintf(stderr, "[context] indirect 0x%x:", opcode);
for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
std::fprintf(stderr, "\n");
}
}
void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
const auto opcode = (packet[0] >> 8u) & 0xffu;
switch (opcode) {
@ -764,20 +802,9 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
case 0x26: queue.indexBase = address(packet[1], packet[2]); return;
case 0x2a: queue.indexType = packet[1]; return;
case 0x2f: queue.instanceCount = packet[1]; return;
case 0x63: case 0x64: case 0x9f: {
std::vector<std::uint32_t> pairs(static_cast<std::size_t>(packet[4]) * 2);
// Named for the [hooksync] attribution (the read goes through the flush hook).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Registers);
GuestMemory::Read(address(packet[1], packet[2]), std::as_writable_bytes(std::span(pairs)), 4);
for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]);
for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
if (queue.savedContext.has_value() && TraceContextState()) {
std::fprintf(stderr, "[context] indirect 0x%x:", opcode);
for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
std::fprintf(stderr, "\n");
}
case 0x63: case 0x64: case 0x9f:
ExecuteIndirectRegisters(packet, ReadIndirectRegisters(packet), queue);
return;
}
case 0x59: break;
case 0x3c: case 0x93: {
// The waited-on value is written by the CPU or another queue; poll it like the CP would.
@ -835,8 +862,10 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
return;
}
case 0x40: {
const auto source = ((packet[1] & 0xfu) << 1u) | ((packet[1] >> 30u) & 1u);
copyMemory(address(packet[2], packet[3]), address(packet[4], packet[5]), (packet[1] & 0x10000u) != 0 ? 8 : 4, source >= 10);
const auto destination = address(packet[4], packet[5]);
const auto data = copyDataSource(packet, (packet[1] & 0x10000u) != 0 ? 8 : 4);
GuestMemory::CheckRange(reinterpret_cast<void*>(destination), data.size(), 1, true);
GuestMemory::Write(destination, data);
return;
}
case 0x50: {

View File

@ -0,0 +1,80 @@
#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
#include "BdaAbi.hpp"
#include <spirv/unified1/spirv.hpp>
#include <algorithm>
namespace AgcDriver {
namespace {
template<typename TFeatures>
const TFeatures* findFeatures(const void* chain, VkStructureType type) {
auto* current = static_cast<const VkBaseInStructure*>(chain);
while (current != nullptr) {
if (current->sType == type) return reinterpret_cast<const TFeatures*>(current);
current = current->pNext;
}
return nullptr;
}
}
ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& target, const VkDeviceCreateInfo& deviceInfo, const VkPhysicalDeviceLimits& limits) : target(target), limits(limits), capabilities(target.supportedCapabilities.begin(), target.supportedCapabilities.end()) {
using Graphics::Require;
Require(deviceInfo.pEnabledFeatures != nullptr, "shader device profile requires enabled core features");
const auto& core = *deviceInfo.pEnabledFeatures;
const auto* robustness = findFeatures<VkPhysicalDeviceRobustness2FeaturesEXT>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT);
nullDescriptors = robustness != nullptr && robustness->nullDescriptor == VK_TRUE;
const auto* bda = findFeatures<VkPhysicalDeviceBufferDeviceAddressFeatures>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_BUFFER_DEVICE_ADDRESS_FEATURES);
const auto* bytes = findFeatures<VkPhysicalDevice8BitStorageFeatures>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_8BIT_STORAGE_FEATURES);
const auto* indexing = findFeatures<VkPhysicalDeviceDescriptorIndexingFeatures>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES);
const auto* barycentric = findFeatures<VkPhysicalDeviceFragmentShaderBarycentricFeaturesKHR>(deviceInfo.pNext, VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_BARYCENTRIC_FEATURES_KHR);
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* 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");
Require(bytes != nullptr && bytes->storageBuffer8BitAccess == VK_TRUE, "shader runtime requires enabled storageBuffer8BitAccess");
Require(nullDescriptors, "shader runtime requires enabled nullDescriptor");
Require(core.vertexPipelineStoresAndAtomics == VK_TRUE && core.fragmentStoresAndAtomics == VK_TRUE, "shader runtime requires enabled graphics stores and atomics");
Require(limits.maxPushConstantsSize >= ShaderRecompiler::RuntimeAbi::PushConstantDwords * sizeof(std::uint32_t) && limits.maxBoundDescriptorSets > ShaderRecompiler::RuntimeAbi::DescriptorSet, "shader runtime ABI exceeds device limits");
Require(limits.maxStorageBufferRange >= sizeof(ShaderRecompiler::RuntimeAbi::ShaderData), "shader runtime ShaderData exceeds device limits");
std::ranges::sort(capabilities);
capabilities.erase(std::unique(capabilities.begin(), capabilities.end()), capabilities.end());
const auto hasCapability = [&](spv::Capability capability) { return std::ranges::binary_search(capabilities, static_cast<std::uint32_t>(capability)); };
Require(hasCapability(spv::CapabilityShader) && hasCapability(spv::CapabilityInt64) && hasCapability(spv::CapabilityPhysicalStorageBufferAddresses) && hasCapability(spv::CapabilityStorageBuffer8BitAccess), "shader device profile is missing runtime capabilities");
const auto checkFeature = [&](spv::Capability capability, bool enabled, const char* name) {
Require(!hasCapability(capability) || enabled, std::string("shader device profile advertises a disabled feature: ") + name);
};
checkFeature(spv::CapabilityFloat64, core.shaderFloat64 == VK_TRUE, "shaderFloat64");
checkFeature(spv::CapabilityTessellation, core.tessellationShader == VK_TRUE, "tessellationShader");
checkFeature(spv::CapabilityImageGatherExtended, core.shaderImageGatherExtended == VK_TRUE, "shaderImageGatherExtended");
checkFeature(spv::CapabilityMinLod, core.shaderResourceMinLod == VK_TRUE, "shaderResourceMinLod");
checkFeature(spv::CapabilityStorageImageMultisample, core.shaderStorageImageMultisample == VK_TRUE, "shaderStorageImageMultisample");
checkFeature(spv::CapabilityStorageImageReadWithoutFormat, core.shaderStorageImageReadWithoutFormat == VK_TRUE, "shaderStorageImageReadWithoutFormat");
checkFeature(spv::CapabilityStorageImageWriteWithoutFormat, core.shaderStorageImageWriteWithoutFormat == VK_TRUE, "shaderStorageImageWriteWithoutFormat");
checkFeature(spv::CapabilitySampledImageArrayDynamicIndexing, core.shaderSampledImageArrayDynamicIndexing == VK_TRUE, "shaderSampledImageArrayDynamicIndexing");
checkFeature(spv::CapabilityStorageImageArrayDynamicIndexing, core.shaderStorageImageArrayDynamicIndexing == VK_TRUE, "shaderStorageImageArrayDynamicIndexing");
const bool sampledNonUniform = indexing != nullptr && indexing->shaderSampledImageArrayNonUniformIndexing == VK_TRUE;
const bool storageNonUniform = indexing != nullptr && indexing->shaderStorageImageArrayNonUniformIndexing == VK_TRUE;
checkFeature(spv::CapabilitySampledImageArrayNonUniformIndexing, sampledNonUniform, "shaderSampledImageArrayNonUniformIndexing");
checkFeature(spv::CapabilityStorageImageArrayNonUniformIndexing, storageNonUniform, "shaderStorageImageArrayNonUniformIndexing");
checkFeature(spv::CapabilityShaderNonUniform, sampledNonUniform || storageNonUniform, "descriptor indexing");
const bool barycentricEnabled = barycentric != nullptr && barycentric->fragmentShaderBarycentric == VK_TRUE;
const bool meshEnabled = mesh != nullptr && mesh->meshShader == VK_TRUE;
checkFeature(spv::CapabilityFragmentBarycentricKHR, barycentricEnabled, "fragmentShaderBarycentric");
checkFeature(spv::CapabilityMeshShadingEXT, meshEnabled, "meshShader");
checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock");
Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics");
Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders");
Require(!target.nonConstantImageOffsets || (maintenance8 != nullptr && maintenance8->maintenance8 == VK_TRUE), "shader device profile exposes disabled maintenance8");
Require(!target.tessellation || core.tessellationShader == VK_TRUE, "shader device profile exposes disabled tessellation");
for (const auto extension : target.supportedExtensions) extensions.emplace_back(extension);
std::ranges::sort(extensions);
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");
this->target.supportedCapabilities = capabilities;
this->target.supportedExtensions = extensionViews;
}
}

View File

@ -201,6 +201,28 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
return true;
}
bool ShaderMemory::accessible(void* context, std::uint64_t address, std::uint64_t bytes) {
auto& self = *static_cast<ShaderMemory*>(context);
if (bytes == 0 || address > std::numeric_limits<std::uint64_t>::max() - bytes) return false;
const auto end = address + bytes;
while (address < end) {
const auto next = self.initial.upper_bound(address);
if (next != self.initial.begin()) {
const auto previous = std::prev(next);
if (address - previous->first < previous->second.size()) {
address = previous->first + previous->second.size();
continue;
}
}
const auto base = address & ~static_cast<std::uint64_t>(PageBytes - 1);
const auto pageEnd = std::min<std::uint64_t>(base + PageBytes, end);
const auto& page = self.page(base);
if (!page.wordwise && !page.valid.all() && !GuestMemory::Accessible(reinterpret_cast<const void*>(address), static_cast<std::size_t>(pageEnd - address))) return false;
address = pageEnd;
}
return true;
}
ShaderMemory::KnownValueCounts ShaderMemory::KnownValues() {
auto& totals = CaptureTotals();
return {totals.wordsKnown.load(std::memory_order_relaxed), totals.wordsKnownVerified.load(std::memory_order_relaxed), totals.wordsKnownMismatches.load(std::memory_order_relaxed)};
@ -212,6 +234,14 @@ void ShaderMemory::CountHandleMemo(bool hit) {
}
std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::Capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle) {
return capture(request, handle, nullptr);
}
std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::Capture(const ShaderRecompiler::PreparedShaderInvocation& invocation) {
return capture(invocation.Request(), nullptr, &invocation);
}
std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::capture(const ShaderRecompiler::RecompileRequest& request, const ShaderRecompiler::SourceHandle* handle, const ShaderRecompiler::PreparedShaderInvocation* invocation) {
// The capture's word and page reads (through `read`) are attributed to it ([hooksync], [guestmem]).
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Capture);
const bool profile = CaptureProfiled();
@ -225,7 +255,8 @@ std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::Capture(c
runtime.userContext = this;
runtime.readMemory = &read;
runtime.readSpecializationMemory = &read;
auto capture = handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime);
runtime.accessible = &accessible;
auto capture = invocation != nullptr ? invocation->Capture(runtime) : handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime);
if (profile) {
totals.captureNanoseconds += NanosecondsSince(started);
totals.resolveNanoseconds += capture->sourceNanoseconds;

View File

@ -0,0 +1,27 @@
#include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp"
#include <algorithm>
#include <array>
namespace AgcDriver {
namespace {
constexpr std::array<std::string_view, 19> NarrowClockFamilies{
"NAVI21", "NAVI22", "VANGOGH", "NAVI23", "NAVI24", "REMBRANDT", "RAPHAEL_MENDOCINO",
"NAVI31", "NAVI32", "NAVI33", "PHOENIX", "PHOENIX2",
"STRIX1", "STRIX_HALO", "KRACKAN1", "GFX1153", "GFX1156",
"GFX1170", "GFX1171",
};
}
bool NarrowSubgroupClock(VkDriverId driver, std::string_view deviceName) {
if (driver != VK_DRIVER_ID_MESA_RADV) return false;
constexpr std::string_view prefix = "(RADV ";
const auto start = deviceName.rfind(prefix);
if (start == std::string_view::npos || deviceName.empty() || deviceName.back() != ')') return false;
const auto family = deviceName.substr(start + prefix.size(), deviceName.size() - start - prefix.size() - 1u);
return std::find(NarrowClockFamilies.begin(), NarrowClockFamilies.end(), family) != NarrowClockFamilies.end();
}
}

View File

@ -1,10 +1,13 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp"
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
#include "BdaAbi.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
#include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
@ -187,12 +190,14 @@ struct VulkanDevice::State {
VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
std::vector<std::uint32_t> capabilities{1};
std::vector<std::string_view> spirvExtensions;
std::unique_ptr<const ShaderDeviceProfile> shaderProfile;
bool tessellationShader = false;
bool meshShader = false;
bool fragmentShaderBarycentric = false;
bool geometryShader = false;
bool sampleRateShading = false;
bool shaderClock = false;
bool narrowSubgroupClock = false;
// VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in
// graphics stages, and compute workgroups wider than a wave).
bool descriptorIndexing = false;
@ -209,13 +214,17 @@ struct VulkanDevice::State {
bool occlusionQueryPrecise = false;
VkDeviceSize hostImportAlignment = 0;
bool dmaBufImport = false;
bool memoryBudget = false;
bool depthRangeUnrestricted = false;
bool samplerAnisotropy = false;
bool textureCompressionBC = false;
bool samplerFilterMinmax = false;
bool fragmentShaderPixelInterlock = false;
bool conservativeRasterization = false;
// VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available.
bool timelineSemaphores = false;
bool computeWave32 = false;
std::uint32_t maxComputeSubgroupSize = 0;
// Indirect draw features enabled (see Graphics::Context).
bool drawIndirectFirstInstance = false;
bool multiDrawIndirect = false;
@ -524,6 +533,7 @@ struct VulkanDevice::State {
resourceCache.Clear();
Graphics::ClearCachedTextures(device);
Graphics::ClearImageMirrors(device);
Graphics::ClearHostImports(device);
patternBuffers.clear();
descriptorCache.reset();
emptyBuffer.reset();
@ -714,6 +724,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->capabilities.push_back(spv::CapabilityGroupNonUniform);
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot);
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_SHUFFLE_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformShuffle);
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_ARITHMETIC_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformArithmetic);
}
APS5_LOG_OUT("Selected GPU name=%s vendor=0x%x device=0x%x subgroup=%u", state->properties.deviceName, state->properties.vendorID, state->properties.deviceID, state->subgroup.subgroupSize);
state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(selected, &state->memoryProperties);
@ -750,12 +761,27 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->fragmentShaderBarycentric = barycentricFeatures.fragmentShaderBarycentric == VK_TRUE;
}
VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT interlockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT};
if (hasExtension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME)) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &interlockFeatures};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->fragmentShaderPixelInterlock = interlockFeatures.fragmentShaderPixelInterlock == VK_TRUE;
}
interlockFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT};
interlockFeatures.fragmentShaderPixelInterlock = VK_TRUE;
VkPhysicalDeviceShaderClockFeaturesKHR clockFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_CLOCK_FEATURES_KHR};
if (hasExtension(VK_KHR_SHADER_CLOCK_EXTENSION_NAME)) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &clockFeatures};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->shaderClock = clockFeatures.shaderSubgroupClock == VK_TRUE && clockFeatures.shaderDeviceClock == VK_TRUE;
}
if (state->shaderClock && hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) {
VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties);
state->narrowSubgroupClock = NarrowSubgroupClock(driver.driverID, state->properties.deviceName);
APS5_LOG_OUT("Shader clock driver=%d narrowSubgroupClock=%d", static_cast<int>(driver.driverID), state->narrowSubgroupClock ? 1 : 0);
}
std::vector<const char*> deviceExtensions;
if (window != nullptr) deviceExtensions.assign(presentationExtensions.begin(), presentationExtensions.end());
if (state->fragmentShaderBarycentric) {
@ -763,6 +789,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->capabilities.push_back(spv::CapabilityFragmentBarycentricKHR);
state->spirvExtensions.push_back("SPV_KHR_fragment_shader_barycentric");
}
if (state->fragmentShaderPixelInterlock) {
deviceExtensions.push_back(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityFragmentShaderPixelInterlockEXT);
state->spirvExtensions.push_back("SPV_EXT_fragment_shader_interlock");
}
if (state->shaderClock) {
deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityShaderClockKHR);
@ -774,6 +805,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
}
const bool bufferInt64Atomics = atomicInt64Features.shaderBufferInt64Atomics == VK_TRUE;
if (bufferInt64Atomics) state->capabilities.push_back(spv::CapabilityInt64Atomics);
atomicInt64Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES_KHR};
atomicInt64Features.shaderBufferInt64Atomics = VK_TRUE;
if (bufferInt64Atomics) deviceExtensions.push_back(VK_KHR_SHADER_ATOMIC_INT64_EXTENSION_NAME);
@ -814,14 +846,23 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->samplerFilterMinmax = minmaxProperties.filterMinmaxSingleComponentFormats == VK_TRUE && minmaxProperties.filterMinmaxImageComponentMapping == VK_TRUE;
if (state->samplerFilterMinmax) deviceExtensions.push_back(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME);
}
if (hasExtension(VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME)) {
VkPhysicalDeviceConservativeRasterizationPropertiesEXT conservativeProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_CONSERVATIVE_RASTERIZATION_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &conservativeProperties};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
state->conservativeRasterization = conservativeProperties.primitiveOverestimationSize <= 1.0f / 256.0f && conservativeProperties.degenerateTrianglesRasterized == VK_TRUE;
if (state->conservativeRasterization) deviceExtensions.push_back(VK_EXT_CONSERVATIVE_RASTERIZATION_EXTENSION_NAME);
}
// Indirect draws with a GPU-side count (DRAW_INDIRECT_MULTI with count_indirect); a device
// without it resolves such draws on the CPU.
state->drawIndirectCount = hasExtension(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME);
if (state->drawIndirectCount) deviceExtensions.push_back(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME);
state->memoryBudget = hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME);
// Guest memory is host memory: importing it lets address-based shaders use it in place instead of
// copying every registered allocation per draw.
if (hasExtension(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME) && std::getenv("APS5_NO_HOST_IMPORT") == nullptr) {
VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT};
VkPhysicalDeviceDriverProperties driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
VkPhysicalDeviceExternalMemoryHostPropertiesEXT hostProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTERNAL_MEMORY_HOST_PROPERTIES_EXT, &driverProperties};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &hostProperties};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
state->hostImportAlignment = hostProperties.minImportedHostPointerAlignment;
@ -834,7 +875,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
#endif
deviceExtensions.push_back(VK_EXT_EXTERNAL_MEMORY_HOST_EXTENSION_NAME);
#ifndef _WIN32
state->dmaBufImport = hasExtension(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME) && hasExtension(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME);
state->dmaBufImport = driverProperties.driverID != VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME) && hasExtension(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME);
if (state->dmaBufImport) {
deviceExtensions.push_back(VK_KHR_EXTERNAL_MEMORY_FD_EXTENSION_NAME);
deviceExtensions.push_back(VK_EXT_EXTERNAL_MEMORY_DMA_BUF_EXTENSION_NAME);
@ -922,6 +963,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
enabled.occlusionQueryPrecise = available.occlusionQueryPrecise;
state->occlusionQueryPrecise = enabled.occlusionQueryPrecise == VK_TRUE;
// Recompiled storage-image access declares no format (the guest descriptor decides it).
enabled.shaderStorageImageMultisample = available.shaderStorageImageMultisample;
if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample);
state->capabilities.push_back(spv::CapabilityImageMSArray);
enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat;
enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat;
// Gathers with non-constant offsets (ImageGatherExtended).
@ -988,6 +1032,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
barycentricFeatures.pNext = byteFeatures.pNext;
byteFeatures.pNext = &barycentricFeatures;
}
if (state->fragmentShaderPixelInterlock) {
interlockFeatures.pNext = byteFeatures.pNext;
byteFeatures.pNext = &interlockFeatures;
}
if (state->shaderClock) {
clockFeatures.pNext = byteFeatures.pNext;
byteFeatures.pNext = &clockFeatures;
@ -1019,7 +1067,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->timelineSemaphores = timelineFeatures.timelineSemaphore == VK_TRUE;
}
VkPhysicalDeviceSubgroupSizeControlFeaturesEXT subgroupSizeFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT};
if (hasExtension(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME) && state->subgroup.subgroupSize > 32u) {
if (hasExtension(VK_EXT_SUBGROUP_SIZE_CONTROL_EXTENSION_NAME) && state->subgroup.subgroupSize >= 32u) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &subgroupSizeFeatures};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
VkPhysicalDeviceSubgroupSizeControlPropertiesEXT subgroupSize{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_PROPERTIES_EXT};
@ -1027,6 +1075,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &sizeProperties);
state->computeWave32 = subgroupSizeFeatures.subgroupSizeControl == VK_TRUE && subgroupSize.minSubgroupSize <= 32u && subgroupSize.maxSubgroupSize >= 32u &&
(subgroupSize.requiredSubgroupSizeStages & VK_SHADER_STAGE_COMPUTE_BIT) != 0;
state->maxComputeSubgroupSize = subgroupSize.maxSubgroupSize;
}
if (state->computeWave32) {
subgroupSizeFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_SIZE_CONTROL_FEATURES_EXT};
@ -1064,14 +1113,33 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
byteFeatures.pNext = &pipelineFeatures;
}
bdaFeatures.pNext = &byteFeatures;
VkPhysicalDeviceRobustness2FeaturesEXT robustness2{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
Graphics::Require(hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME), "shader runtime requires VK_EXT_robustness2");
VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &queried);
Graphics::Require(robustness2.nullDescriptor == VK_TRUE, "shader runtime requires nullDescriptor");
robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
robustness2.nullDescriptor = VK_TRUE;
robustness2.pNext = bdaFeatures.pNext;
bdaFeatures.pNext = &robustness2;
deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME);
deviceInfo.enabledExtensionCount = static_cast<std::uint32_t>(deviceExtensions.size());
deviceInfo.ppEnabledExtensionNames = deviceExtensions.data();
deviceInfo.pNext = &bdaFeatures;
auto shaderProfile = std::make_unique<const ShaderDeviceProfile>(buildTarget(), deviceInfo, state->properties.limits);
check(state->InstanceFunction<PFN_vkCreateDevice>("vkCreateDevice")(selected, &deviceInfo, nullptr, &state->device), "vkCreateDevice");
state->shaderProfile = std::move(shaderProfile);
state->DeviceFunction<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(state->device, family, 0, &state->queue);
APS5_LOG_OUT("Vulkan device ready device=%p queue=%p family=%u", reinterpret_cast<void*>(state->device), reinterpret_cast<void*>(state->queue), family);
VkCommandPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = family;
check(state->DeviceFunction<PFN_vkCreateCommandPool>("vkCreateCommandPool")(state->device, &poolInfo, nullptr, &state->pool), "vkCreateCommandPool");
static const bool procTable = std::getenv("APS5_NO_PROC_TABLE") == nullptr;
if (procTable) {
Graphics::FillDeviceFunctions(graphicsContext(), state->deviceFunctions);
state->functionsReady = true;
}
Graphics::PrepareImportWatch(graphicsContext());
state->bufferPool = std::make_shared<Graphics::BufferPool>(graphicsContext());
state->emptyBuffer = std::make_unique<Graphics::Buffer>(graphicsContext(), Graphics::EmptyBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
@ -1083,14 +1151,6 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
state->samplerCache = std::make_unique<Graphics::SamplerCache>();
state->recorder = std::make_unique<Graphics::Recorder>(graphicsContext(), state->timelineSemaphores);
state->recorder->Activate();
// The entry points every record site uses, resolved once (APS5_NO_PROC_TABLE=1: per call, as
// before), and the context copy graphicsContext() hands out from here on: built after the
// recorder and the descriptor cache exist, so it carries them.
static const bool procTable = std::getenv("APS5_NO_PROC_TABLE") == nullptr;
if (procTable) {
Graphics::FillDeviceFunctions(graphicsContext(), state->deviceFunctions);
state->functionsReady = true;
}
state->context = buildContext();
state->contextReady = true;
if (window != nullptr) {
@ -1914,37 +1974,30 @@ std::shared_ptr<Graphics::StorageTexture> PresentableResident(const Graphics::Co
return resident;
}
Graphics::DccKeys DisplayKeys(const DisplayBuffer& buffer, std::size_t bytes) {
const auto keys = Graphics::CurrentDccKeys(buffer.dccAddress, bytes);
if (keys != Graphics::DccKeys::Uncompressed && !Graphics::IsDccClear(keys)) {
char message[256];
std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads %s DCC keys at 0x%llx: presenting DCC metadata that is not uniformly uncompressed or fast-cleared is not implemented", static_cast<unsigned long long>(buffer.address), Graphics::DccKeysName(keys), static_cast<unsigned long long>(buffer.dccAddress));
throw std::runtime_error(message);
}
return keys;
Graphics::DccKeys ResidentKeys(const Graphics::StorageTexture& resident) {
const auto& own = resident.Descriptor();
GuestMemory::CollectWritesUncached(own.dccAddress, Graphics::DccKeyBytes(resident.GuestBytes()));
return Graphics::ProvedClearKeys(own, resident.GuestBytes(), resident.KeyProof());
}
bool ResidentServesDisplay(const Graphics::StorageTexture& resident, const DisplayBuffer& buffer, std::size_t bytes) {
const auto keys = DisplayKeys(buffer, bytes);
bool ResidentServesDisplay(const Graphics::StorageTexture& resident, const DisplayBuffer& buffer) {
const auto keys = DisplayBufferKeys(buffer);
if (keys == Graphics::DccKeys::ClearRegister) {
char message[320];
std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads register-clear DCC keys at 0x%llx over the pending image 0x%llx (DCC 0x%llx, filled keys %s): whether its results precede the clear is not modeled", static_cast<unsigned long long>(buffer.address), static_cast<unsigned long long>(buffer.dccAddress), static_cast<unsigned long long>(resident.Descriptor().baseAddress), static_cast<unsigned long long>(resident.Descriptor().dccAddress), Graphics::DccKeysName(resident.FilledKeys()));
throw std::runtime_error(message);
}
if (Graphics::IsDccClear(resident.FilledKeys()) && resident.FilledKeys() == keys) return false;
return Graphics::StorageImageServesKeys(resident, buffer.dccAddress);
return Graphics::KeysServeSurface(resident.Descriptor().dccAddress, resident.UploadedKeys(), resident.FilledKeys(), buffer.dccAddress, [&] { return ResidentKeys(resident); }, [&] { return keys; });
}
bool ResidentKeysMoved(const Graphics::StorageTexture& resident) {
const auto& own = resident.Descriptor();
if (own.dccAddress == 0) return false;
GuestMemory::CollectWritesUncached(own.dccAddress, Graphics::DccKeyBytes(resident.GuestBytes()));
return Graphics::ProvedClearKeys(own, resident.GuestBytes(), resident.KeyProof()) != resident.UploadedKeys();
return resident.Descriptor().dccAddress != 0 && ResidentKeys(resident) != resident.UploadedKeys();
}
std::optional<std::array<std::byte, 4>> CompressedClearPixel(const DisplayBuffer& buffer, std::size_t bytes) {
GuestMemory::FlushGpuWrites(buffer.address, bytes);
const auto keys = DisplayKeys(buffer, bytes);
const auto keys = DisplayBufferKeys(buffer);
if (keys == Graphics::DccKeys::Uncompressed) return std::nullopt;
return DisplayBufferClearPixel(buffer, keys);
}
@ -1998,7 +2051,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
if (!NoResidentPresent()) {
bool pending = false;
resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert);
if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer, bytes)) {
if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer)) {
resident.reset();
convert = false;
}
@ -2398,10 +2451,16 @@ std::string VulkanDevice::DeviceName() const {
}
ShaderRecompiler::SpirvTarget VulkanDevice::Target() const {
require(state->shaderProfile != nullptr, "shader device profile is unavailable");
return state->shaderProfile->Target();
}
ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
const auto& limits = state->properties.limits;
ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}};
target.fragmentShaderBarycentricEnabled = state->fragmentShaderBarycentric;
target.nonConstantImageOffsets = state->maintenance8;
target.narrowSubgroupClock = state->narrowSubgroupClock;
target.srgbDecodeFormats = state->srgbDecodeFormats;
if (state->meshShader) {
const auto& mesh = state->meshLimits;
@ -2419,6 +2478,10 @@ bool VulkanDevice::SamplerFilterMinmax() const {
return state->samplerFilterMinmax;
}
bool VulkanDevice::ConservativeRasterization() const {
return state->conservativeRasterization;
}
Graphics::Context VulkanDevice::graphicsContext() const {
static const bool noCache = std::getenv("APS5_NO_CONTEXT_CACHE") != nullptr;
if (state->contextReady && !noCache) return state->context;
@ -2463,6 +2526,7 @@ Graphics::Context VulkanDevice::buildContext() const {
context.depthBounds = state->depthBounds;
context.depthBiasClamp = state->depthBiasClamp;
context.samplerFilterMinmax = state->samplerFilterMinmax;
context.conservativeRasterization = state->conservativeRasterization;
context.drawIndirectCount = state->drawIndirectCount;
context.occlusionQueryPrecise = state->occlusionQueryPrecise;
context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE;
@ -2472,10 +2536,12 @@ Graphics::Context VulkanDevice::buildContext() const {
context.imageInt64Atomics = state->imageInt64Atomics;
context.geometryShader = state->geometryShader;
context.sampleRateShading = state->sampleRateShading;
context.nullDescriptors = state->shaderProfile != nullptr && state->shaderProfile->NullDescriptors();
context.primitiveListRestart = state->primitiveListRestart;
context.imageViewMinLod = state->imageViewMinLod;
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
context.srgbDecodeFormats = state->srgbDecodeFormats;
context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
return context;
}
@ -2492,6 +2558,10 @@ void VulkanDevice::ColorMetadataPass(const Graphics::ColorMetadataPass& pass) {
}
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe) {
for (const auto& shader : shaders) {
require(shader.program != nullptr, "missing compiled shader");
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.program->runtimeAbiVersion);
}
// Two stdout lines per draw cost ~1.3 ms per frame of the queue-0 worker (part of it under the
// GPU mutex); APS5_TRACE_DRAWS=1 restores them.
static const bool trace = std::getenv("APS5_TRACE_DRAWS") != nullptr;
@ -2898,6 +2968,7 @@ struct RecordedDispatch {
DispatchTimer* timer;
// Whether the data refresh recorded anything.
bool refreshed = false;
bool resourceKeyMatched = false;
};
VkDevice VulkanDevice::Device() const {
@ -3001,6 +3072,7 @@ std::uint64_t VulkanDevice::presync(std::span<const std::pair<std::uint64_t, std
}
std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span<const Graphics::GuestMemorySnapshot> snapshots) {
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
// APS5_LOCKED_BUILD=1: the whole build under the mutex, as before the split.
static const bool lockedBuild = std::getenv("APS5_LOCKED_BUILD") != nullptr;
if (lockedBuild || shader.spirv.size() < 5 || shader.spirv[0] != 0x07230203u) return nullptr;
@ -3020,7 +3092,7 @@ std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderReco
prepared->phaseMs[which] += std::chrono::duration<double, std::milli>(now - phaseStart).count();
phaseStart = now;
};
if (ResourceCacheEnabled() && shader.variantId != 0) {
if (ResourceCacheEnabled() && shader.PipelineVariantId() != 0) {
prepared->key = DispatchContentKey(compute, context.device);
phase(PreparedDispatch::PrepareKey);
cached = state->resourceCache.Find(prepared->key);
@ -3216,7 +3288,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
// after an earlier dispatch's reads of the buffers by that dispatch's trailing barrier. A
// recipe compares the two 64-bit hashes first: equal hashes mean the buffers hold the words.
const bool differs = record.dataRefresh == RecordedDispatch::DataRefresh::Words || resources.DataWordsHash() != record.dataWordsHash;
if (differs && resources.RefreshData(commands, *record.shader, &recorder)) {
if (differs && (record.resourceKeyMatched ? resources.refreshData(commands, *record.shader, &recorder) : resources.RefreshData(commands, *record.shader, &recorder))) {
covered = 0;
++d.templateRefreshed;
d.templateRevalidateMs += record.revalidateMs;
@ -3293,6 +3365,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
}
VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipeOut) {
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
PerformanceTimer timing("Vulkan.Dispatch");
if (recipeOut != nullptr) *recipeOut = nullptr;
// Group counts for the trace lines; an indirect dispatch does not know them.
@ -3316,7 +3389,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
}
// Pipelines are shared by dispatches of one compiled variant; descriptor set layouts built from the
// same bindings are compatible, so the pipeline layout of the first dispatch serves them all.
const std::uint64_t pipelineKey = shader.variantId != 0 ? (shader.variantId << 1u) | (pushStages != 0 ? 1u : 0u) : 0u;
const std::uint64_t pipelineKey = shader.PipelineVariantId() != 0 ? (shader.PipelineVariantId() << 1u) | (pushStages != 0 ? 1u : 0u) : 0u;
std::shared_ptr<ComputePipelineObjects> objects;
if (pipelineKey != 0) {
std::lock_guard pipelines(state->computePipelinesMutex);
@ -3332,7 +3405,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
const auto lookupsBefore = Graphics::DeviceProcLookups();
std::shared_ptr<Graphics::ShaderResources> resources;
ResourceCache::Key contentKey;
const bool cacheable = ResourceCacheEnabled() && shader.variantId != 0;
const bool cacheable = ResourceCacheEnabled() && shader.PipelineVariantId() != 0;
// The object came from the resource cache: a compute template whose data buffers take this
// dispatch's words at the record (ShaderResources::RefreshData).
bool fromCache = false;
@ -3485,9 +3558,11 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
pipelineInfo.stage.stage = VK_SHADER_STAGE_COMPUTE_BIT;
pipelineInfo.stage.module = objects->module;
pipelineInfo.stage.pName = "main";
const Graphics::PipelineSpecialization specialization(shader);
pipelineInfo.stage.pSpecializationInfo = specialization.Info();
VkPipelineShaderStageRequiredSubgroupSizeCreateInfoEXT requiredSubgroup{VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_REQUIRED_SUBGROUP_SIZE_CREATE_INFO_EXT};
requiredSubgroup.requiredSubgroupSize = shader.hostSubgroupSize;
if (state->computeWave32 && shader.hostSubgroupSize == 32u) pipelineInfo.stage.pNext = &requiredSubgroup;
requiredSubgroup.requiredSubgroupSize = std::min(shader.hostSubgroupSize, state->maxComputeSubgroupSize);
if (state->computeWave32 && (shader.hostSubgroupSize == 32u || shader.hostSubgroupSize > state->maxComputeSubgroupSize)) pipelineInfo.stage.pNext = &requiredSubgroup;
pipelineInfo.layout = objects->layout;
if (profile && shader.spirv.size() > 100000) std::fprintf(stderr, "[dispatch] creating a pipeline for %zu SPIR-V words (program 0x%llx)\n", shader.spirv.size(), static_cast<unsigned long long>(programAddress));
check(state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines")(state->device, context.pipelineCache, 1, &pipelineInfo, nullptr, &objects->pipeline), "vkCreateComputePipelines");
@ -3506,6 +3581,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
auto& recorder = *state->recorder;
IndirectOutcome outcome{0, 0};
RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer};
record.resourceKeyMatched = fromCache;
if (arguments != 0) decideIndirect(record, outcome, groupsText);
// The indirect hold total below still covers the whole call of a CPU-resolved one.
const bool indirect = record.arguments != 0 || outcome.cpuReason != 0;
@ -3533,7 +3609,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
static auto lastReport = std::chrono::steady_clock::now();
const auto now = std::chrono::steady_clock::now();
const auto ms = std::chrono::duration<double, std::milli>(now - syncStart).count();
auto& waits = byProgram[programAddress != 0 ? programAddress : shader.variantId];
auto& waits = byProgram[programAddress != 0 ? programAddress : shader.PipelineVariantId()];
++waits.count;
waits.ms += ms;
++syncs;
@ -3581,6 +3657,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
}
RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr<RecipeHit>& hit, IndirectOutcome& outcome, const std::shared_ptr<PreparedDispatch>& verify, bool refreshByWords) {
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
PerformanceTimer timing("Vulkan.DispatchRecipe");
outcome = {0, 0};
char groupsText[40];
@ -3630,7 +3707,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu
return RecipeOutcome::Rebuild;
}
std::shared_ptr<ComputePipelineObjects> mapped;
const auto pipelineKey = (shader.variantId << 1u) | (recipe.pushes ? 1u : 0u);
const auto pipelineKey = (shader.PipelineVariantId() << 1u) | (recipe.pushes ? 1u : 0u);
{
std::lock_guard pipelines(state->computePipelinesMutex);
if (const auto found = state->computePipelines.find(pipelineKey); found != state->computePipelines.end()) mapped = found->second;
@ -3675,6 +3752,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu
const auto dataRefresh = !TemplateDataRefresh() ? RecordedDispatch::DataRefresh::None : refreshByWords ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::Hash;
if (dataRefresh == RecordedDispatch::DataRefresh::Words) counters.refreshByWords.fetch_add(1, std::memory_order_relaxed);
RecordedDispatch record{&context, &shaders[0], hit->resources, hit->objects, recipe.pushes ? VkShaderStageFlags{VK_SHADER_STAGE_COMPUTE_BIT} : VkShaderStageFlags{0}, &recipe.pushBytes, x, y, z, arguments, nullptr, programAddress, dataRefresh, recipe.dataWordsHash, 0, &timer};
record.resourceKeyMatched = verify != nullptr;
if (arguments != 0) decideIndirect(record, outcome, groupsText);
const bool indirect = record.arguments != 0 || outcome.cpuReason != 0;
const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};

View File

@ -55,7 +55,8 @@ public:
BufferPool(const BufferPool&) = delete;
BufferPool& operator=(const BufferPool&) = delete;
// The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large.
static std::size_t Capacity(std::size_t bytes);
static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties);
static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties);
std::optional<BufferAllocation> Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
void Put(const BufferAllocation& allocation) noexcept;
@ -92,6 +93,8 @@ private:
Tier& tierFor(std::size_t capacity, VkMemoryPropertyFlags properties);
// The device tier's budget (APS5_STAGING_POOL_MIB), read once.
static VkDeviceSize DeviceBudget();
static bool DeviceTiered(VkMemoryPropertyFlags properties);
static std::size_t NextCapacity(std::size_t capacity);
void destroy(const BufferAllocation& allocation) noexcept;
// Moves the tier's least recently used slot (the oldest front of its lists) to `evicted`; the
// caller destroys those after releasing the mutex, so builds taking buffers on other threads
@ -116,6 +119,7 @@ private:
static constexpr VkDeviceSize smallBudget = 64ull * 1024 * 1024;
// Requests of this size and more keep their exact size and go to the large tier.
static constexpr std::size_t classLimit = std::size_t{1} << 20u;
static constexpr unsigned deviceClassBits = 3;
static constexpr std::size_t defaultSlots = 4096;
};

View File

@ -12,11 +12,12 @@ namespace AgcDriver::Graphics {
enum class ColorTileMode : std::uint32_t {
Linear = 0,
Standard4KB = 5,
Standard64KB = 9,
RenderTarget = 0x1b
};
constexpr TextureTileMode ColorTextureTileMode(ColorTileMode mode) {
return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : TextureTileMode::kR64KBX;
return mode == ColorTileMode::Linear ? TextureTileMode::kLinear : mode == ColorTileMode::Standard4KB ? TextureTileMode::kStandard4KB : mode == ColorTileMode::Standard64KB ? TextureTileMode::kStandard64KB : TextureTileMode::kR64KBX;
}
ColorTileMode DecodeColorTileMode(std::uint32_t attrib3);

View File

@ -107,6 +107,7 @@ struct Context {
// Nonzero when VK_EXT_external_memory_host is enabled: the required host pointer alignment.
VkDeviceSize hostImportAlignment = 0;
bool dmaBufImport = false;
PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr;
RenderCache* renderCache = nullptr;
DrawQueue* drawQueue = nullptr;
GraphicsPipelineCache* graphicsPipelines = nullptr;
@ -125,6 +126,7 @@ struct Context {
bool depthBounds = false;
bool depthBiasClamp = false;
bool samplerFilterMinmax = false;
bool conservativeRasterization = false;
VkBuffer emptyBuffer = VK_NULL_HANDLE;
// The device's list of recorded dispatches whose copied written buffers await a CPU write-back
// (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect
@ -138,6 +140,7 @@ struct Context {
bool imageInt64Atomics = false;
bool geometryShader = false;
bool sampleRateShading = false;
bool nullDescriptors = false;
bool primitiveListRestart = false;
bool imageViewMinLod = false;
bool pipelineExecutableInfo = false;

View File

@ -23,6 +23,8 @@ enum class DccKeys { Uncompressed, Clear0000, Clear0001, Clear1110, Clear1111, C
const char* DccKeysName(DccKeys keys);
std::size_t DccKeyBytes(std::uint64_t surfaceBytes);
std::size_t DccKeyCount(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t width, std::uint32_t height, std::uint64_t surfaceBytes);
std::size_t DccKeyCount(const GuestTextureResource& surface, std::uint64_t surfaceBytes);
template<typename ReadFollowed, typename ReadNamed>
bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys filled, std::uint64_t namedDcc, ReadFollowed&& readFollowed, ReadNamed&& readNamed) {
@ -35,15 +37,18 @@ bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys fille
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
bool IsDccClear(DccKeys keys);
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount);
// Stores "uncompressed" keys over the surface's metadata on the CPU (a guest memory write: it waits
// for recorded GPU work that writes the keys first).
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount);
// The same after a write-back recorded under GuestMemory::GpuMutex: when the keys are in host-imported
// memory and a recorder is active, the store is a fill recorded into the open batch (ordered behind
// the title's key-writing kernels like the write-back itself, nothing waits on the CPU) and the range
// reads as uncompressed from ReadDccKeys while that batch is pending; otherwise the CPU store above.
// APS5_CPU_DCC_KEYS=1 always stores on the CPU.
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes);
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount);
// Fills `bytes` with the texel a 0000/0001/1110/1111 clear code stands for ("1" is 1.0 or the integer
// maximum; the alpha channel is the last one in memory when alphaOnMsb, else the first, and 3-channel
// formats have none). False when the format has no encoding here.

View File

@ -28,7 +28,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);
void KeepDrawInput(Recorder* recorder, std::uint64_t address, const DrawInputCopy& copy, Recorder::SnapshotUse use, std::uint32_t derived);
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress);
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw);
struct MeshArguments {
std::uint32_t groups;

View File

@ -150,9 +150,12 @@ struct MirrorStats {
std::uint64_t rebuilds = 0;
std::uint64_t blocksCopied = 0;
std::uint64_t heapRefills = 0;
std::uint64_t sweeps = 0;
std::uint64_t heapChecks = 0;
};
MirrorStats MirrorCounters();
void ClearImageMirrors(VkDevice device);
void ClearHostImports(VkDevice device);
struct AddressCopy {
std::uint64_t begin;
@ -203,6 +206,7 @@ public:
void UploadPrepare(bool addressable);
void UploadFinish(bool addressable);
VkDescriptorBufferInfo Descriptor(std::uint64_t address, std::size_t bytes, std::uint32_t& adjustment) const;
static std::uint64_t ViewBytes(std::uint64_t bytes, std::uint32_t adjustment);
std::vector<ShaderRecompiler::BdaAbi::Range> AddressRanges() const;
// The BDA table of the cached address space when it serves this upload alone (an address-based
// build with no region outside it): its ranges, immutable while the space lives, and the
@ -344,6 +348,8 @@ private:
// The cached address space this build maps through (its lease pins the ranges); `regions` then
// holds only the regions outside it (V#s, snapshots, ranges copied per build).
std::shared_ptr<const AddressSpace> space;
mutable std::uint64_t writeTableSerial = 0;
mutable std::shared_ptr<const std::vector<ShaderRecompiler::BdaAbi::Range>> writeTableRanges;
// Import registry epoch when `direct` pointers were taken at acquire time; they are reused while
// no import was destroyed since.
std::uint64_t importsEpoch = 0;

View File

@ -81,6 +81,7 @@ struct GuestTextureResource {
// DCC metadata of a compressed surface, or 0 (see DccMetadata.hpp).
std::uint64_t dccAddress = 0;
bool dccAlphaOnMsb = false;
bool dccPipeAligned = false;
std::uint32_t minLod = 0;
std::uint32_t allocatedMipCount = 0;
};

View File

@ -0,0 +1,76 @@
#ifndef CORE_LIBS_AGCDRIVER_GRAPHICS_PIPELINESPECIALIZATION_HPP
#define CORE_LIBS_AGCDRIVER_GRAPHICS_PIPELINESPECIALIZATION_HPP
#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp"
#include <limits>
#include <map>
#include <set>
#include <spirv/unified1/spirv.hpp>
#include <vector>
namespace AgcDriver::Graphics {
class PipelineSpecialization {
public:
static void Validate(const ShaderRecompiler::RecompileResult& shader) {
const auto& words = shader.spirv;
Require(words.size() >= 5 && words[0] == spv::MagicNumber && words[3] != 0, "invalid specialization module header");
std::set<std::uint32_t> unsignedTypes;
std::map<std::uint32_t, std::uint32_t> constants;
std::map<std::uint32_t, std::uint32_t> decorations;
std::set<std::uint32_t> constantIds;
std::set<std::uint32_t> supplied;
for (const auto& constant : shader.specialization) Require(supplied.insert(constant.id).second, "duplicate pipeline specialization ID");
for (std::size_t cursor = 5; cursor < words.size();) {
const auto count = words[cursor] >> 16u;
const auto op = static_cast<spv::Op>(words[cursor] & 0xffffu);
Require(count != 0 && count <= words.size() - cursor, "truncated specialization module instruction");
const auto validId = [&](std::uint32_t id) { Require(id != 0 && id < words[3], "specialization ID exceeds module bound"); };
if (op == spv::OpTypeInt) {
Require(count == 4, "malformed integer type");
if (words[cursor + 2] == 32u && words[cursor + 3] == 0u) unsignedTypes.insert(words[cursor + 1]);
} else if (op == spv::OpSpecConstant) {
Require(count == 4, "specialization constant must have a 32-bit literal");
validId(words[cursor + 2]);
Require(constants.emplace(words[cursor + 2], words[cursor + 1]).second, "duplicate specialization result ID");
} else if (op == spv::OpDecorate && count >= 3 && words[cursor + 2] == spv::DecorationSpecId) {
Require(count == 4, "malformed SpecId decoration");
validId(words[cursor + 1]);
Require(decorations.emplace(words[cursor + 1], words[cursor + 3]).second && constantIds.insert(words[cursor + 3]).second, "duplicate SpecId decoration");
} else if (op == spv::OpSpecConstantTrue || op == spv::OpSpecConstantFalse || op == spv::OpSpecConstantComposite || op == spv::OpSpecConstantOp) {
throw std::runtime_error("AGC graphics: unsupported specialization constant representation");
}
cursor += count;
}
Require(constants.size() == decorations.size(), "specialization declarations and SpecId decorations disagree");
for (const auto& [id, type] : constants) {
Require(unsignedTypes.contains(type), "specialization constant must be uint32");
const auto decoration = decorations.find(id);
Require(decoration != decorations.end() && supplied.contains(decoration->second), "required pipeline specialization value is missing");
}
}
explicit PipelineSpecialization(const ShaderRecompiler::RecompileResult& shader) {
Validate(shader);
entries.reserve(shader.specialization.size());
for (std::size_t index = 0; index < shader.specialization.size(); ++index) {
const auto offset = index * sizeof(ShaderRecompiler::PipelineSpecializationConstant) + offsetof(ShaderRecompiler::PipelineSpecializationConstant, value);
Require(offset <= std::numeric_limits<std::uint32_t>::max(), "pipeline specialization offset exceeds Vulkan limits");
entries.push_back({shader.specialization[index].id, static_cast<std::uint32_t>(offset), sizeof(std::uint32_t)});
}
info.mapEntryCount = static_cast<std::uint32_t>(entries.size());
info.pMapEntries = entries.data();
info.dataSize = shader.specialization.size() * sizeof(ShaderRecompiler::PipelineSpecializationConstant);
info.pData = shader.specialization.data();
}
[[nodiscard]] const VkSpecializationInfo* Info() const { return entries.empty() ? nullptr : &info; }
private:
std::vector<VkSpecializationMapEntry> entries;
VkSpecializationInfo info{};
};
}
#endif

View File

@ -67,7 +67,7 @@ public:
// continuing draw the command buffer without ending it.
bool ContinuesRenderPass(std::uint64_t key) const;
VkCommandBuffer CommandsInRenderPass();
void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool continuable);
void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool continuable, std::function<void(VkCommandBuffer)> afterPass = {});
// DCC "uncompressed" key stores (DccMetadata.cpp StoreUncompressedOnGpu): queued on the open
// batch and recorded as one run (one barrier pair for every queued fill) at Submit, before a
// label store (RecordStore), or before a command that writes or reads a queued range (the
@ -86,14 +86,10 @@ public:
bool Idle() const { return open == nullptr && inFlight.empty(); }
// Whether recorded work still has completion actions (write-backs the CPU must see) to run.
bool HasCompletions() const;
// The object lives at least until the batch open now completed; it is then destroyed AFTER the
// finishing thread released GuestMemory::GpuMutex, on a release thread of its own (never under
// a hold, see Recorder.cpp ReleaseDeferredKeeps; the finishing thread destroys it itself when
// the thread's queue is full, APS5_RELEASE_QUEUE_MAX batches, or with APS5_RELEASE_ON_UNLOCK=1;
// APS5_RELEASE_UNDER_LOCK=1 destroys it in the reap as before), so its destructor must need
// neither the mutex nor the recorder nor a particular thread. ~Recorder joins the release
// thread and waits for every release in progress before the device goes.
void Keep(std::shared_ptr<void> object);
void Keep(std::shared_ptr<void> object, std::size_t bytes = 0);
static constexpr std::size_t KeptBytesBudget = std::size_t{512} << 20u;
void BoundKeptBytes();
std::size_t InFlightKeptBytes() const { return inFlightKeptBytes; }
enum class SnapshotUse : std::uint8_t { Storage, Vertex, Index16, Index32 };
static constexpr std::size_t DrawSnapshotBudget = std::size_t{256} << 20u;
static constexpr std::size_t DrawSnapshotEntries = 1024;
@ -499,6 +495,7 @@ private:
VkCommandBuffer commands = VK_NULL_HANDLE;
VkFence fence = VK_NULL_HANDLE;
std::vector<std::shared_ptr<void>> kept;
std::size_t keptBytes = 0;
std::vector<std::function<void()>> completions;
std::vector<std::pair<std::uint64_t, std::uint64_t>> writes;
std::vector<std::uint64_t> writeNotes;
@ -590,6 +587,7 @@ private:
bool continuable = false;
std::uint64_t key = 0;
std::uint32_t timing = NoTiming;
std::function<void(VkCommandBuffer)> afterPass;
} renderPass;
// A pass ended in this batch: Submit records the host-read barrier its draws left out.
bool hostReadOwed = false;
@ -685,7 +683,7 @@ private:
// Returns false when the locked path must run instead (no timeline, nested acquisition, off).
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);
void finish(std::unique_ptr<Batch> batch, bool wait, int source, bool releaseNow = false);
void release(Batch& batch) noexcept;
static bool overlaps(const Batch& batch, std::uint64_t address, std::uint64_t end);
// The first noted read of `batch` overlapping [address, end), or null.
@ -740,6 +738,7 @@ private:
// snapshot (a rebuild from inside a completion must not drop them) until finish returns.
std::vector<const Batch*> finishing;
std::uint64_t submissions = 0;
std::size_t inFlightKeptBytes = 0;
std::uint64_t writeNoteCount = 0;
// Command buffers and fences of completed batches, reused by later ones (hundreds of batches per
// frame would otherwise allocate and free their objects each time).

View File

@ -11,14 +11,14 @@
namespace AgcDriver::Graphics {
ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& context, const std::array<std::uint8_t, 8>& exportMappings, bool nullProgram = false);
ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader);
ShaderRecompiler::ShaderComputeStageInfo DecodeComputeStageInfo(const Registers& shader, std::span<const std::byte> header);
// A guest range DecodeVertexStageInfo read (an attribute word, a vertex V#) with the bytes as read:
// the draw cache validates them by value with the stage's capture (design_cpu_final rule RD).
struct DecodeRead {
std::uint64_t address;
std::vector<std::byte> bytes;
};
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads = nullptr);
ShaderRecompiler::ShaderVertexStageInfo DecodeVertexStageInfo(std::span<const std::byte> header, std::uint64_t headerAddress, std::span<const std::uint32_t> userData, std::vector<DecodeRead>* reads = nullptr, bool staticAbi = false);
}

View File

@ -18,12 +18,25 @@
#include <unordered_map>
#include <vector>
namespace AgcDriver { class VulkanDevice; }
namespace AgcDriver::Graphics {
class Recorder;
void FlushCachedTextures(VkDevice device);
void ClearCachedTextures(VkDevice device);
std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory);
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);
struct TextureCacheUse {
std::size_t sampledEntries = 0;
std::uint64_t sampledBytes = 0;
std::uint64_t storageBytes = 0;
};
TextureCacheUse TextureCacheUsage();
std::uint64_t SampledTextureCacheBudget(const Context& context);
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, 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.
@ -60,9 +73,6 @@ public:
VkDescriptorSet set = VK_NULL_HANDLE;
VkDescriptorPool pool = VK_NULL_HANDLE;
};
// A set of `layout` needing `sizes` descriptors from the pool chain, opening a pool when no pool
// has room; a null set when the needs exceed what one chain pool holds (the caller then makes a
// dedicated pool, as before).
SetAllocation Allocate(VkDescriptorSetLayout layout, std::span<const VkDescriptorPoolSize> sizes);
void Free(const SetAllocation& allocation) noexcept;
// APS5_PROFILE_DRAW counters: layouts served from the map / created, sets allocated, pools opened.
@ -71,6 +81,7 @@ public:
std::uint64_t layoutMisses = 0;
std::uint64_t sets = 0;
std::uint64_t pools = 0;
std::uint64_t dedicatedPools = 0;
};
Stats Counters() const;
@ -82,6 +93,7 @@ private:
mutable std::mutex mutex;
std::map<std::vector<std::uint32_t>, VkDescriptorSetLayout> layouts;
std::vector<VkDescriptorPool> pools;
std::vector<VkDescriptorPool> dedicated;
Stats stats;
};
@ -228,6 +240,8 @@ public:
std::vector<std::pair<std::uint64_t, std::uint64_t>> PresyncSurfaces() const;
private:
friend class AgcDriver::VulkanDevice;
bool refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder);
struct DescribedRange {
const char* kind;
std::uint64_t address;
@ -396,7 +410,6 @@ private:
// Whether the layout is this object's own (no cache) and destroyed with it.
bool ownsLayout = false;
VkDescriptorSet _set = VK_NULL_HANDLE;
// Dedicated pool of this object's set (no cache, or a set too large for a cache pool).
VkDescriptorPool pool = VK_NULL_HANDLE;
// The cache pool the set was allocated from, freed back to it on release.
VkDescriptorPool cachePool = VK_NULL_HANDLE;
@ -414,6 +427,7 @@ private:
std::vector<bool> storageAtomic;
std::vector<bool> storageAtomic64;
std::vector<std::shared_ptr<Sampler>> samplers;
std::shared_ptr<Sampler> paddingSampler;
bool reusable = false;
std::vector<DirectRegion> directRegions;
std::vector<ValidatedSurface> validatedTextures;
@ -434,6 +448,7 @@ private:
// still to look up (index into `bindings`; the DescriptorBinding lives in the compiled shader),
// the descriptor counts the set was sized for, and the compute stage of a deferred build.
std::vector<Binding> bindings;
std::vector<std::uint32_t> refreshResourceKey;
struct DeferredImages {
const ShaderRecompiler::DescriptorBinding* binding;
std::size_t index;

View File

@ -41,6 +41,9 @@ struct ColorTarget {
// DCC metadata of a compressed target (CB_COLOR_INFO DCC_ENABLE), or 0 (see DccMetadata.hpp).
std::uint64_t dccAddress = 0;
bool dccAlphaOnMsb = false;
bool dccPipeAligned = false;
std::uint64_t cmaskAddress = 0;
std::size_t cmaskBytes = 0;
std::uint64_t surfaceAddress = 0;
VkExtent2D surfaceExtent{};
std::uint32_t mipCount = 1;
@ -89,6 +92,7 @@ struct State {
VkViewport viewport;
bool negativeOneToOne;
bool depthClamp = false;
VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT;
VkRect2D scissor;
VkCullModeFlags cullMode;
VkFrontFace frontFace;
@ -100,6 +104,8 @@ ShaderStages DecodeShaderStages(const QueueState& queue);
State DecodeState(const QueueState& queue);
std::array<std::uint8_t, 8> ExportMappings(const State& state);
ColorTarget DecodeColorBuffer(const Registers& context, std::uint32_t slot);
std::size_t CmaskBytes(std::uint32_t width, std::uint32_t height);
std::uint32_t ColorWriteMask(const Registers& context);
struct ColorMetadataPass {
enum class Mode { EliminateFastClear, DccDecompress };
@ -107,11 +113,12 @@ struct ColorMetadataPass {
std::vector<ColorTarget> targets;
};
std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue);
std::string DepthMaintenanceRejection(const QueueState& queue);
// The message DecodeState (or the pixel stage decode after it) would throw for the register rules
// this precheck covers, evaluated without exceptions before the draw is decoded; empty when they
// pass (DecodeState still checks everything). A register a rule needs that is absent is no verdict.
std::string DrawRejection(const QueueState& queue, bool indexed);
bool PixelProgramUnset(const QueueState& queue);
bool PixelProgramSkipped(const QueueState& queue);
std::string NullPixelProgramRejection(const QueueState& queue);
// The recording facade of the draw decoders (design_cpu_final M8, step 8a): every register read

View File

@ -34,6 +34,7 @@ bool StorageFormatAvailable(const Context& context, std::uint32_t guestFormat);
// Whether a storage image of the guest format takes DCC clear `keys` as a GPU clear (see
// StorageTexture::upload); false for integer formats and non-clear keys.
bool StorageClearAvailable(const Context& context, std::uint32_t guestFormat, DccKeys keys);
std::uint64_t SampledTextureMemory();
// A sampled texture's own VkImage with its memory, shared with the recorder while a recorded upload
// still writes it (see the snapshot constructor), so the texture may go before the batch completes.
@ -101,12 +102,15 @@ private:
ViewRange firstLayerRange{};
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
VkDeviceSize allocationBytes = 0;
VkDeviceSize countedBytes = 0;
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
std::shared_ptr<ResidentColor> source;
std::shared_ptr<StorageTexture> storageSource;
std::unique_ptr<CommandBatch> upload;
};
VkFormat AttachmentProxyFormat(const Context& context, VkFormat format);
// A guest texture a shader writes through a storage image. It is uploaded like a sampled texture;
// after the GPU work completes its results are stored to guest memory (retiled, changed bytes only),
// either at once (WriteBack) or deferred: MarkDirty keeps them on the GPU until something reads that
@ -131,6 +135,9 @@ public:
// buffer's format and mark the image dirty like a storage write.
bool Attachable() const { return attachable; }
VkImageView AttachmentView(VkFormat format, std::uint32_t mip = 0, std::uint32_t depthSlice = 0);
VkImageView AttachmentProxyView();
void RecordAttachmentProxyLoad(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
void RecordAttachmentProxyStore(VkCommandBuffer commands, VkImageLayout attachmentLayout) const;
void WriteBack();
// Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead).
void MarkDirty();
@ -285,6 +292,7 @@ public:
// Keeps the image current with guest memory (see GuestMemory::CollectWrites).
bool Refresh();
std::uint64_t GuestBytes() const;
VkDeviceSize AllocationBytes() const { return memoryBytes; }
private:
// The regions of every array layer, or of the tracked layers `layers` selects.
@ -292,6 +300,8 @@ private:
// Uploads the surface, or only the tracked layers `layers` selects (the direct path; the others
// upload everything).
void upload(const std::vector<bool>* layers = nullptr);
void captureGuestBytes(const std::vector<bool>* layers);
bool compareUntracked(std::uint64_t address, std::size_t bytes, std::span<std::uint8_t> changed, bool memoize = false) const;
// Stores the pending tracked layers overlapping [address, address + bytes) to guest memory; in
// each, 64 KiB blocks the CPU wrote since the layer's generation keep the CPU's bytes. Block
// units asked for in pieces too often are all stored at once for a while (the hysteresis:
@ -420,9 +430,10 @@ private:
std::uint64_t sliceLinearBytes = 0;
SurfaceGeometry geometry;
std::vector<std::byte> original;
mutable std::array<std::uint64_t, 4> comparedGuestBytes{};
// DCC keys the image content was uploaded under: a fast-cleared surface starts as its clear value.
DccKeys uploadedKeys = DccKeys::Uncompressed;
DccKeys filledKeys = DccKeys::Uncompressed;
mutable DccKeys filledKeys = DccKeys::Uncompressed;
mutable DccKeyProof keyProof;
struct ForeignKeyProof {
std::uint64_t dccAddress = 0;
@ -452,6 +463,7 @@ private:
bool lent = false;
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkDeviceSize memoryBytes = 0;
VkImageView view = VK_NULL_HANDLE;
std::uint32_t defaultMip = 0;
std::map<std::uint32_t, VkImageView> extraViews;
@ -460,6 +472,9 @@ private:
std::map<std::pair<std::uint32_t, bool>, VkImageView> uintViews;
bool attachable = false;
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
VkImage proxyImage = VK_NULL_HANDLE;
VkDeviceMemory proxyMemory = VK_NULL_HANDLE;
VkImageView proxyView = VK_NULL_HANDLE;
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
// Results are on the GPU only (guarded by the pending-write registry lock).
bool dirty = false;

View File

@ -20,37 +20,39 @@ struct VertexFormat {
inline VertexFormat DecodeVertexFormat(const ShaderRecompiler::VertexAttribute& attribute) {
Require(attribute.components >= 1 && attribute.components <= 4, "invalid vertex attribute component count");
const auto components = attribute.formatComponents == 0u ? attribute.components : attribute.formatComponents;
Require(components >= 1u && components <= 4u, "invalid vertex format component count");
const auto format = (attribute.resource.fields[3] >> 12u) & 0x7fu;
switch (format) {
case 1: { const std::array formats{VK_FORMAT_R8_UNORM}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 2: { const std::array formats{VK_FORMAT_R8_SNORM}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 3: { const std::array formats{VK_FORMAT_R8_USCALED}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 4: { const std::array formats{VK_FORMAT_R8_SSCALED}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 5: { const std::array formats{VK_FORMAT_R8_UINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 6: { const std::array formats{VK_FORMAT_R8_SINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 7: { const std::array formats{VK_FORMAT_R16_UNORM}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 8: { const std::array formats{VK_FORMAT_R16_SNORM}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 9: { const std::array formats{VK_FORMAT_R16_USCALED}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 10: { const std::array formats{VK_FORMAT_R16_SSCALED}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 11: { const std::array formats{VK_FORMAT_R16_UINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 12: { const std::array formats{VK_FORMAT_R16_SINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 13: { const std::array formats{VK_FORMAT_R16_SFLOAT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 14: { const std::array formats{VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 15: { const std::array formats{VK_FORMAT_R8_SNORM, VK_FORMAT_R8G8_SNORM}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 16: { const std::array formats{VK_FORMAT_R8_USCALED, VK_FORMAT_R8G8_USCALED}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 17: { const std::array formats{VK_FORMAT_R8_SSCALED, VK_FORMAT_R8G8_SSCALED}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 18: { const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 19: { const std::array formats{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 20: { const std::array formats{VK_FORMAT_R32_UINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 21: { const std::array formats{VK_FORMAT_R32_SINT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 22: { const std::array formats{VK_FORMAT_R32_SFLOAT}; const auto count = std::min(attribute.components, 1u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 23: { const std::array formats{VK_FORMAT_R16_UNORM, VK_FORMAT_R16G16_UNORM}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 24: { const std::array formats{VK_FORMAT_R16_SNORM, VK_FORMAT_R16G16_SNORM}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 25: { const std::array formats{VK_FORMAT_R16_USCALED, VK_FORMAT_R16G16_USCALED}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 26: { const std::array formats{VK_FORMAT_R16_SSCALED, VK_FORMAT_R16G16_SSCALED}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 27: { const std::array formats{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 28: { const std::array formats{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 29: { const std::array formats{VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 1: { const std::array formats{VK_FORMAT_R8_UNORM}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 2: { const std::array formats{VK_FORMAT_R8_SNORM}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 3: { const std::array formats{VK_FORMAT_R8_USCALED}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 4: { const std::array formats{VK_FORMAT_R8_SSCALED}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 5: { const std::array formats{VK_FORMAT_R8_UINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 6: { const std::array formats{VK_FORMAT_R8_SINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 7: { const std::array formats{VK_FORMAT_R16_UNORM}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 8: { const std::array formats{VK_FORMAT_R16_SNORM}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 9: { const std::array formats{VK_FORMAT_R16_USCALED}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 10: { const std::array formats{VK_FORMAT_R16_SSCALED}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 11: { const std::array formats{VK_FORMAT_R16_UINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 12: { const std::array formats{VK_FORMAT_R16_SINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 13: { const std::array formats{VK_FORMAT_R16_SFLOAT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 14: { const std::array formats{VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 15: { const std::array formats{VK_FORMAT_R8_SNORM, VK_FORMAT_R8G8_SNORM}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 16: { const std::array formats{VK_FORMAT_R8_USCALED, VK_FORMAT_R8G8_USCALED}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 17: { const std::array formats{VK_FORMAT_R8_SSCALED, VK_FORMAT_R8G8_SSCALED}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 18: { const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 19: { const std::array formats{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 20: { const std::array formats{VK_FORMAT_R32_UINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 21: { const std::array formats{VK_FORMAT_R32_SINT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 22: { const std::array formats{VK_FORMAT_R32_SFLOAT}; const auto count = std::min(components, 1u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 23: { const std::array formats{VK_FORMAT_R16_UNORM, VK_FORMAT_R16G16_UNORM}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 24: { const std::array formats{VK_FORMAT_R16_SNORM, VK_FORMAT_R16G16_SNORM}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 25: { const std::array formats{VK_FORMAT_R16_USCALED, VK_FORMAT_R16G16_USCALED}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 26: { const std::array formats{VK_FORMAT_R16_SSCALED, VK_FORMAT_R16G16_SSCALED}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 27: { const std::array formats{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 28: { const std::array formats{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 29: { const std::array formats{VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 36: return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4u, 4u, "f32"};
case 50: return {VK_FORMAT_A2B10G10R10_UNORM_PACK32, 4u, 4u, "f32"};
case 51: return {VK_FORMAT_A2B10G10R10_SNORM_PACK32, 4u, 4u, "f32"};
@ -58,28 +60,28 @@ inline VertexFormat DecodeVertexFormat(const ShaderRecompiler::VertexAttribute&
case 53: return {VK_FORMAT_A2B10G10R10_SSCALED_PACK32, 4u, 4u, "f32"};
case 54: return {VK_FORMAT_A2B10G10R10_UINT_PACK32, 4u, 4u, "u32"};
case 55: return {VK_FORMAT_A2B10G10R10_SINT_PACK32, 4u, 4u, "i32"};
case 56: { const std::array formats{VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8B8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 57: { const std::array formats{VK_FORMAT_R8_SNORM, VK_FORMAT_R8G8_SNORM, VK_FORMAT_R8G8B8_SNORM, VK_FORMAT_R8G8B8A8_SNORM}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 58: { const std::array formats{VK_FORMAT_R8_USCALED, VK_FORMAT_R8G8_USCALED, VK_FORMAT_R8G8B8_USCALED, VK_FORMAT_R8G8B8A8_USCALED}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 59: { const std::array formats{VK_FORMAT_R8_SSCALED, VK_FORMAT_R8G8_SSCALED, VK_FORMAT_R8G8B8_SSCALED, VK_FORMAT_R8G8B8A8_SSCALED}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 60: { const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT, VK_FORMAT_R8G8B8_UINT, VK_FORMAT_R8G8B8A8_UINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 61: { const std::array formats{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT, VK_FORMAT_R8G8B8_SINT, VK_FORMAT_R8G8B8A8_SINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 62: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 63: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 64: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT}; const auto count = std::min(attribute.components, 2u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 65: { const std::array formats{VK_FORMAT_R16_UNORM, VK_FORMAT_R16G16_UNORM, VK_FORMAT_R16G16B16_UNORM, VK_FORMAT_R16G16B16A16_UNORM}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 66: { const std::array formats{VK_FORMAT_R16_SNORM, VK_FORMAT_R16G16_SNORM, VK_FORMAT_R16G16B16_SNORM, VK_FORMAT_R16G16B16A16_SNORM}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 67: { const std::array formats{VK_FORMAT_R16_USCALED, VK_FORMAT_R16G16_USCALED, VK_FORMAT_R16G16B16_USCALED, VK_FORMAT_R16G16B16A16_USCALED}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 68: { const std::array formats{VK_FORMAT_R16_SSCALED, VK_FORMAT_R16G16_SSCALED, VK_FORMAT_R16G16B16_SSCALED, VK_FORMAT_R16G16B16A16_SSCALED}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 69: { const std::array formats{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT, VK_FORMAT_R16G16B16_UINT, VK_FORMAT_R16G16B16A16_UINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 70: { const std::array formats{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT, VK_FORMAT_R16G16B16_SINT, VK_FORMAT_R16G16B16A16_SINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 71: { const std::array formats{VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R16G16B16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 72: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT}; const auto count = std::min(attribute.components, 3u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 73: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT}; const auto count = std::min(attribute.components, 3u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 74: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT}; const auto count = std::min(attribute.components, 3u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 75: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT, VK_FORMAT_R32G32B32A32_UINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 76: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT, VK_FORMAT_R32G32B32A32_SINT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 77: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT}; const auto count = std::min(attribute.components, 4u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 56: { const std::array formats{VK_FORMAT_R8_UNORM, VK_FORMAT_R8G8_UNORM, VK_FORMAT_R8G8B8_UNORM, VK_FORMAT_R8G8B8A8_UNORM}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 57: { const std::array formats{VK_FORMAT_R8_SNORM, VK_FORMAT_R8G8_SNORM, VK_FORMAT_R8G8B8_SNORM, VK_FORMAT_R8G8B8A8_SNORM}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 58: { const std::array formats{VK_FORMAT_R8_USCALED, VK_FORMAT_R8G8_USCALED, VK_FORMAT_R8G8B8_USCALED, VK_FORMAT_R8G8B8A8_USCALED}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 59: { const std::array formats{VK_FORMAT_R8_SSCALED, VK_FORMAT_R8G8_SSCALED, VK_FORMAT_R8G8B8_SSCALED, VK_FORMAT_R8G8B8A8_SSCALED}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "f32"}; }
case 60: { const std::array formats{VK_FORMAT_R8_UINT, VK_FORMAT_R8G8_UINT, VK_FORMAT_R8G8B8_UINT, VK_FORMAT_R8G8B8A8_UINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "u32"}; }
case 61: { const std::array formats{VK_FORMAT_R8_SINT, VK_FORMAT_R8G8_SINT, VK_FORMAT_R8G8B8_SINT, VK_FORMAT_R8G8B8A8_SINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 1u, 1u, "i32"}; }
case 62: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 63: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 64: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT}; const auto count = std::min(components, 2u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 65: { const std::array formats{VK_FORMAT_R16_UNORM, VK_FORMAT_R16G16_UNORM, VK_FORMAT_R16G16B16_UNORM, VK_FORMAT_R16G16B16A16_UNORM}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 66: { const std::array formats{VK_FORMAT_R16_SNORM, VK_FORMAT_R16G16_SNORM, VK_FORMAT_R16G16B16_SNORM, VK_FORMAT_R16G16B16A16_SNORM}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 67: { const std::array formats{VK_FORMAT_R16_USCALED, VK_FORMAT_R16G16_USCALED, VK_FORMAT_R16G16B16_USCALED, VK_FORMAT_R16G16B16A16_USCALED}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 68: { const std::array formats{VK_FORMAT_R16_SSCALED, VK_FORMAT_R16G16_SSCALED, VK_FORMAT_R16G16B16_SSCALED, VK_FORMAT_R16G16B16A16_SSCALED}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 69: { const std::array formats{VK_FORMAT_R16_UINT, VK_FORMAT_R16G16_UINT, VK_FORMAT_R16G16B16_UINT, VK_FORMAT_R16G16B16A16_UINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "u32"}; }
case 70: { const std::array formats{VK_FORMAT_R16_SINT, VK_FORMAT_R16G16_SINT, VK_FORMAT_R16G16B16_SINT, VK_FORMAT_R16G16B16A16_SINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "i32"}; }
case 71: { const std::array formats{VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R16G16B16_SFLOAT, VK_FORMAT_R16G16B16A16_SFLOAT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 2u, 2u, "f32"}; }
case 72: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT}; const auto count = std::min(components, 3u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 73: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT}; const auto count = std::min(components, 3u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 74: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT}; const auto count = std::min(components, 3u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
case 75: { const std::array formats{VK_FORMAT_R32_UINT, VK_FORMAT_R32G32_UINT, VK_FORMAT_R32G32B32_UINT, VK_FORMAT_R32G32B32A32_UINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 4u, 4u, "u32"}; }
case 76: { const std::array formats{VK_FORMAT_R32_SINT, VK_FORMAT_R32G32_SINT, VK_FORMAT_R32G32B32_SINT, VK_FORMAT_R32G32B32A32_SINT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 4u, 4u, "i32"}; }
case 77: { const std::array formats{VK_FORMAT_R32_SFLOAT, VK_FORMAT_R32G32_SFLOAT, VK_FORMAT_R32G32B32_SFLOAT, VK_FORMAT_R32G32B32A32_SFLOAT}; const auto count = std::min(components, 4u); return {formats[count - 1], count * 4u, 4u, "f32"}; }
default: throw std::runtime_error("AGC graphics: unsupported vertex format " + std::to_string(format));
}
}

View File

@ -5,6 +5,7 @@
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <limits>
namespace AgcDriver::Graphics {
@ -16,6 +17,13 @@ bool sharedTiers() {
return shared;
}
bool exactSizes() {
static const bool exact = std::getenv("APS5_NO_BUFFER_CLASSES") != nullptr;
return exact;
}
constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
}
BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")), maxSlots(std::max<std::size_t>(1, std::min<std::size_t>(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))) {
@ -47,21 +55,35 @@ void BufferPool::destroy(const BufferAllocation& allocation) noexcept {
freeMemory(device, allocation.memory, nullptr);
}
std::size_t BufferPool::Capacity(std::size_t bytes) {
static const bool exact = std::getenv("APS5_NO_BUFFER_CLASSES") != nullptr;
bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) {
return (properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0 && DeviceBudget() != 0;
}
std::size_t BufferPool::Capacity(std::size_t bytes, VkMemoryPropertyFlags properties) {
constexpr std::size_t smallest = 256;
if (exact || bytes >= classLimit) return bytes;
return std::max(smallest, std::bit_ceil(bytes));
if (exactSizes()) return bytes;
if (bytes < classLimit) return std::max(smallest, std::bit_ceil(bytes));
if (!DeviceTiered(properties)) return bytes;
const auto step = std::bit_floor(bytes) >> deviceClassBits;
return (bytes + step - 1) / step * step;
}
std::size_t BufferPool::NextCapacity(std::size_t capacity) {
return capacity < classLimit ? capacity * 2 : capacity + (std::bit_floor(capacity) >> deviceClassBits);
}
VkBufferUsageFlags BufferPool::Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) {
return DeviceTiered(properties) ? usage | deviceUsage : usage;
}
BufferPool::Tier& BufferPool::tierFor(std::size_t capacity, VkMemoryPropertyFlags properties) {
if ((properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0 && DeviceBudget() != 0) return deviceTier;
if (DeviceTiered(properties)) return deviceTier;
return !sharedTiers() && capacity < classLimit ? smallTier : largeTier;
}
std::optional<BufferAllocation> BufferPool::Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto capacity = Capacity(bytes);
const auto capacity = Capacity(bytes, properties);
std::lock_guard lock(mutex);
++clock;
if (profile) {
@ -73,7 +95,9 @@ std::optional<BufferAllocation> BufferPool::Take(std::size_t bytes, VkBufferUsag
}
}
auto& tier = tierFor(capacity, properties);
const auto found = tier.free.find({capacity, usage, properties});
const auto largest = &tier == &deviceTier && !exactSizes() && bytes <= std::numeric_limits<std::size_t>::max() / 2 ? std::max(capacity, bytes * 2) : capacity;
auto found = tier.free.end();
for (auto size = capacity; size <= largest && found == tier.free.end(); size = NextCapacity(size)) found = tier.free.find({size, usage, properties});
if (found == tier.free.end() || found->second.empty()) {
++tier.misses;
return std::nullopt;

View File

@ -27,6 +27,16 @@ std::uint32_t standardOffset(std::uint32_t x, std::uint32_t y, std::uint32_t ele
}
}
std::uint32_t standard64Extra(std::uint32_t x, std::uint32_t y, std::uint32_t elementBytes) {
switch (elementBytes) {
case 1u: return ((x << 7) & 0x2000u) ^ ((x << 8) & 0x8000u) ^ ((y << 6) & 0x1000u) ^ ((y << 7) & 0x4000u);
case 2u: return ((x << 7) & 0x2000u) ^ ((x << 8) & 0x8000u) ^ ((y << 7) & 0x1000u) ^ ((y << 8) & 0x4000u);
case 4u: return ((x << 8) & 0x2000u) ^ ((x << 9) & 0x8000u) ^ ((y << 7) & 0x1000u) ^ ((y << 8) & 0x4000u);
case 8u: return ((x << 8) & 0x2000u) ^ ((x << 9) & 0x8000u) ^ ((y << 8) & 0x1000u) ^ ((y << 9) & 0x4000u);
default: return ((x << 9) & 0x2000u) ^ ((x << 10) & 0x8000u) ^ ((y << 8) & 0x1000u) ^ ((y << 9) & 0x4000u);
}
}
}
ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) {
@ -35,7 +45,7 @@ ColorTileMode DecodeColorTileMode(std::uint32_t attrib3) {
const auto mode = (attrib3 >> 14u) & 0x1fu;
const auto fmaskMode = (attrib3 >> 19u) & 0x1fu;
require(fmaskMode == 0 || fmaskMode == 0x18, "AGC graphics: unsupported color FMASK swizzle mode");
require(mode == 0 || mode == 5 || mode == 0x1b, "AGC graphics: unsupported color tile mode");
require(mode == 0 || mode == 5 || mode == 9 || mode == 0x1b, "AGC graphics: unsupported color tile mode");
return static_cast<ColorTileMode>(mode);
}
@ -70,18 +80,18 @@ const SwizzleTables& renderTargetTables(std::uint32_t bytesPerElement, std::uint
return tables[index];
}
const SwizzleTables& standardTables(std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight) {
static std::once_flag once[5];
static SwizzleTables tables[5];
const SwizzleTables& standardTables(std::uint32_t bytesPerElement, std::uint32_t blockWidth, std::uint32_t blockHeight, bool block64KB) {
static std::once_flag once[2][5];
static SwizzleTables tables[2][5];
const auto index = static_cast<std::size_t>(std::countr_zero(bytesPerElement));
std::call_once(once[index], [&] {
auto& table = tables[index];
std::call_once(once[block64KB][index], [&] {
auto& table = tables[block64KB][index];
table.x.resize(blockWidth);
table.y.resize(blockHeight);
for (std::uint32_t x = 0; x < blockWidth; ++x) table.x[x] = standardOffset(x, 0, bytesPerElement);
for (std::uint32_t y = 0; y < blockHeight; ++y) table.y[y] = standardOffset(0, y, bytesPerElement);
for (std::uint32_t x = 0; x < blockWidth; ++x) table.x[x] = standardOffset(x, 0, bytesPerElement) ^ (block64KB ? standard64Extra(x, 0, bytesPerElement) : 0u);
for (std::uint32_t y = 0; y < blockHeight; ++y) table.y[y] = standardOffset(0, y, bytesPerElement) ^ (block64KB ? standard64Extra(0, y, bytesPerElement) : 0u);
});
return tables[index];
return tables[block64KB][index];
}
}
@ -108,13 +118,16 @@ ColorTargetLayout::ColorTargetLayout(std::uint32_t width, std::uint32_t height,
yOffsets = tables.y.data();
break;
}
case ColorTileMode::Standard4KB: {
case ColorTileMode::Standard4KB:
case ColorTileMode::Standard64KB: {
const auto log2Bytes = static_cast<std::uint32_t>(std::countr_zero(bytesPerElement));
blockWidth = 1u << (6u - (log2Bytes + 1u) / 2u);
blockHeight = 1u << (6u - log2Bytes / 2u);
const bool block64KB = mode == ColorTileMode::Standard64KB;
const auto log2Side = block64KB ? 8u : 6u;
blockWidth = 1u << (log2Side - log2Bytes / 2u);
blockHeight = 1u << (log2Side - (log2Bytes + 1u) / 2u);
pitch = (width + blockWidth - 1u) / blockWidth * blockWidth;
paddedHeight = (height + blockHeight - 1u) / blockHeight * blockHeight;
const auto& tables = standardTables(bytesPerElement, blockWidth, blockHeight);
const auto& tables = standardTables(bytesPerElement, blockWidth, blockHeight, block64KB);
xOffsets = tables.x.data();
yOffsets = tables.y.data();
break;

View File

@ -392,13 +392,27 @@ std::size_t DccKeyBytes(std::uint64_t surfaceBytes) {
return static_cast<std::size_t>(surfaceBytes / KeyBytes);
}
std::size_t DccKeyCount(TextureTileMode tileMode, std::uint32_t elementBytes, std::uint32_t width, std::uint32_t height, std::uint64_t surfaceBytes) {
const auto keys = DccKeyBytes(surfaceBytes);
if (tileMode != TextureTileMode::kR64KBX || elementBytes != 4) return keys;
constexpr std::uint64_t MetaBlockEdge = 512;
constexpr std::uint64_t MetaBlockBytes = 4096;
const auto console = (width + MetaBlockEdge - 1) / MetaBlockEdge * ((height + MetaBlockEdge - 1) / MetaBlockEdge) * MetaBlockBytes;
return std::max(keys, static_cast<std::size_t>(console));
}
std::size_t DccKeyCount(const GuestTextureResource& surface, std::uint64_t surfaceBytes) {
const bool single = surface.mipCount == 1 && surface.baseArray == 0 && surface.depthOrLastArray == 0 && (surface.dimension == TextureDimension::k2D || surface.dimension == TextureDimension::k2DArray);
if (!single || !surface.dccPipeAligned || surface.tileMode != TextureTileMode::kR64KBX) return DccKeyBytes(surfaceBytes);
return DccKeyCount(surface.tileMode, BytesPerElement(surface.format), surface.width, surface.height, surfaceBytes);
}
namespace {
// ReadDccKeys, saying in `memoized` whether the answer came from the pending-store memo rather
// than the bytes.
DccKeys readDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, bool& memoized) {
DccKeys readDccKeys(std::uint64_t metaAddress, std::size_t count, bool& memoized) {
memoized = false;
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count)) return DccKeys::Unreadable;
// A driver store still pending on the GPU is not in the bytes yet.
if (const auto memo = MemoizedKeys(metaAddress, metaAddress + count)) {
@ -430,7 +444,7 @@ DccKeys readDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, bool&
DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t guestBytes, bool& memoized) {
memoized = false;
if (resource.dccAddress == 0) return DccKeys::Uncompressed;
const auto keys = readDccKeys(resource.dccAddress, guestBytes, memoized);
const auto keys = readDccKeys(resource.dccAddress, DccKeyBytes(guestBytes), memoized);
if (keys == DccKeys::Uncompressed) return keys;
if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented");
std::byte probe[16]{};
@ -444,15 +458,7 @@ DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t gue
return keys;
}
}
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
bool memoized = false;
return readDccKeys(metaAddress, surfaceBytes, memoized);
}
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
DccKeys currentDccKeys(std::uint64_t metaAddress, std::size_t count) {
if (metaAddress != 0 && count != 0 && GuestMemory::GpuMutex().HeldByThisThread()) {
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count)) {
if (const auto keys = PendingStoreKeys(*recorder, metaAddress, count)) return *keys;
@ -460,7 +466,35 @@ DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
recorder->SyncThrough(metaAddress, count);
}
}
return ReadDccKeys(metaAddress, surfaceBytes);
bool memoized = false;
return readDccKeys(metaAddress, count, memoized);
}
std::size_t StoredKeyCount(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) {
const auto keys = DccKeyBytes(surfaceBytes);
Require(keyCount >= keys, "a DCC key range covers fewer keys than one per 256 surface bytes");
if (metaAddress == 0 || keyCount == 0) return 0;
if (GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), keyCount, true)) return keyCount;
if (keyCount == keys) return 0;
char message[256];
std::snprintf(message, sizeof(message), "AGC graphics: DCC metadata 0x%llx is not writable over the 0x%zx key bytes of its surface (0x%zx at one per 256 bytes): metadata smaller than the console's DCC extent is not modeled", static_cast<unsigned long long>(metaAddress), keyCount, keys);
throw std::runtime_error(message);
}
}
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
bool memoized = false;
return readDccKeys(metaAddress, DccKeyBytes(surfaceBytes), memoized);
}
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
return currentDccKeys(metaAddress, DccKeyBytes(surfaceBytes));
}
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) {
Require(keyCount >= DccKeyBytes(surfaceBytes), "a DCC key range covers fewer keys than one per 256 surface bytes");
return currentDccKeys(metaAddress, keyCount);
}
bool IsDccClear(DccKeys keys) {
@ -468,8 +502,12 @@ bool IsDccClear(DccKeys keys) {
}
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count, true)) return;
MarkDccUncompressed(metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes));
}
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) {
const auto count = StoredKeyCount(metaAddress, surfaceBytes, keyCount);
if (count == 0) return;
// The bytes are trusted only when no recorded work writes them (lock-free, with or without
// GuestMemory::GpuMutex); otherwise the store runs and waits through the flush hook. A pending
// GPU store of the driver's own is no reason to skip: a title kernel recorded into the same
@ -479,8 +517,12 @@ void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes)
}
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
const auto count = static_cast<std::size_t>(surfaceBytes / KeyBytes);
if (metaAddress == 0 || count == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(metaAddress), count, true)) return;
MarkDccUncompressed(context, metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes));
}
void MarkDccUncompressed(const Context& context, std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount) {
const auto count = StoredKeyCount(metaAddress, surfaceBytes, keyCount);
if (count == 0) return;
auto* recorder = CpuKeysOnly() || !GuestMemory::GpuMutex().HeldByThisThread() ? nullptr : Recorder::Active();
// The bytes are scanned only when no recorded work writes them: a pending clear kernel's keys are
// not in memory yet, so the bytes could read as uncompressed while the kernel will store a clear

View File

@ -42,7 +42,7 @@ std::uint32_t GuestFormatFor(VkFormat format, std::uint32_t elementBytes) {
// The color buffer as a single-mip 2D surface descriptor (tile mode SW_64KB_R_X).
GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
Require(color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB, "only 4 KiB standard and 64 KiB tiled color targets are resident");
Require(color.tileMode != ColorTileMode::Linear, "linear color targets are not resident");
const bool chain = color.mipCount > 1;
GuestTextureResource surface{};
surface.baseAddress = chain ? color.surfaceAddress : color.address;
@ -62,6 +62,7 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
surface.dstSelW = 7;
surface.dccAddress = color.dccAddress;
surface.dccAlphaOnMsb = color.dccAlphaOnMsb;
surface.dccPipeAligned = color.dccPipeAligned;
return surface;
}
@ -69,6 +70,11 @@ GuestTextureResource SurfaceForTarget(const ColorTarget& color) {
namespace {
std::size_t colorKeyCount(const ColorTarget& color, std::size_t bytes) {
if (color.mipCount != 1 || color.depth != 1 || !color.dccPipeAligned) return DccKeyBytes(bytes);
return DccKeyCount(ColorTextureTileMode(color.tileMode), color.elementBytes, color.extent.width, color.extent.height, bytes);
}
std::array<std::byte, 16> clearTexel(const ColorTarget& color, DccKeys keys) {
std::array<std::byte, 16> texel{};
const auto elementBytes = static_cast<std::size_t>(color.elementBytes);
@ -90,16 +96,19 @@ bool clearToTexel(StorageTexture& image, const std::array<std::byte, 16>& texel,
return image.FillClear(std::span<const std::uint32_t, 4>(pattern), StorageTexture::WholeImage, refusal);
}
void writeTexels(const ColorTarget& color, const std::array<std::byte, 16>& texel) {
const auto elementBytes = static_cast<std::size_t>(color.elementBytes);
std::vector<std::byte> texels(color.bytes);
for (std::size_t offset = 0; offset + elementBytes <= texels.size(); offset += elementBytes) std::memcpy(texels.data() + offset, texel.data(), elementBytes);
GuestMemory::Write(color.address, texels);
}
void storeClearTexels(const Context& context, const ColorTarget& color, const std::array<std::byte, 16>& texel) {
StorageTexture::FlushPending(color.address, color.bytes, nullptr, "fast-clear materialization");
const auto keys = ReadDccKeys(color.dccAddress, color.bytes);
if (!IsDccClear(keys)) return;
const auto current = keys == DccKeys::ClearRegister ? texel : clearTexel(color, keys);
const auto elementBytes = static_cast<std::size_t>(color.elementBytes);
std::vector<std::byte> texels(color.bytes);
for (std::size_t offset = 0; offset + elementBytes <= texels.size(); offset += elementBytes) std::memcpy(texels.data() + offset, current.data(), elementBytes);
GuestMemory::Write(color.address, texels);
MarkDccUncompressed(context, color.dccAddress, color.bytes);
writeTexels(color, keys == DccKeys::ClearRegister ? texel : clearTexel(color, keys));
MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes));
}
void materializeRegisterClear(const Context& context, const ColorTarget& color, StorageTexture& resident) {
@ -118,13 +127,31 @@ void materializeRegisterClear(const Context& context, const ColorTarget& color,
}
}
if (cleared) {
MarkDccUncompressed(context, color.dccAddress, color.bytes);
MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes));
return;
}
storeClearTexels(context, color, texel);
resident.Refresh();
}
bool materializeCmaskClear(const Context& context, const ColorTarget& color, StorageTexture* resident) {
if (color.cmaskAddress == 0) return false;
const auto metadataBytes = static_cast<std::uint64_t>(color.cmaskBytes) * 256u;
const auto state = CurrentDccKeys(color.cmaskAddress, metadataBytes);
if (state == DccKeys::Uncompressed) return false;
Require(color.dccAddress == 0, std::string("CMASK of a DCC color target that is not all expanded is not modeled (") + DccKeysName(state) + ")");
Require(state == DccKeys::Clear0000, std::string("CMASK whose tiles are not all fast-cleared or all expanded is not modeled (") + DccKeysName(state) + ")");
const auto texel = clearTexel(color, DccKeys::ClearRegister);
const char* refusal = nullptr;
if (resident == nullptr || !clearToTexel(*resident, texel, color.elementBytes, refusal)) {
StorageTexture::FlushPending(color.address, color.bytes, nullptr, "fast-clear materialization");
writeTexels(color, texel);
if (resident != nullptr) resident->Refresh();
}
MarkDccUncompressed(context, color.cmaskAddress, metadataBytes);
return true;
}
void imageBarrier(const Context& context, VkCommandBuffer commands, VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout, VkPipelineStageFlags sourceStage, VkPipelineStageFlags destinationStage, VkAccessFlags sourceAccess, VkAccessFlags destinationAccess) {
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
barrier.srcAccessMask = sourceAccess;
@ -438,12 +465,12 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
Require(shader.program != nullptr, "missing compiled shader");
const auto& program = *shader.program;
const bool generated = state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation);
if (!generated && program.variantId == 0) {
if (!generated && program.PipelineVariantId() == 0) {
keyed = false;
break;
}
add(shader.stage);
add(generated ? std::uint64_t{0} : program.variantId);
add(generated ? std::uint64_t{0} : program.PipelineVariantId());
add(shader.pushConstantOffset);
add(program.pushConstants.size());
add(program.bdaAbiVersion);
@ -452,6 +479,7 @@ bool ValidationKey(const Context& context, std::span<const CompiledShader> shade
add(attribute.location);
add(attribute.components);
add(attribute.fetchIndex);
add(attribute.formatComponents);
// The data format bits of the V# decide the attribute's signature.
add((attribute.resource.fields[3] >> 12u) & 0x7fu);
}
@ -601,11 +629,12 @@ void CheckBufferAliases(std::span<const CompiledShader> shaders, const ColorTarg
}
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw, std::uint64_t unreadAddress) {
const auto address = draw.indexed ? draw.indexAddress : unreadAddress;
const auto bytes = draw.indexed ? (static_cast<std::uint64_t>(draw.indexCount) * draw.indexSize + 3u) & ~std::uint64_t{3} : 4u;
Require(address != 0 && bytes != 0 && bytes <= 0xffffffffu && (address >> 48u) == 0, "invalid mesh index buffer range");
std::array<std::uint32_t, 4> MeshIndexBufferDescriptor(const Pm4::DrawParameters& draw) {
constexpr std::uint32_t RawWord3 = 0x31016facu;
if (!draw.indexed) return {0u, 0u, 0u, RawWord3};
const auto address = draw.indexAddress;
const auto bytes = (static_cast<std::uint64_t>(draw.indexCount) * draw.indexSize + 3u) & ~std::uint64_t{3};
Require(address != 0 && bytes != 0 && bytes <= 0xffffffffu && (address >> 48u) == 0, "invalid mesh index buffer range");
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u) & 0xffffu, static_cast<std::uint32_t>(bytes), RawWord3};
}
@ -895,6 +924,7 @@ std::shared_ptr<StorageTexture> refreshResidentTarget(const Context& context, co
std::lock_guard lock(reportMutex);
if (reported.insert(color.address).second) std::fprintf(stderr, "[gpu] color target 0x%llx stays non-resident: %s\n", static_cast<unsigned long long>(color.address), error.what());
}
if (resident != nullptr) materializeCmaskClear(context, color, resident.get());
if (profile) {
const auto lookupUs = std::chrono::duration<double, std::micro>(std::chrono::steady_clock::now() - lookupStart).count();
++outcome.targetLookups;
@ -934,7 +964,7 @@ ResolvedResources resolveDrawResources(const Context& context, const State& stat
// The render target and index ranges stay out of the key (see DrawResourceKey); a hit repeats
// the alias checks instead. Debug aid: APS5_NO_DRAW_KEY_TRIM=1 keys them as before.
static const bool trimKey = std::getenv("APS5_NO_DRAW_KEY_TRIM") == nullptr;
resolved.cacheable = recordable && !noDrawResourceCache && !noTextureCache && std::all_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->variantId != 0; });
resolved.cacheable = recordable && !noDrawResourceCache && !noTextureCache && std::all_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->PipelineVariantId() != 0; });
if (resolved.cacheable) {
resolved.contentKey = DrawResourceKey(context, shaders, state.color, draw.indexAddress, indexBytes, !trimKey);
if (auto cached = SharedResourceCache().Find(resolved.contentKey)) {
@ -1182,6 +1212,7 @@ struct RecordedDraw {
std::shared_ptr<Framebuffer> framebuffer;
std::span<const VkImageView> targetViews;
std::vector<std::shared_ptr<StorageTexture>> targets;
std::vector<std::shared_ptr<StorageTexture>> proxies;
const IndirectRecord* indirect = nullptr;
std::span<const ShaderResources::MovedBuffer> moved;
bool listed = false;
@ -1358,6 +1389,10 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
if (continued) {
record.pipeline->Continue(commands, state);
} else {
for (const auto& proxy : record.proxies) {
proxy->RecordAttachmentProxyLoad(commands, VK_IMAGE_LAYOUT_GENERAL);
countBarrier(2);
}
const VkMemoryBarrier before{VK_STRUCTURE_TYPE_MEMORY_BARRIER, nullptr, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | VK_ACCESS_INDEX_READ_BIT | VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT | VK_ACCESS_UNIFORM_READ_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT};
context.Resolved(&DeviceFunctions::cmdPipelineBarrier, "vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT | VK_PIPELINE_STAGE_VERTEX_INPUT_BIT | inputs.shaderStages, 0, 1, &before, 0, nullptr, 0, nullptr);
countBarrier(1);
@ -1391,7 +1426,16 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
// The pass stays open for the next draw of these attachments; the recorder ends it (and the
// draw class range) before anything else is recorded. A draw that wrote memory owes the next
// one a barrier, so its pass cannot be continued.
recorder->LeaveRenderPassOpen(passKey, drawTiming, !resources.WritesMemory());
std::function<void(VkCommandBuffer)> storeProxies;
if (!continued && !record.proxies.empty()) {
storeProxies = [proxies = record.proxies](VkCommandBuffer passCommands) {
for (const auto& proxy : proxies) {
proxy->RecordAttachmentProxyStore(passCommands, VK_IMAGE_LAYOUT_GENERAL);
Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
}
};
}
recorder->LeaveRenderPassOpen(passKey, drawTiming, !resources.WritesMemory(), std::move(storeProxies));
timer.phase(PhaseRecord);
keepRecordedDraw(*recorder, record.resources, std::move(record.pipeline), std::move(record.framebuffer), inputs, std::move(record.targets), std::move(scratch), std::move(checkRecords), record.listed, record.completion, outcome);
timer.phase(PhaseKeep);
@ -1480,6 +1524,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
// Resident target: the surface's cached storage image is attached directly and marked dirty
// afterwards, so nothing is copied in or out per draw.
std::shared_ptr<StorageTexture> resident;
bool proxied = false;
// Linear pixels converted on the CPU, for targets the GPU detiler does not handle.
std::unique_ptr<Buffer> transfer;
// Guest bytes in host memory, and their device-local copies for the detiler.
@ -1508,7 +1553,7 @@ 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::Standard4KB && context.detiler != nullptr)) && residentTargets) {
if ((binding.gpuTiling || (color.tileMode != ColorTileMode::Linear && 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
@ -1524,9 +1569,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
}
if (binding.resident != nullptr) {
timer.phase(PhaseReadTarget);
targetViews.push_back(binding.resident->AttachmentView(color.format, color.mip, color.depthSlice));
binding.proxied = AttachmentProxyFormat(context, color.format) != VK_FORMAT_UNDEFINED;
targetViews.push_back(binding.proxied ? binding.resident->AttachmentProxyView() : binding.resident->AttachmentView(color.format, color.mip, color.depthSlice));
continue;
}
materializeCmaskClear(context, color, nullptr);
Require(!color.mipTail, "rendering into a packed mip tail needs the resident image of its surface");
Require(color.depth == 1, "rendering into a 3D color target needs the resident image of its surface");
if (binding.gpuTiling) {
@ -1683,6 +1730,13 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
APS5_LOG_CHARS_OUT_DEBUG("Creating Pipeline");
// The state is copied only when a mask must change.
auto masked = maskedState(state, inputs.fragmentOutputs);
if (std::any_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.proxied; })) recipeOut = nullptr;
for (std::size_t index = 0; index < targets.size(); ++index) {
if (!targets[index].proxied) continue;
if (!masked.has_value()) masked = state;
masked->colors[index].format = AttachmentProxyFormat(context, state.colors[index].format);
masked->blends.at(state.colors[index].exportIndex).colorWriteMask &= VK_COLOR_COMPONENT_R_BIT;
}
const State& pipelineState = masked.has_value() ? *masked : state;
auto pipeline = CachedPipeline(context, pipelineState, inputs.vertexInput, *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
// Resident targets' views are stable while their storage image lives, so the framebuffer is
@ -1702,6 +1756,9 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
record.framebuffer = framebuffer;
record.targetViews = targetViews;
record.targets = owners;
for (const auto& binding : targets) {
if (binding.proxied) record.proxies.push_back(binding.resident);
}
record.indirect = args != nullptr ? &indirect : nullptr;
record.listed = listed;
record.completion = completion;
@ -1800,6 +1857,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
rewritten = recordIndirectArguments(context, commands, recorder, recorded, indirect, scratch, argumentBuffer, argumentOffset, countBarrier);
}
for (auto& binding : targets) {
if (binding.proxied) {
binding.resident->RecordAttachmentProxyLoad(commands, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
countBarrier(2);
continue;
}
if (binding.resident != nullptr) {
imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT);
countBarrier();
@ -1835,6 +1897,11 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands);
APS5_LOG_CHARS_OUT_DEBUG("Render pass ended");
for (auto& binding : targets) {
if (binding.proxied) {
binding.resident->RecordAttachmentProxyStore(commands, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
countBarrier(2);
continue;
}
if (binding.resident != nullptr) {
imageBarrier(context, commands, binding.resident->Image(), VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT);
countBarrier();
@ -1930,7 +1997,8 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
if (binding.gpuTiling) GuestMemory::WriteChanged(binding.color.address, binding.tiled->Bytes(), binding.original);
else WriteColorTarget(binding.color, binding.transfer->Bytes());
// The stored texels are the whole target now, so later reads must see them rather than a fast clear.
MarkDccUncompressed(binding.color.dccAddress, ColorTargetLayout(binding.color.extent.width, binding.color.extent.height, binding.color.tileMode, binding.color.elementBytes).Bytes());
const auto targetBytes = ColorTargetLayout(binding.color.extent.width, binding.color.extent.height, binding.color.tileMode, binding.color.elementBytes).Bytes();
MarkDccUncompressed(binding.color.dccAddress, targetBytes, colorKeyCount(binding.color, targetBytes));
}
timer.phase(PhaseWriteBack);
if (profile && traceDraws) {
@ -2067,28 +2135,39 @@ DrawRecipeOutcome DrawWithRecipe(const Context& context, const State& state, con
return result;
}
namespace {
std::shared_ptr<StorageTexture> metadataPassResident(const Context& context, const ColorTarget& color) {
if (color.tileMode == ColorTileMode::Linear || context.detiler == nullptr) return nullptr;
std::shared_ptr<StorageTexture> resident;
try {
resident = CachedStorageSurface(context, SurfaceForTarget(color));
} catch (const std::exception&) {
return nullptr;
}
return resident != nullptr && resident->GuestBytes() == color.bytes ? resident : nullptr;
}
}
void RunColorMetadataPass(const Context& context, const ColorMetadataPass& pass) {
for (const auto& color : pass.targets) {
if (color.cmaskAddress != 0) {
const auto resident = metadataPassResident(context, color);
if (materializeCmaskClear(context, color, resident.get()) && resident != nullptr) resident->MarkDirty();
}
if (color.dccAddress == 0) continue;
auto keys = CurrentDccKeys(color.dccAddress, color.bytes);
if (keys == DccKeys::Uncompressed) continue;
Require(IsDccClear(keys), std::string("CB metadata pass over DCC keys that are ") + DccKeysName(keys) + " (per-block metadata is not modeled)");
const auto texel = clearTexel(color, keys);
std::shared_ptr<StorageTexture> resident;
if ((color.tileMode == ColorTileMode::RenderTarget || color.tileMode == ColorTileMode::Standard4KB) && context.detiler != nullptr) {
try {
resident = CachedStorageSurface(context, SurfaceForTarget(color));
} catch (const std::exception&) {
resident = nullptr;
}
if (resident != nullptr && resident->GuestBytes() != color.bytes) resident = nullptr;
}
const auto resident = metadataPassResident(context, color);
if (resident != nullptr) {
const char* refusal = nullptr;
const bool current = keys == DccKeys::ClearRegister ? clearToTexel(*resident, texel, color.elementBytes, refusal) : StorageTexture::FindPending(color.address, color.bytes) == resident || resident->UploadedKeys() == keys;
if (current) {
resident->MarkDirty();
MarkDccUncompressed(context, color.dccAddress, color.bytes);
MarkDccUncompressed(context, color.dccAddress, color.bytes, colorKeyCount(color, color.bytes));
continue;
}
}

View File

@ -36,6 +36,8 @@
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
#include <string_view>
namespace AgcDriver::Graphics {
@ -83,9 +85,22 @@ struct GuestBufferMemory::AddressSpace {
std::uint64_t serial = 0;
GuestAllocations::Lease lease;
std::vector<Region> base;
struct HeapRun {
std::uint64_t begin;
std::uint64_t bytes;
std::size_t first;
std::size_t last;
};
bool mirrored = false;
std::vector<std::size_t> writableMirrors;
std::vector<ImageMirror*> heapMirrors;
std::vector<HeapRun> heapRuns;
std::unique_ptr<std::atomic<std::uint64_t>[]> verified;
std::vector<CopiedRange> copied;
// The BDA table entries of `base`, in its order.
std::vector<ShaderRecompiler::BdaAbi::Range> ranges;
mutable std::mutex tableMutex;
mutable std::map<std::vector<std::pair<std::uint64_t, std::uint64_t>>, std::pair<std::uint64_t, std::shared_ptr<const std::vector<ShaderRecompiler::BdaAbi::Range>>>> writeTables;
};
namespace {
@ -285,8 +300,9 @@ void decideImportWatch(const Context& context, HostImports& state) {
if (state.watchDevice == context.device) return;
#ifdef _WIN32
state.watchDevice = context.device;
state.unwatchImports = false;
state.unwatchImports = true;
state.unwatchDmaBufImports = false;
if (context.hostImportAlignment != 0 && GuestMemory::WriteWatched()) std::fprintf(stderr, "[write-watch] Windows direct host imports use comparisons; separate shared aliases retain guest write tracking\n");
#else
const auto request = importWatchRequest();
state.watchDevice = context.device;
@ -395,6 +411,11 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
}
entry.alias = GuestArena::GuestArenaMapAlias_nid_postfix(static_cast<std::uintptr_t>(base), static_cast<std::size_t>(bytes));
}
#else
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(base), static_cast<std::size_t>(bytes), true)) {
state.failed.insert(base);
return nullptr;
}
#endif
decideImportWatch(context, state);
VkResult result = VK_SUCCESS;
@ -404,6 +425,9 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
return step == nullptr;
});
entry.unwatched = step == nullptr && (entry.dmaBuf ? state.unwatchDmaBufImports : state.unwatchImports);
#ifdef _WIN32
if (entry.alias != nullptr) entry.unwatched = false;
#endif
if (entry.unwatched) GuestMemory::Unwatch(base, bytes);
if (step != nullptr) {
#ifdef _WIN32
@ -525,6 +549,8 @@ struct ImageMirrors {
std::uint64_t builds = 0;
std::uint64_t heapRefills = 0;
std::uint64_t heapUnwatched = 0;
std::uint64_t heapImported = 0;
std::uint64_t heapImportedBytes = 0;
std::uint64_t subranges = 0;
std::uint64_t rebuilds = 0;
std::uint64_t refreshes = 0;
@ -533,6 +559,11 @@ struct ImageMirrors {
// Refreshes that had to wait for recorded work writing the range.
std::uint64_t syncs = 0;
std::uint64_t serials = 0;
std::uint64_t version = 0;
std::uint64_t pinnedSpace = 0;
std::uint64_t pinnedVersion = 0;
std::uint64_t sweeps = 0;
std::uint64_t heapChecks = 0;
std::chrono::steady_clock::time_point lastReport = std::chrono::steady_clock::now();
};
@ -586,12 +617,26 @@ std::uint64_t heapMirrorBudget() {
return bytes;
}
[[noreturn]] void heapMirrorFatal(const GuestAllocations::Range& range, std::uint64_t held, const char* reason) {
std::fprintf(stderr, "FATAL: heap mirror of 0x%llx+0x%llx: %s; heap mirrors hold %llu MiB of %llu MiB (APS5_HEAP_MIRROR_MIB)\n", static_cast<unsigned long long>(range.address), static_cast<unsigned long long>(range.bytes), reason, static_cast<unsigned long long>(held >> 20u), static_cast<unsigned long long>(heapMirrorBudget() >> 20u));
std::fflush(stderr);
std::abort();
std::string heapMirrorError(const GuestAllocations::Range& range, std::uint64_t held, std::string_view reason) {
constexpr std::string_view prefix = "AGC graphics: ";
if (reason.starts_with(prefix)) reason.remove_prefix(prefix.size());
char where[64];
std::snprintf(where, sizeof(where), "heap mirror of 0x%llx+0x%llx: ", static_cast<unsigned long long>(range.address), static_cast<unsigned long long>(range.bytes));
char holding[96];
std::snprintf(holding, sizeof(holding), "; heap mirrors hold %llu MiB of %llu MiB (APS5_HEAP_MIRROR_MIB)", static_cast<unsigned long long>(held >> 20u), static_cast<unsigned long long>(heapMirrorBudget() >> 20u));
return std::string(prefix) + where + std::string(reason) + holding;
}
std::uint64_t heapMirrorsBeside(const GuestAllocations::Range& range) {
auto& state = Mirrors();
std::lock_guard lock(state.mutex);
std::uint64_t held = state.heapBytes;
if (const auto found = state.entries.find(range.address); found != state.entries.end() && found->second->heap) held -= found->second->bytes;
return held;
}
void sweepMirrors();
bool sameRange(const std::weak_ptr<const GuestAllocations::Range>& mirrored, const std::shared_ptr<const GuestAllocations::Range>& range) {
return !mirrored.owner_before(range) && !range.owner_before(mirrored);
}
@ -767,11 +812,30 @@ void refreshMirror(ImageMirror& mirror, std::uint64_t address, std::uint64_t byt
compareBlocks(blocks);
}
void refreshHeapMirrors(std::vector<ImageMirror*>& mirrors, std::vector<RefreshBlock>& blocks) {
void appendChangedBlocks(std::span<ImageMirror* const> mirrors, std::uint64_t generation, std::vector<RefreshBlock>& blocks) {
constexpr std::uint64_t block = 65536;
std::sort(mirrors.begin(), mirrors.end(), [](const ImageMirror* left, const ImageMirror* right) { return left->base < right->base; });
thread_local std::vector<std::uint8_t>* changedSlot = nullptr;
auto& changed = ShaderRecompiler::ThreadOwned(changedSlot);
Mirrors().heapChecks += mirrors.size();
for (auto* const pointer : mirrors) {
auto& mirror = *pointer;
changed.assign(mirror.generations.size(), 0);
GuestMemory::ChangedBlocks(mirror.base, static_cast<std::size_t>(mirror.bytes), mirror.generations, changed);
const auto aligned = mirror.base / block * block;
const auto end = mirror.base + mirror.bytes;
bool refilled = false;
for (std::size_t at = 0; at < changed.size(); ++at) {
if (changed[at] == 0) continue;
const auto from = std::max(mirror.base, aligned + at * block);
blocks.push_back({&mirror, from, static_cast<std::size_t>(std::min(end, aligned + (at + 1) * block) - from), generation});
refilled = true;
}
if (refilled) ++Mirrors().heapRefills;
}
}
void refreshHeapMirrors(std::vector<ImageMirror*>& mirrors, std::vector<RefreshBlock>& blocks) {
std::sort(mirrors.begin(), mirrors.end(), [](const ImageMirror* left, const ImageMirror* right) { return left->base < right->base; });
for (std::size_t first = 0; first < mirrors.size();) {
auto last = first + 1;
while (last < mirrors.size() && mirrors[last]->base == mirrors[last - 1]->base + mirrors[last - 1]->bytes) ++last;
@ -780,25 +844,45 @@ void refreshHeapMirrors(std::vector<ImageMirror*>& mirrors, std::vector<RefreshB
prepareRange(begin, bytes);
const auto generation = GuestMemory::CollectWrites(begin, static_cast<std::size_t>(bytes));
Require(generation != 0, "a heap mirror's range is no longer write-watched");
for (auto index = first; index < last; ++index) {
auto& mirror = *mirrors[index];
changed.assign(mirror.generations.size(), 0);
GuestMemory::ChangedBlocks(mirror.base, static_cast<std::size_t>(mirror.bytes), mirror.generations, changed);
const auto aligned = mirror.base / block * block;
const auto end = mirror.base + mirror.bytes;
bool refilled = false;
for (std::size_t at = 0; at < changed.size(); ++at) {
if (changed[at] == 0) continue;
const auto from = std::max(mirror.base, aligned + at * block);
blocks.push_back({&mirror, from, static_cast<std::size_t>(std::min(end, aligned + (at + 1) * block) - from), generation});
refilled = true;
}
if (refilled) ++Mirrors().heapRefills;
}
appendChangedBlocks(std::span<ImageMirror* const>(mirrors).subspan(first, last - first), generation, blocks);
first = last;
}
}
void planSpaceMirrors(GuestBufferMemory::AddressSpace& space) {
for (std::size_t index = 0; index < space.base.size(); ++index) {
const auto& region = space.base[index];
if (region.mirror == nullptr) continue;
space.mirrored = true;
if (!region.mirror->heap) {
if (region.mirror->writable) space.writableMirrors.push_back(index);
continue;
}
auto* mirror = region.mirror.get();
if (space.heapRuns.empty() || mirror->base != space.heapMirrors.back()->base + space.heapMirrors.back()->bytes) space.heapRuns.push_back({mirror->base, 0, space.heapMirrors.size(), 0});
space.heapMirrors.push_back(mirror);
auto& run = space.heapRuns.back();
run.bytes = mirror->base + mirror->bytes - run.begin;
run.last = space.heapMirrors.size();
}
space.verified = std::make_unique<std::atomic<std::uint64_t>[]>(space.heapRuns.size());
}
std::vector<std::pair<std::size_t, std::uint64_t>> refreshHeapRuns(const GuestBufferMemory::AddressSpace& space, std::vector<RefreshBlock>& blocks) {
std::vector<std::pair<std::size_t, std::uint64_t>> checked;
checked.reserve(space.heapRuns.size());
for (std::size_t index = 0; index < space.heapRuns.size(); ++index) {
const auto& run = space.heapRuns[index];
prepareRange(run.begin, run.bytes);
const auto generation = GuestMemory::CollectWrites(run.begin, static_cast<std::size_t>(run.bytes));
Require(generation != 0, "a heap mirror's range is no longer write-watched");
const auto since = space.verified[index].load(std::memory_order_relaxed);
if (since == 0 || !GuestMemory::UnchangedSince(run.begin, static_cast<std::size_t>(run.bytes), since)) appendChangedBlocks(std::span<ImageMirror* const>(space.heapMirrors).subspan(run.first, run.last - run.first), generation, blocks);
checked.emplace_back(index, generation);
}
return checked;
}
// The mirror for a leased image range: the existing one, or a new one when none exists or the
// registered Range object changed (a protection change). Null when the range cannot be mirrored. An
// existing writable mirror's blocks are appended to `blocks` for the caller's one compare of every
@ -812,6 +896,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
state.failed.clear();
state.heapBytes = 0;
state.device = context.device;
++state.version;
}
if (state.failed.contains(range->address)) return nullptr;
const auto found = state.entries.find(range->address);
@ -833,19 +918,20 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
const auto generation = GuestMemory::CollectWrites(range->address, range->bytes);
constexpr std::uint64_t block = 65536;
mirror->generations.assign(static_cast<std::size_t>(((range->address + range->bytes + block - 1) / block) - range->address / block), generation);
std::lock_guard lock(state.mutex);
if (generation == 0) {
std::lock_guard lock(state.mutex);
++state.heapUnwatched;
return nullptr;
}
std::uint64_t held = state.heapBytes;
if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) held -= found->second->bytes;
if (held + range->bytes > heapMirrorBudget()) heapMirrorFatal(*range, held, "past the budget");
if (heapMirrorsBeside(*range) + range->bytes > heapMirrorBudget()) {
sweepMirrors();
if (const auto held = heapMirrorsBeside(*range); held + range->bytes > heapMirrorBudget()) throw std::runtime_error(heapMirrorError(*range, held, "past the budget"));
}
}
try {
mirror->buffer = std::make_shared<Buffer>(context, range->bytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
} catch (const std::runtime_error& error) {
if (heap) heapMirrorFatal(*range, state.heapBytes, error.what());
if (heap) throw std::runtime_error(heapMirrorError(*range, heapMirrorsBeside(*range), error.what()));
std::fprintf(stderr, "[gpu] image mirror of 0x%llx+0x%llx failed: %s; falling back to copies\n", static_cast<unsigned long long>(range->address), static_cast<unsigned long long>(range->bytes), error.what());
std::lock_guard lock(state.mutex);
state.failed.insert(range->address);
@ -865,6 +951,7 @@ std::shared_ptr<ImageMirror> acquireMirror(const Context& context, const std::sh
if (const auto found = state.entries.find(range->address); found != state.entries.end() && found->second->heap) state.heapBytes -= found->second->bytes;
if (heap) state.heapBytes += mirror->bytes;
state.entries[range->address] = mirror;
++state.version;
return mirror;
}
@ -882,15 +969,44 @@ std::shared_ptr<ImageMirror> findMirror(const Context& context, std::uint64_t be
// Drops mirrors whose registered range is gone (a later build made a new one for the replacement).
void sweepMirrors() {
const auto space = Spaces().current.load();
auto& state = Mirrors();
std::lock_guard lock(state.mutex);
if (space != nullptr && state.pinnedSpace == space->serial && state.pinnedVersion == state.version) return;
++state.sweeps;
bool pinned = space != nullptr;
std::size_t leased = 0;
for (auto it = state.entries.begin(); it != state.entries.end();) {
if (!it->second->range.expired()) {
if (pinned) {
while (leased < space->lease.size() && space->lease[leased]->address < it->first) ++leased;
pinned = leased < space->lease.size() && sameRange(it->second->range, space->lease[leased]);
}
++it;
continue;
}
if (it->second->heap) state.heapBytes -= it->second->bytes;
it = state.entries.erase(it);
++state.version;
}
state.pinnedSpace = pinned ? space->serial : 0;
state.pinnedVersion = state.version;
}
void releaseImportedMirrors(const Context& context, const std::vector<std::uint64_t>& imported) {
if (imported.empty()) return;
std::vector<std::shared_ptr<ImageMirror>> released;
auto& state = Mirrors();
std::lock_guard lock(state.mutex);
if (state.device != context.device || state.entries.empty()) return;
for (const auto base : imported) {
const auto found = state.entries.find(base);
if (found == state.entries.end() || !found->second->heap) continue;
state.heapBytes -= found->second->bytes;
++state.heapImported;
state.heapImportedBytes += found->second->bytes;
released.push_back(std::move(found->second));
state.entries.erase(found);
}
}
@ -916,7 +1032,7 @@ void reportMirrors() {
}
heapBytes = state.heapBytes;
}
AgcDriver::ProfilePrint_nid_no_patch("[buffers] image mirrors: %zu ranges (%.1f MiB, %zu writable, %zu heap %.1f MiB), %llu address-based builds served, %llu descriptor sub-ranges bound, %llu rebuilds, %llu refreshes: %llu blocks compared, %llu copied, %llu refresh syncs; heap refills %llu, unwatched heaps %llu\n", count, bytes / 1048576.0, writable, heaps, heapBytes / 1048576.0, static_cast<unsigned long long>(state.builds), static_cast<unsigned long long>(state.subranges), static_cast<unsigned long long>(state.rebuilds), static_cast<unsigned long long>(state.refreshes), static_cast<unsigned long long>(state.blocksCompared), static_cast<unsigned long long>(state.blocksCopied), static_cast<unsigned long long>(state.syncs), static_cast<unsigned long long>(state.heapRefills), static_cast<unsigned long long>(state.heapUnwatched));
AgcDriver::ProfilePrint_nid_no_patch("[buffers] image mirrors: %zu ranges (%.1f MiB, %zu writable, %zu heap %.1f MiB), %llu address-based builds served, %llu descriptor sub-ranges bound, %llu rebuilds, %llu refreshes: %llu blocks compared, %llu copied, %llu refresh syncs; heap refills %llu, unwatched heaps %llu, %llu heap mirrors released for imports (%.1f MiB)\n", count, bytes / 1048576.0, writable, heaps, heapBytes / 1048576.0, static_cast<unsigned long long>(state.builds), static_cast<unsigned long long>(state.subranges), static_cast<unsigned long long>(state.rebuilds), static_cast<unsigned long long>(state.refreshes), static_cast<unsigned long long>(state.blocksCompared), static_cast<unsigned long long>(state.blocksCopied), static_cast<unsigned long long>(state.syncs), static_cast<unsigned long long>(state.heapRefills), static_cast<unsigned long long>(state.heapUnwatched), static_cast<unsigned long long>(state.heapImported), state.heapImportedBytes / 1048576.0);
}
// Deferred lease release. The lease an address-based build takes (AcquireRegistered) is dropped by its
@ -1094,7 +1210,7 @@ std::string AddressCopyOverflow(std::vector<AddressCopy> copies, std::uint64_t l
MirrorStats MirrorCounters() {
auto& state = Mirrors();
std::lock_guard lock(state.mutex);
MirrorStats stats{0, state.heapBytes, state.rebuilds, state.blocksCopied, state.heapRefills};
MirrorStats stats{0, state.heapBytes, state.rebuilds, state.blocksCopied, state.heapRefills, state.sweeps, state.heapChecks};
for (const auto& [base, mirror] : state.entries) stats.heapMirrors += mirror->heap ? 1 : 0;
return stats;
}
@ -1110,6 +1226,25 @@ void ClearImageMirrors(VkDevice device) {
state.heapBytes = 0;
state.device = VK_NULL_HANDLE;
}
Spaces().current.store(nullptr);
}
void ClearHostImports(VkDevice device) {
auto& state = Imports();
std::lock_guard lock(state.mutex);
if (state.device != device) return;
for (const auto& [address, entry] : state.imports) {
state.destroyBuffer(state.device, entry.buffer, nullptr);
state.freeMemory(state.device, entry.memory, nullptr);
#ifdef _WIN32
GuestArena::GuestArenaUnmapAlias_nid_postfix(entry.alias);
#endif
}
state.imports.clear();
state.failed.clear();
state.device = VK_NULL_HANDLE;
state.refreshedGeneration = 0;
++state.epoch;
}
#ifndef _WIN32
@ -1364,6 +1499,7 @@ void GuestBufferMemory::AcquireRegistered() {
std::vector<RefreshBlock> blocks;
std::vector<ImageMirror*> heaps;
bool mirrored = false;
bool served = false;
auto& spaces = Spaces();
// Read before the lease below is acquired: a mutation ending in between leaves the space
// stale by its generation, never newer than it (see AddressSpace).
@ -1391,11 +1527,11 @@ void GuestBufferMemory::AcquireRegistered() {
// an existing mirror. The preparation may wait for recorded work, whose completions can
// retire an import the space's regions point at: UploadFinish re-checks the epoch
// before any of them is dereferenced.
for (const auto& region : space->base) {
if (region.mirror == nullptr) continue;
mirrored = true;
if (region.mirror->heap) heaps.push_back(region.mirror.get());
else if (region.mirror->writable && prepareRefresh(*region.mirror, region.begin, region.end - region.begin)) appendBlocks(blocks, *region.mirror, region.begin, region.end - region.begin);
served = true;
mirrored = space->mirrored;
for (const auto index : space->writableMirrors) {
const auto& region = space->base[index];
if (prepareRefresh(*region.mirror, region.begin, region.end - region.begin)) appendBlocks(blocks, *region.mirror, region.begin, region.end - region.begin);
}
lap(timing.mirrorsUs, at);
}
@ -1420,6 +1556,7 @@ void GuestBufferMemory::AcquireRegistered() {
// must not happen under it.
regions.reserve(lease.size());
std::vector<AddressCopy> copies;
std::vector<std::uint64_t> imported;
for (const auto& range : lease) {
if (!range->readable) continue;
validate(range->address, range->bytes);
@ -1436,6 +1573,7 @@ void GuestBufferMemory::AcquireRegistered() {
// Reused by Upload while no import was dropped since (see importsEpoch).
region.direct = entry;
regions.push_back(std::move(region));
imported.push_back(range->address);
continue;
}
if (mirrorsEnabled()) {
@ -1458,6 +1596,7 @@ void GuestBufferMemory::AcquireRegistered() {
const char* reason = !mirrorsEnabled() ? "mirrors disabled by APS5_NO_LEASE_MIRROR" : copied.sparse ? "unreadable pages" : !range->releasable ? "image mirror refused" : "outside the write-watched arena";
copies.push_back({copied.begin, copied.end, committed, reason});
}
releaseImportedMirrors(context, imported);
regionsSorted = sortedLookup;
static const std::uint64_t copyLimit = [] {
const char* value = std::getenv("APS5_ADDRESS_COPY_MAX_MIB");
@ -1507,16 +1646,20 @@ void GuestBufferMemory::AcquireRegistered() {
}
regions = std::move(extras);
lease.clear();
planSpaceMirrors(*built);
space = std::move(built);
spaces.droppedByWaiter.store(false, std::memory_order_relaxed);
spaces.current.store(space);
}
}
}
refreshHeapMirrors(heaps, blocks);
std::vector<std::pair<std::size_t, std::uint64_t>> checked;
if (served) checked = refreshHeapRuns(*space, blocks);
else refreshHeapMirrors(heaps, blocks);
lap(timing.mirrorsUs, at);
const auto copiedBefore = Mirrors().blocksCopied;
compareBlocks(blocks);
for (const auto& [run, collected] : checked) space->verified[run].store(collected, std::memory_order_relaxed);
lap(timing.compareUs, at);
if (profile) {
timing.blocksCompared += blocks.size();
@ -1604,7 +1747,9 @@ const GuestBufferMemory::Region* GuestBufferMemory::owner(std::uint64_t address)
void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t bytes, bool atomic) {
validate(address, bytes);
switch (baseOverlap(address, address + bytes, nullptr)) {
const auto begin = address & ~std::uint64_t{3};
const auto end = begin == address ? address + bytes : (address + bytes + 3) & ~std::uint64_t{3};
switch (baseOverlap(begin, end, nullptr)) {
case BaseOverlap::Inside:
return;
case BaseOverlap::Partial:
@ -1616,9 +1761,9 @@ void GuestBufferMemory::addDescriptorRegion(std::uint64_t address, std::size_t b
}
// `writable` here means the bytes come from live guest memory (not a snapshot), whether or not
// the shader stores to them; what is written back is decided by Writes() alone.
Region region{address, address + bytes, true, {}, nullptr};
Region region{begin, end, true, {}, nullptr};
region.atomic = atomic;
auto committed = GuestMemory::DescribeCommitted(address, bytes);
auto committed = GuestMemory::DescribeCommitted(begin, static_cast<std::size_t>(end - begin));
if (!committed.whole) {
// A GPU heap bound whole while the guest commits its pages on demand, or a descriptor left
region.sparse = true;
@ -1934,6 +2079,7 @@ void GuestBufferMemory::UploadPrepare(bool addressable) {
previous.hostBacked = true;
}
mergeBacked(previous, region);
if (region.end > previous.end) previous.direct = nullptr;
previous.atomic = previous.atomic || region.atomic;
// A range starting before the mirror (only possible after the swap) keeps its prefix; the
// earlier merged region ends at or before it, so the merged list stays sorted.
@ -2460,11 +2606,18 @@ VkDescriptorBufferInfo GuestBufferMemory::Descriptor(std::uint64_t address, std:
const auto base = region.direct != nullptr ? region.direct->base : region.mirror != nullptr ? region.mirror->base : region.begin;
const auto offset = address - base;
Require(context.limits.minStorageBufferOffsetAlignment != 0, "no storage buffer offset alignment");
adjustment = static_cast<std::uint32_t>(offset % context.limits.minStorageBufferOffsetAlignment);
Require(adjustment % 4 == 0, "guest buffer view off the storage buffer offset alignment is not DWORD aligned");
Require(bytes + adjustment <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit");
Require(base % 4 == 0, "a guest buffer view in a GPU owner that does not start at a DWORD boundary is not implemented");
adjustment = static_cast<std::uint32_t>(offset % std::max<VkDeviceSize>(context.limits.minStorageBufferOffsetAlignment, 4));
const auto range = ViewBytes(bytes, adjustment);
const auto end = region.direct != nullptr ? region.direct->base + region.direct->bytes : region.end;
Require(address - adjustment + range <= end, "guest buffer view exceeds its GPU owner");
Require(range <= context.limits.maxStorageBufferRange, "guest buffer view exceeds descriptor range limit");
const auto handle = region.direct != nullptr ? region.direct->buffer : region.mirror != nullptr ? region.mirror->buffer->Handle() : region.buffer->Handle();
return {handle, offset - adjustment, bytes + adjustment};
return {handle, offset - adjustment, range};
}
std::uint64_t GuestBufferMemory::ViewBytes(std::uint64_t bytes, std::uint32_t adjustment) {
return adjustment % 4 == 0 ? bytes + adjustment : 4 * (adjustment / 4 + bytes / 4 + 1);
}
ShaderRecompiler::BdaAbi::Range GuestBufferMemory::addressRange(const Region& region) {
@ -2480,19 +2633,70 @@ std::vector<ShaderRecompiler::BdaAbi::Range> GuestBufferMemory::AddressRanges()
std::vector<ShaderRecompiler::BdaAbi::Range> result;
result.reserve(regions.size() + (space != nullptr ? space->ranges.size() : 0));
for (const auto& region : regions) result.push_back(addressRange(region));
if (space == nullptr) return result;
// The table is searched by address (the recompiler's lookup bisects it): both lists are sorted
// and disjoint, so a merge keeps it so.
if (space != nullptr) {
std::vector<ShaderRecompiler::BdaAbi::Range> combined;
combined.reserve(result.size() + space->ranges.size());
std::merge(space->ranges.begin(), space->ranges.end(), result.begin(), result.end(), std::back_inserter(combined), [](const auto& left, const auto& right) { return left.begin < right.begin; });
result = std::move(combined);
}
if (writes.empty()) return result;
auto sortedWrites = writes;
std::sort(sortedWrites.begin(), sortedWrites.end());
std::vector<std::pair<std::uint64_t, std::uint64_t>> intervals;
for (const auto& interval : sortedWrites) {
if (!intervals.empty() && interval.first <= intervals.back().second) intervals.back().second = std::max(intervals.back().second, interval.second);
else intervals.push_back(interval);
}
std::vector<ShaderRecompiler::BdaAbi::Range> merged;
merged.reserve(result.size() + space->ranges.size());
std::merge(space->ranges.begin(), space->ranges.end(), result.begin(), result.end(), std::back_inserter(merged), [](const auto& left, const auto& right) { return left.begin < right.begin; });
merged.reserve(result.size() + intervals.size() * 2u);
std::size_t firstWrite = 0;
for (const auto& range : result) {
if ((range.permissions & ShaderRecompiler::BdaAbi::Write) != 0u) {
merged.push_back(range);
continue;
}
while (firstWrite < intervals.size() && intervals[firstWrite].second <= range.begin) ++firstWrite;
auto cursor = range.begin;
const auto append = [&](std::uint64_t end, bool written) {
if (cursor == end) return;
auto part = range;
part.begin = cursor;
part.end = end;
part.deviceAddress += cursor - range.begin;
if (written) {
const auto* region = owner(cursor);
Require(region != nullptr && region->writable && (region->mirror == nullptr || region->mirror->writable), "runtime buffer write has no writable GPU owner");
part.permissions |= ShaderRecompiler::BdaAbi::Write;
}
merged.push_back(part);
cursor = end;
};
for (auto index = firstWrite; index < intervals.size() && intervals[index].first < range.end; ++index) {
append(std::max(cursor, intervals[index].first), false);
append(std::min(range.end, intervals[index].second), true);
}
append(range.end, false);
}
return merged;
}
std::optional<GuestBufferMemory::CachedTable> GuestBufferMemory::CachedAddressTable() const {
if (space == nullptr || !regions.empty()) return std::nullopt;
Require(uploaded && !committed, "guest GPU address ranges are not available");
return CachedTable{space->serial, &space->ranges};
if (writes.empty()) return CachedTable{space->serial, &space->ranges};
if (writeTableRanges != nullptr) return CachedTable{writeTableSerial, writeTableRanges.get()};
std::lock_guard lock(space->tableMutex);
const auto found = space->writeTables.find(writes);
if (found != space->writeTables.end()) {
writeTableSerial = found->second.first;
writeTableRanges = found->second.second;
} else {
writeTableRanges = std::make_shared<const std::vector<ShaderRecompiler::BdaAbi::Range>>(AddressRanges());
writeTableSerial = Spaces().serials.fetch_add(1, std::memory_order_relaxed) + 1;
if (space->writeTables.size() >= 64u) space->writeTables.erase(space->writeTables.begin());
space->writeTables.emplace(writes, std::pair{writeTableSerial, writeTableRanges});
}
return CachedTable{writeTableSerial, writeTableRanges.get()};
}
bool GuestBufferMemory::HasCopiedWrites() const {

View File

@ -71,7 +71,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
const auto bcSwizzle = (words[3] >> 25u) & 0x7u;
const auto typeRaw = (words[3] >> 28u) & 0xfu;
const auto depth = (words[4] >> 0u) & 0x1fffu;
auto depth = (words[4] >> 0u) & 0x1fffu;
const auto baseArray = (words[4] >> 16u) & 0x1fffu;
const auto arrayPitch = (words[5] >> 0u) & 0xfu;
@ -115,7 +115,6 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
static_cast<void>(maxCompBlkSize);
// Surfaces are always written uncompressed here (render targets and storage images bypass DCC), so
// only the fast-clear keys in DCC metadata change what a read returns (see DccMetadata.hpp).
static_cast<void>(metaPipeAligned);
static_cast<void>(writeCompress);
if (dccColorTransf) {
static bool reported = false;
@ -166,7 +165,11 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
case TextureDimension::kCube:
Require(width == height, "guest cube texture descriptor is not square");
Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice");
Require((depth - baseArray + 1u) % 6u == 0, "guest cube texture descriptor does not contain a multiple of 6 array slices");
if (baseArray == depth) depth += 5u;
if ((depth - baseArray + 1u) % 6u != 0) {
Require(false, "guest cube texture descriptor does not contain a multiple of 6 array slices" +
describe() + " (base array " + std::to_string(baseArray) + ")");
}
break;
}
@ -189,6 +192,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
result.dstSelW = static_cast<std::uint8_t>(dstSelW);
result.dccAddress = metaCompress ? metaAddr << 8u : 0u;
result.dccAlphaOnMsb = dccAlphaPos;
result.dccPipeAligned = metaPipeAligned;
result.minLod = minLod;
if (baseLevel > maxMip) {
Require(LevelsFitAllocation(result, lastLevel + 1u), "guest texture descriptor starts past the surface's last mip level at levels that would move the surface's own" + describe());

View File

@ -1,5 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Pipeline.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PipelineSpecialization.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include <algorithm>
#include <array>
@ -92,6 +94,7 @@ void ValidateViewport(const Context& context, const VkViewport& viewport) {
}
Pipeline::Pipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) : context(context), _modules(shaders.size()), attachments(state.colors.size() + (state.depth ? 1u : 0u)), colorAttachments(state.colors.size()), depthBounds(state.depth.has_value() && state.depthBoundsTest), depthBias(state.depth.has_value() && state.depthBias) {
PerformanceTimer timing("Vulkan.GraphicsPipeline");
// A cached pipeline may outlive its device's teardown (see ClearCachedPipelines); it must not keep
// the buffer pool, which is reset with the device, alive past it.
this->context.bufferPool.reset();
@ -101,6 +104,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
Require(!depthBounds || context.depthBounds, "device does not support the depth bounds test");
Require(!depthBias || state.depthBiasClamp == 0.0f || context.depthBiasClamp, "device does not support depth bias clamping");
Require(!state.negativeOneToOne || context.depthClipControl, "negative-one-to-one depth clipping requires VK_EXT_depth_clip_control with depthClipControl enabled");
Require(state.conservativeRasterization == VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT || context.conservativeRasterization, "conservative rasterization requires VK_EXT_conservative_rasterization with at most 1/256 pixel of overestimation and degenerate triangles rasterized");
if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points");
if (state.stages.tessellation) {
Require(context.tessellationShader, "device does not support tessellation shaders");
@ -117,6 +121,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
Require(pushStages == 0 || context.limits.maxPushConstantsSize >= PipelinePushConstantBytes, "graphics push constant range exceeds device limit");
try {
std::vector<VkPipelineShaderStageCreateInfo> stages(shaders.size());
std::vector<PipelineSpecialization> specializations;
specializations.reserve(shaders.size());
for (std::uint32_t i = 0; i < shaders.size(); ++i) {
const auto& shader = *shaders[i].program;
VkShaderModuleCreateInfo module{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
@ -128,6 +134,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
stages[i].stage = stage;
stages[i].module = _modules[i];
stages[i].pName = "main";
specializations.emplace_back(shader);
stages[i].pSpecializationInfo = specializations.back().Info();
}
// A descriptor set layout with the same bindings as this one is compatible with the pipeline
// layout, so later draws bind their own ShaderResources' set under it.
@ -208,6 +216,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
raster.frontFace = state.frontFace;
raster.depthBiasEnable = depthBias;
raster.lineWidth = 1;
VkPipelineRasterizationConservativeStateCreateInfoEXT conservative{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_CONSERVATIVE_STATE_CREATE_INFO_EXT};
conservative.conservativeRasterizationMode = state.conservativeRasterization;
if (state.conservativeRasterization != VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT) raster.pNext = &conservative;
VkPipelineMultisampleStateCreateInfo samples{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO};
samples.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT;
VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO};
@ -241,7 +252,9 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
pipelineInfo.pDynamicState = &dynamic;
pipelineInfo.layout = layout;
pipelineInfo.renderPass = renderPass;
timing.Mark("modules_and_state");
Check(context.Function<PFN_vkCreateGraphicsPipelines>("vkCreateGraphicsPipelines")(context.device, context.pipelineCache, 1, &pipelineInfo, nullptr, &pipeline), "vkCreateGraphicsPipelines");
timing.Mark("create");
LogPipelineStatistics_nid_no_patch(context, pipeline);
} catch (...) {
release();
@ -367,9 +380,9 @@ std::vector<std::byte> pipelineKey(const Context& context, const State& state, c
for (const auto& shader : shaders) {
Require(shader.program != nullptr, "missing compiled shader");
const bool generated = state.rectList && (shader.stage == Stage::TessellationControl || shader.stage == Stage::TessellationEvaluation);
if (!generated && shader.program->variantId == 0) return {};
if (!generated && shader.program->PipelineVariantId() == 0) return {};
append(key, shader.stage);
append(key, generated ? std::uint64_t{0} : shader.program->variantId);
append(key, generated ? std::uint64_t{0} : shader.program->PipelineVariantId());
// Where the stage's push constants sit in the block (AssemblePushConstants).
append(key, shader.pushConstantOffset);
}
@ -397,6 +410,7 @@ std::vector<std::byte> pipelineKey(const Context& context, const State& state, c
append(key, state.frontFace);
append(key, state.negativeOneToOne);
append(key, state.depthClamp && context.depthClamp);
append(key, state.conservativeRasterization);
append(key, state.blends.size());
for (const auto& blend : state.blends) append(key, blend);
for (const auto value : state.blendConstants) append(key, value);

Some files were not shown because too many files have changed in this diff Show More