mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
Merge main into fix/agc-sample-location-grid-exact
This commit is contained in:
commit
8f6f7d7500
418 files changed
+17947
-1260
No files matched your search
+135
-8
@@ -70,23 +70,77 @@ jobs:
|
||||
const { owner, repo } = context.repo;
|
||||
const workflow = '.github/workflows/build.yml';
|
||||
const libc = 'core/libs/prx/libc/';
|
||||
const jobOfLine = (text) => {
|
||||
let inJobs = false;
|
||||
let job = '';
|
||||
return text.split('\n').map((line) => {
|
||||
if (/^\S/.test(line)) {
|
||||
inJobs = /^jobs:\s*$/.test(line);
|
||||
job = '';
|
||||
} else if (inJobs) {
|
||||
job = /^ ([\w-]+):\s*$/.exec(line)?.[1] ?? job;
|
||||
}
|
||||
return job;
|
||||
});
|
||||
};
|
||||
const patchedJobs = (patch, text) => {
|
||||
const jobs = jobOfLine(text);
|
||||
const touched = new Set();
|
||||
let line = 1;
|
||||
for (const row of patch.split('\n')) {
|
||||
const hunk = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(row);
|
||||
if (hunk) {
|
||||
line = Number(hunk[1]);
|
||||
} else if (row.startsWith('+')) {
|
||||
touched.add(jobs[line - 1] ?? '');
|
||||
line += 1;
|
||||
} else if (row.startsWith('-')) {
|
||||
touched.add(jobs[line - 2] ?? '').add(jobs[line - 1] ?? '');
|
||||
} else if (row.startsWith(' ')) {
|
||||
line += 1;
|
||||
}
|
||||
}
|
||||
return touched;
|
||||
};
|
||||
const workflowJobs = new Set();
|
||||
const readWorkflow = async (files, ref) => {
|
||||
const file = files.find((file) => file.filename === workflow || file.previous_filename === workflow);
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (file.filename !== workflow || !file.patch) {
|
||||
workflowJobs.add('*');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({ owner, repo, path: workflow, ref });
|
||||
const text = Buffer.from(data.content, 'base64').toString('utf8');
|
||||
patchedJobs(file.patch, text).forEach((job) => workflowJobs.add(job));
|
||||
} catch (error) {
|
||||
core.warning(`Could not read ${workflow} at ${ref}, so every test job runs: ${error.message}`);
|
||||
workflowJobs.add('*');
|
||||
}
|
||||
};
|
||||
const select = (paths, all) => {
|
||||
const touches = (...prefixes) => all ||
|
||||
paths.some((path) => prefixes.some((prefix) => prefix.endsWith('/') ? path.startsWith(prefix) : path === prefix));
|
||||
const edits = (job) => ['*', '', 'eligibility', job].some((name) => workflowJobs.has(name));
|
||||
return {
|
||||
relinker: touches(
|
||||
relinker: edits('relinker') || touches(
|
||||
'core/relinker/', libc, 'core/libs/prx/libkernel/AppMetadata/', 'core/libs/tests/ParamJsonParser.cpp',
|
||||
'core/shader/recompiler/tests/CacheFileReadTests.cpp', 'core/shader/recompiler/ShaderCacheDirectory.hpp',
|
||||
'tools/', 'cmake/', 'CMakeLists.txt', workflow
|
||||
'tools/', 'cmake/', 'CMakeLists.txt'
|
||||
),
|
||||
registerContext: touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc, workflow),
|
||||
writeWatch: touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc, workflow),
|
||||
registerContext: edits('register-context') ||
|
||||
touches('core/libs/tests/register_context/', 'core/libs/tests/RegisterContext.cpp', libc),
|
||||
writeWatch: edits('write-watch') || touches('core/libs/tests/write_watch/', 'core/libs/tests/WriteWatch.cpp', libc),
|
||||
};
|
||||
};
|
||||
const relinkerHosts = (full, linux) => JSON.stringify([
|
||||
...(full && linux ? [] : [{ os: 'ubuntu-latest', compiler: 'g++' }]),
|
||||
...(full ? [] : [{ os: 'windows-latest', compiler: 'g++' }]),
|
||||
{ os: 'macos-latest', compiler: 'clang++' },
|
||||
{ os: 'macos-latest', compiler: 'clang++', architectures: 'x86_64' },
|
||||
]);
|
||||
if (context.eventName === 'push') {
|
||||
if (context.ref !== 'refs/heads/main') {
|
||||
@@ -102,6 +156,7 @@ jobs:
|
||||
const files = data.files ?? [];
|
||||
all = files.length >= 300;
|
||||
paths = files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean));
|
||||
await readWorkflow(files, context.sha);
|
||||
} catch (error) {
|
||||
core.warning(`Could not list the files of this push, so every test job runs: ${error.message}`);
|
||||
}
|
||||
@@ -167,6 +222,7 @@ jobs:
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number, per_page: 100 });
|
||||
truncated ||= files.length >= 3000;
|
||||
paths.push(...files.flatMap((file) => [file.filename, file.previous_filename].filter(Boolean)));
|
||||
await readWorkflow(files, pr.head.sha);
|
||||
pulls.push({ number: pull_number, head: pr.head.sha });
|
||||
}
|
||||
const run = select(paths, truncated);
|
||||
@@ -259,9 +315,40 @@ jobs:
|
||||
7z x "$RUNNER_TEMP/winlibs.zip" -oC:/winlibs > /dev/null
|
||||
- if: runner.os == 'Windows'
|
||||
run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
|
||||
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON
|
||||
- if: ${{ matrix.architectures == 'x86_64' }}
|
||||
run: sudo softwareupdate --install-rosetta --agree-to-license
|
||||
- run: cmake -S . -B build-relinker -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=${{ matrix.compiler }} -DANYPS5_RELINKER_ONLY=ON -DBUILD_TESTING=ON -DCMAKE_OSX_ARCHITECTURES=${{ matrix.architectures }}
|
||||
- run: cmake --build build-relinker --parallel 3
|
||||
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120
|
||||
- run: ctest --test-dir build-relinker --output-on-failure --timeout 120 --parallel 4
|
||||
|
||||
relinker-asan:
|
||||
needs: eligibility
|
||||
if: ${{ needs.eligibility.outputs.relinker == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
git fetch --no-tags --unshallow origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
- run: cmake -S . -B build-relinker-asan -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=g++ -DANYPS5_RELINKER_ONLY=ON -DANYPS5_ENABLE_ASAN=ON -DBUILD_TESTING=ON
|
||||
- run: cmake --build build-relinker-asan --parallel 4
|
||||
- run: ctest --test-dir build-relinker-asan --output-on-failure --timeout 120 --label-regex '^tls_function_coverage$' --no-tests=error
|
||||
|
||||
register-context:
|
||||
needs: eligibility
|
||||
@@ -275,6 +362,25 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
shell: bash
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
git fetch --no-tags --unshallow origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
- run: cmake -S core/libs/tests/register_context -B build/register-context -DCMAKE_BUILD_TYPE=${{ matrix.configuration }}
|
||||
- run: cmake --build build/register-context --parallel
|
||||
- run: ctest --test-dir build/register-context --output-on-failure --repeat until-fail:3 --no-tests=error
|
||||
@@ -290,6 +396,25 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ needs.eligibility.outputs.sha }}
|
||||
persist-credentials: false
|
||||
- name: Merge the pull request into main
|
||||
if: ${{ needs.eligibility.outputs.head != '' }}
|
||||
shell: bash
|
||||
env:
|
||||
PR_NUMBER: ${{ needs.eligibility.outputs.pr }}
|
||||
PR_HEAD: ${{ needs.eligibility.outputs.head }}
|
||||
run: |
|
||||
read -ra numbers <<< "$PR_NUMBER"
|
||||
read -ra heads <<< "$PR_HEAD"
|
||||
refspecs=()
|
||||
for number in "${numbers[@]}"; do refspecs+=("+refs/pull/$number/head:refs/remotes/pull/$number"); done
|
||||
git fetch --no-tags --unshallow origin "${refspecs[@]}"
|
||||
for i in "${!numbers[@]}"; do
|
||||
if [ "$(git rev-parse "refs/remotes/pull/${numbers[i]}")" != "${heads[i]}" ]; then echo "::error::Pull request #${numbers[i]} has newer commits; start a new build"; exit 1; fi
|
||||
if ! git -c user.name=ci -c user.email=ci@localhost merge --no-ff --no-edit "refs/remotes/pull/${numbers[i]}"; then echo "::error::Pull request #${numbers[i]} does not merge cleanly into main and the pull requests before it; rebase it first"; exit 1; fi
|
||||
done
|
||||
- run: cmake -S core/libs/tests/write_watch -B build/write-watch -DCMAKE_BUILD_TYPE=Release
|
||||
- run: cmake --build build/write-watch --parallel
|
||||
- run: ctest --test-dir build/write-watch --output-on-failure --repeat until-fail:3 --no-tests=error
|
||||
@@ -503,8 +628,9 @@ jobs:
|
||||
- run: echo 'C:\winlibs\mingw64\bin' >> "$GITHUB_PATH"
|
||||
- uses: hendrikmuhs/[email protected]
|
||||
with:
|
||||
key: windows
|
||||
save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
|
||||
key: ${{ github.event_name == 'workflow_dispatch' && 'dispatch-windows' || 'windows' }}
|
||||
restore-keys: ${{ github.event_name == 'workflow_dispatch' && 'windows' || '' }}
|
||||
save: ${{ github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
|
||||
evict-old-files: job
|
||||
- id: ffmpeg-cache
|
||||
uses: actions/cache/restore@v6
|
||||
@@ -577,3 +703,4 @@ jobs:
|
||||
[ -f ctest.log ] || exit 0
|
||||
awk '/Test +#[0-9]+: / && $NF == "sec" { for (i = 1; i <= NF; i++) if ($i ~ /^#[0-9]+:$/) name = $(i + 1); t = $(NF - 1) + 0; if (t > max[name]) max[name] = t }
|
||||
END { for (name in max) if (max[name] > 30) printf "::warning title=Slow test::%s took %.1f s on %s; tests must finish in 30 s (CONTRIBUTING.md)\n", name, max[name], ENVIRON["RUNNER_OS"] }' ctest.log
|
||||
|
||||
@@ -27,6 +27,27 @@ jobs:
|
||||
if (pr.state !== 'open' || pr.base.ref !== 'main') {
|
||||
return;
|
||||
}
|
||||
if (context.payload.action === 'reopened') {
|
||||
let maintainer = false;
|
||||
try {
|
||||
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner, repo, username: context.payload.sender.login
|
||||
});
|
||||
maintainer = ['admin', 'maintain', 'write'].includes(access.role_name ?? access.permission);
|
||||
} catch (error) {
|
||||
core.warning(`Could not check the permission of ${context.payload.sender.login}, so the open PR limit applies: ${error.message}`);
|
||||
}
|
||||
if (maintainer) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, name: 'open PR limit' });
|
||||
} catch (error) {
|
||||
if (error.status !== 404) {
|
||||
core.warning(`Could not remove the open PR limit label: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { data: mergedPRs } = await github.rest.search.issuesAndPullRequests({
|
||||
q: `repo:${owner}/${repo} is:pr is:merged base:main author:${pr.user.login}`,
|
||||
per_page: 1
|
||||
|
||||
+19
-2
@@ -17,6 +17,12 @@ function(add_test_executable target)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(add_asan_test_options test options)
|
||||
if(ANYPS5_ENABLE_ASAN)
|
||||
set_property(TEST ${test} APPEND PROPERTY ENVIRONMENT_MODIFICATION "ASAN_OPTIONS=string_append::${options}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
option(BUILD_TESTING "Build and register relinker tests" OFF)
|
||||
option(ANYPS5_RELINKER_ONLY "Build the relinker without system libraries or third-party dependencies" OFF)
|
||||
option(ANYPS5_BUILD_WASM "Build the browser relinker" OFF)
|
||||
@@ -91,6 +97,17 @@ if(BUILD_TESTING)
|
||||
add_test(NAME request_serializer COMMAND request_serializer_tests)
|
||||
set_tests_properties(request_serializer PROPERTIES TIMEOUT 20)
|
||||
find_package(Python3 QUIET COMPONENTS Interpreter)
|
||||
add_test_executable(shader_ir_use_rewrite_tests
|
||||
core/shader/recompiler/IntermediateRepresentation/tests/UseRewrite.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/tests/UseRewriteAllocation.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrValue.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrBlock.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp
|
||||
core/shader/recompiler/IntermediateRepresentation/src/GuestRegister.cpp)
|
||||
target_include_directories(shader_ir_use_rewrite_tests PRIVATE
|
||||
core/shader/recompiler/IntermediateRepresentation/include)
|
||||
add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests)
|
||||
set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py
|
||||
@@ -118,14 +135,14 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_import_audit.py)
|
||||
add_test(NAME unwrap_self COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_unwrap_self.py)
|
||||
foreach(shard RANGE 0 5)
|
||||
foreach(shard RANGE 0 11)
|
||||
if(shard EQUAL 0)
|
||||
set(conventions_test_name conventions_function_bodies)
|
||||
else()
|
||||
set(conventions_test_name conventions_function_bodies_shard_${shard})
|
||||
endif()
|
||||
add_test(NAME ${conventions_test_name} COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/6)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_comments.py --shard ${shard}/12)
|
||||
set_tests_properties(${conventions_test_name} PROPERTIES TIMEOUT 30)
|
||||
endforeach()
|
||||
add_test(NAME conventions_wrapped_declarations COMMAND ${Python3_EXECUTABLE}
|
||||
|
||||
@@ -242,6 +242,17 @@ if(BUILD_TESTING)
|
||||
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_library(host_libstdcxx_exception_module SHARED tests/HostLibstdcxxExceptionModule.cpp)
|
||||
target_link_libraries(host_libstdcxx_exception_module PRIVATE ${PATCHED_DIR}/libc.prx)
|
||||
add_dependencies(host_libstdcxx_exception_module libs)
|
||||
add_test_executable(host_libstdcxx_exception_tests tests/HostLibstdcxxException.cpp)
|
||||
target_link_libraries(host_libstdcxx_exception_tests PRIVATE host_libstdcxx_exception_module ${PATCHED_DIR}/libc.prx)
|
||||
add_dependencies(host_libstdcxx_exception_tests libs)
|
||||
set_target_properties(host_libstdcxx_exception_module host_libstdcxx_exception_tests PROPERTIES BUILD_RPATH ${PATCHED_DIR})
|
||||
add_test(NAME host_libstdcxx_exception COMMAND host_libstdcxx_exception_tests)
|
||||
set_tests_properties(host_libstdcxx_exception PROPERTIES TIMEOUT 20)
|
||||
endif()
|
||||
if(Python3_Interpreter_FOUND)
|
||||
add_test(NAME nid_elf_layout COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_elf_layout.py $<TARGET_FILE:nid_patcher>)
|
||||
@@ -384,6 +395,16 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_rudp_context_lifecycle_tests tests/GuestRudpContextLifecycle.cpp)
|
||||
target_include_directories(guest_rudp_context_lifecycle_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_rudp_context_lifecycle_tests PRIVATE libSceRudp libc)
|
||||
configure_windows_unwind(guest_rudp_context_lifecycle_tests)
|
||||
add_test(NAME guest_rudp_context_lifecycle COMMAND guest_rudp_context_lifecycle_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_rudp_context_lifecycle PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
endif()
|
||||
|
||||
add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp)
|
||||
target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc)
|
||||
|
||||
@@ -921,6 +921,14 @@ struct VoicePortInfo {
|
||||
std::uint16_t reserved;
|
||||
};
|
||||
|
||||
struct VoiceResourceInfo {
|
||||
std::uint16_t max_voice_input_ports;
|
||||
std::uint16_t max_voice_output_ports;
|
||||
std::uint16_t max_device_input_ports;
|
||||
std::uint16_t max_device_output_ports;
|
||||
std::uint16_t max_ports;
|
||||
};
|
||||
|
||||
struct VoiceStartParam {
|
||||
void* container;
|
||||
std::uint32_t mem_size;
|
||||
@@ -2032,6 +2040,11 @@ struct VideoOutColorSettings {
|
||||
std::uint32_t reserved[3] = {};
|
||||
};
|
||||
|
||||
struct VideoOutColorSpaceConversionSettings {
|
||||
std::uint32_t internalData[8] = {};
|
||||
};
|
||||
static_assert(sizeof(VideoOutColorSpaceConversionSettings) == 32);
|
||||
|
||||
|
||||
using atexit_func_t = void (*)();
|
||||
|
||||
|
||||
@@ -204,6 +204,7 @@ set(agcDriverSources
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Driver/Synchronization/WaitStatistics.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/WorkerSampler.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTrace.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceTraceThread.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/TraceFile.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/ThreadPriority.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/GuestMemory.cpp
|
||||
@@ -405,6 +406,17 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_performance_trace PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_handoff_spin_tests tests/HandoffSpin.cpp)
|
||||
target_include_directories(agc_handoff_spin_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_handoff_spin_tests PRIVATE libc)
|
||||
configure_windows_unwind(agc_handoff_spin_tests)
|
||||
add_test(NAME agc_handoff_spin COMMAND agc_handoff_spin_tests)
|
||||
set_tests_properties(agc_handoff_spin PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_handoff_spin PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
@@ -504,6 +516,7 @@ add_test_executable(agc_shader_memory_tests EXCLUDE_FROM_ALL
|
||||
tests/ShaderMemory.cpp
|
||||
Execution/src/ShaderMemory.cpp
|
||||
Execution/src/GuestMemory.cpp
|
||||
Execution/src/PerformanceTraceThread.cpp
|
||||
)
|
||||
target_include_directories(agc_shader_memory_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_shader_memory_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
@@ -519,6 +532,34 @@ target_link_libraries(agc_vertex_input_tests PRIVATE shader_recompiler_agc_drive
|
||||
set_target_properties(agc_vertex_input_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_vertex_input_tests)
|
||||
|
||||
add_test_executable(agc_guarded_memory_loads_tests EXCLUDE_FROM_ALL tests/GuardedMemoryLoads.cpp)
|
||||
target_link_libraries(agc_guarded_memory_loads_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_guarded_memory_loads_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_guarded_memory_loads_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_guarded_memory_loads_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_guarded_memory_loads COMMAND agc_guarded_memory_loads_tests)
|
||||
set_tests_properties(agc_guarded_memory_loads PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_guarded_memory_loads PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_lds_lock_budget_tests EXCLUDE_FROM_ALL tests/LdsLockBudget.cpp)
|
||||
target_link_libraries(agc_lds_lock_budget_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_lds_lock_budget_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_lds_lock_budget_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_lds_lock_budget_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_lds_lock_budget COMMAND agc_lds_lock_budget_tests)
|
||||
set_tests_properties(agc_lds_lock_budget PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_shared_write_tests EXCLUDE_FROM_ALL tests/SharedWrites.cpp)
|
||||
target_include_directories(agc_shared_write_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_shared_write_tests PRIVATE libSceAgcDriver libkernel libc)
|
||||
@@ -528,6 +569,7 @@ configure_windows_unwind(agc_shared_write_tests)
|
||||
add_test_executable(agc_write_tracking_tests EXCLUDE_FROM_ALL
|
||||
tests/WriteTracking.cpp
|
||||
Execution/src/GuestMemory.cpp
|
||||
Execution/src/PerformanceTraceThread.cpp
|
||||
)
|
||||
target_include_directories(agc_write_tracking_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_write_tracking_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
@@ -797,6 +839,24 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_pipeline_queue_failure_tests EXCLUDE_FROM_ALL tests/PipelineQueueFailure.cpp
|
||||
Graphics/src/PipelineLibrary.cpp tests/AllocationFailure.cpp)
|
||||
target_include_directories(agc_pipeline_queue_failure_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_pipeline_queue_failure_tests PRIVATE libSceAgcDriver libc)
|
||||
set_target_properties(agc_pipeline_queue_failure_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_pipeline_queue_failure_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_pipeline_queue_failure_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
foreach(mode wait clear stop)
|
||||
add_test(NAME agc_pipeline_queue_failure_${mode} COMMAND agc_pipeline_queue_failure_tests ${mode})
|
||||
set_tests_properties(agc_pipeline_queue_failure_${mode} PROPERTIES TIMEOUT 15)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_pipeline_queue_failure_${mode} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_pass_hazards_tests EXCLUDE_FROM_ALL tests/PassHazards.cpp Graphics/src/PassHazards.cpp)
|
||||
target_include_directories(agc_pass_hazards_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_pass_hazards_tests PRIVATE libc)
|
||||
@@ -826,8 +886,14 @@ configure_windows_unwind(agc_buffer_pool_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_buffer_pool_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_buffer_pool COMMAND agc_buffer_pool_tests)
|
||||
add_test(NAME agc_buffer_pool_small_device_shared COMMAND agc_buffer_pool_tests --small-device-retention)
|
||||
set_tests_properties(agc_buffer_pool_small_device_shared PROPERTIES ENVIRONMENT
|
||||
"APS5_STAGING_POOL_MIB=256;APS5_BUFFER_POOL_SHARED=1")
|
||||
add_test(NAME agc_buffer_pool_small_device_no_staging COMMAND agc_buffer_pool_tests --small-device-retention)
|
||||
set_tests_properties(agc_buffer_pool_small_device_no_staging PROPERTIES ENVIRONMENT
|
||||
"APS5_STAGING_POOL_MIB=0")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_buffer_pool agc_buffer_pool_small_device_shared agc_buffer_pool_small_device_no_staging PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -1127,6 +1193,12 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_storage_upload_lifetime COMMAND agc_driver_recorder_tests --storage-upload-lifetime-only)
|
||||
add_test(NAME agc_sampled_storage_base_level_usage COMMAND agc_driver_recorder_tests --sampled-storage-base-level-usage-only)
|
||||
set_tests_properties(agc_sampled_storage_base_level_usage PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_sampled_storage_base_level_usage PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_storage_upload_failure COMMAND agc_driver_recorder_tests --storage-upload-failure-only)
|
||||
set_tests_properties(agc_storage_upload_lifetime agc_storage_upload_failure PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
if(WIN32)
|
||||
@@ -1219,8 +1291,10 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_shadowed_key_stores PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_sample_dumps COMMAND agc_driver_recorder_tests --sample-dumps-only)
|
||||
set_tests_properties(agc_sample_dumps PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_test(NAME agc_draw_descriptors COMMAND agc_driver_recorder_tests --draw-descriptors-only)
|
||||
set_tests_properties(agc_draw_descriptors PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_pending_key_stores agc_shadowed_key_stores agc_sample_dumps agc_draw_descriptors PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -1239,6 +1313,23 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_command_batch_failure_tests EXCLUDE_FROM_ALL
|
||||
tests/CommandBatch.cpp tests/CommandBatchFailure.cpp Graphics/src/Resources.cpp tests/AllocationFailure.cpp)
|
||||
target_include_directories(agc_command_batch_failure_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_command_batch_failure_tests PRIVATE libSceAgcDriver libc)
|
||||
set_target_properties(agc_command_batch_failure_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_command_batch_failure_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_command_batch_failure_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_command_batch_failure COMMAND agc_command_batch_failure_tests)
|
||||
set_tests_properties(agc_command_batch_failure PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_command_batch_failure PROPERTIES
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_host_import_retire_tests EXCLUDE_FROM_ALL tests/HostImportRetireHold.cpp)
|
||||
target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc)
|
||||
@@ -1328,6 +1419,22 @@ add_test_executable(agc_driver_graphics_tests EXCLUDE_FROM_ALL
|
||||
tests/DepthSurfaceReuse.cpp
|
||||
)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test_executable(agc_descriptor_cache_failure_tests
|
||||
tests/DescriptorCacheFailure.cpp
|
||||
Graphics/src/ShaderResources.cpp)
|
||||
target_include_directories(agc_descriptor_cache_failure_tests PRIVATE
|
||||
$<TARGET_PROPERTY:libSceAgcDriver,INCLUDE_DIRECTORIES>)
|
||||
target_link_libraries(agc_descriptor_cache_failure_tests PRIVATE libSceAgcDriver shader_recompiler_agc_driver libc libkernel)
|
||||
configure_windows_unwind(agc_descriptor_cache_failure_tests)
|
||||
add_test(NAME agc_descriptor_cache_failure COMMAND agc_descriptor_cache_failure_tests)
|
||||
set_tests_properties(agc_descriptor_cache_failure PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_descriptor_cache_failure PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_include_directories(agc_driver_graphics_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
$<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>
|
||||
@@ -1428,6 +1535,14 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
|
||||
configure_windows_unwind(${agcExecutionTest})
|
||||
list(APPEND agcExecutionTests ${agcExecutionTest})
|
||||
endforeach()
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_address_passes_targets_split COMMAND agc_driver_address_passes_tests --targets)
|
||||
set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT "APS5_NO_ADDRESS_READ_PASSES=1" SKIP_RETURN_CODE 77 TIMEOUT 60)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_address_passes_targets_split PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
|
||||
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/tests/execution/${agcExecutionTestName}.cpp)
|
||||
message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution")
|
||||
@@ -1435,11 +1550,14 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
|
||||
endforeach()
|
||||
|
||||
if(NOT ANYPS5_AGC_EXECUTION_TESTS OR "PooledBufferRange" IN_LIST ANYPS5_AGC_EXECUTION_TESTS)
|
||||
target_sources(agc_driver_pooled_buffer_range_tests PRIVATE Graphics/src/BufferPool.cpp tests/AllocationFailure.cpp)
|
||||
target_sources(agc_driver_pooled_buffer_range_tests PRIVATE
|
||||
Graphics/src/BufferPool.cpp Graphics/src/Resources.cpp tests/AllocationFailure.cpp tests/CommandBatchFailure.cpp)
|
||||
endif()
|
||||
|
||||
target_link_libraries(agc_driver_gpu_map_on_fiber_tests PRIVATE libSceFiber)
|
||||
|
||||
target_link_libraries(agc_driver_queue_zero_late_interrupt_tests PRIVATE libkernel)
|
||||
|
||||
target_link_libraries(agc_driver_preparation_scope_settle_tests PRIVATE libSceAgc)
|
||||
|
||||
target_include_directories(agc_driver_alpha_to_coverage_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
@@ -1457,6 +1575,13 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_native_msaa_vertex_input COMMAND agc_driver_sample_multisample_target_tests --native-vertex-input)
|
||||
set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_native_msaa_vertex_input PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state)
|
||||
add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state)
|
||||
set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES
|
||||
@@ -1491,10 +1616,16 @@ target_include_directories(agc_driver_reversed_order_blend_tests PRIVATE $<TARGE
|
||||
|
||||
target_include_directories(agc_driver_swap_alt_blend_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_packed_float_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_link_libraries(agc_driver_job_manager_compute_tests PRIVATE libSceJobManager libSceAgc libkernel)
|
||||
|
||||
target_include_directories(agc_driver_packed_unorm_color_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_color1555_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_color565_target_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
foreach(stage IN ITEMS vert frag)
|
||||
set(agcProvokingShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/ProvokingVertex.${stage})
|
||||
set(agcProvokingShaderSpv ${agcTestShaderGeneratedRoot}/ProvokingVertex_${stage}.spv)
|
||||
@@ -1540,6 +1671,8 @@ target_include_directories(agc_driver_pipeline_journal_tests PRIVATE $<TARGET_PR
|
||||
|
||||
target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_prepared_publication_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
target_include_directories(agc_driver_prepared_graphics_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
@@ -1621,11 +1754,28 @@ if(BUILD_TESTING)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file)
|
||||
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint)
|
||||
set_tests_properties(agc_driver_untracked_snapshot_hint PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30
|
||||
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set_tests_properties(agc_driver_registration_trace PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TRACE_FRAMES=0:1;APS5_TRACE_EVENTS=64" TIMEOUT 30)
|
||||
set_property(TEST agc_driver_registration_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION
|
||||
"APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:")
|
||||
add_test(NAME agc_driver_registration_trace_disabled COMMAND ${CMAKE_COMMAND} -E env
|
||||
--unset=APS5_TRACE_FRAMES --unset=APS5_TRACE_SECONDS --unset=APS5_TRACE_SUMMARY_EVERY
|
||||
$<TARGET_FILE:agc_driver_registration_trace_tests> disabled)
|
||||
set_tests_properties(agc_driver_registration_trace_disabled PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_registration_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_sparse_buffer_holes COMMAND agc_driver_graphics_tests --sparse-buffer-holes)
|
||||
set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30)
|
||||
set_tests_properties(agc_driver_sample_order PROPERTIES TIMEOUT 30)
|
||||
@@ -1647,15 +1797,24 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30)
|
||||
add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal)
|
||||
add_test(NAME agc_pipeline_library_clear_waits_for_build COMMAND agc_driver_graphics_tests --pipeline-library-clear-waits-for-build)
|
||||
add_test(NAME agc_pipeline_store_lazy_buffer_pool COMMAND agc_driver_graphics_tests --pipeline-store-lazy-buffer-pool)
|
||||
set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_pipeline_journal agc_pipeline_library_clear_waits_for_build agc_pipeline_store_lazy_buffer_pool PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_sparse_buffer_holes PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_lds_lock_budget COMMAND agc_driver_lds_mixed_width_atomics_tests --lock-budget)
|
||||
set_tests_properties(agc_driver_lds_lock_budget PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_lds_lock_budget PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
|
||||
add_test(NAME agc_driver_emulated_color_compare_srgb_decode COMMAND agc_driver_emulated_color_compare_tests)
|
||||
set_tests_properties(agc_driver_emulated_color_compare_srgb_decode PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SRGB_SHADER_DECODE=1"
|
||||
@@ -1791,12 +1950,13 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endforeach()
|
||||
add_test(NAME agc_descriptor_pool_reset_failure COMMAND agc_driver_graphics_tests --descriptor-pool-reset-failure)
|
||||
add_test(NAME agc_descriptor_cache COMMAND agc_driver_graphics_tests --descriptor-cache)
|
||||
add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown)
|
||||
set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_driver_pm4_failure_inline COMMAND agc_driver_pm4_tests failure)
|
||||
set_tests_properties(agc_driver_pm4_failure_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_pm4_failure agc_driver_pm4_failure_inline agc_descriptor_pool_reset_failure agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_driver_pm4_failure agc_driver_pm4_failure_inline agc_descriptor_pool_reset_failure agc_descriptor_cache agc_driver_address_space_teardown PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
if(NOT WIN32)
|
||||
@@ -1904,6 +2064,17 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(${preparedTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endforeach()
|
||||
add_test(NAME agc_driver_shutdown_threads_joined COMMAND agc_driver_shutdown_threads_tests --joined)
|
||||
add_test(NAME agc_driver_shutdown_threads_compare_helpers COMMAND agc_driver_shutdown_threads_tests --compare-helpers)
|
||||
add_test(NAME agc_driver_shutdown_threads_draw_committer COMMAND agc_driver_shutdown_threads_tests --draw-committer)
|
||||
add_test(NAME agc_driver_shutdown_threads_shader_cache_writer COMMAND agc_driver_shutdown_threads_tests --shader-cache-writer)
|
||||
add_test(NAME agc_driver_shutdown_threads_late_shader_cache COMMAND agc_driver_shutdown_threads_tests --late-shader-cache)
|
||||
add_test(NAME agc_driver_shutdown_threads_async_log_held COMMAND agc_driver_shutdown_threads_tests --async-log-held)
|
||||
add_test(NAME agc_driver_shutdown_threads_exit_after_start COMMAND agc_driver_shutdown_threads_tests --exit-after-start)
|
||||
set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_shutdown_threads_joined agc_driver_shutdown_threads_compare_helpers agc_driver_shutdown_threads_draw_committer agc_driver_shutdown_threads_shader_cache_writer agc_driver_shutdown_threads_late_shader_cache agc_driver_shutdown_threads_async_log_held agc_driver_shutdown_threads_exit_after_start PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_compute_user_data_calls_registered COMMAND agc_driver_compute_user_data_calls_tests --registered)
|
||||
set_tests_properties(agc_driver_compute_user_data_calls_registered PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
@@ -2517,6 +2688,19 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_constant_select_tests EXCLUDE_FROM_ALL tests/ConstantSelect.cpp)
|
||||
target_link_libraries(agc_constant_select_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_constant_select_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_constant_select_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_constant_select_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_constant_select COMMAND agc_constant_select_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_constant_select PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
get_directory_property(agcRegisteredTests TESTS)
|
||||
list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$")
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
#include <initializer_list>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -171,6 +172,16 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
static void FlushAtEnd() {
|
||||
auto& log = Get();
|
||||
if (log.abandoned.load(std::memory_order_acquire)) return;
|
||||
if (!log.FlushFor(EndFlushLimit)) log.abandoned.store(true, std::memory_order_release);
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> Hold() {
|
||||
return std::unique_lock(mutex);
|
||||
}
|
||||
|
||||
private:
|
||||
struct Window {
|
||||
Clock::time_point first;
|
||||
@@ -193,14 +204,14 @@ private:
|
||||
if (!asynchronous) return;
|
||||
LogSink().store(&AsyncLog::Sink, std::memory_order_release);
|
||||
std::atexit(&AsyncLog::FlushAtEnd);
|
||||
InitializeWaits();
|
||||
std::thread([this] { Run(); }).detach();
|
||||
LibcSetEndFlush_nid_no_patch(&AsyncLog::FlushAtEnd);
|
||||
}
|
||||
|
||||
static void FlushAtEnd() {
|
||||
auto& log = Get();
|
||||
if (log.abandoned.load(std::memory_order_acquire)) return;
|
||||
if (!log.FlushFor(EndFlushLimit)) log.abandoned.store(true, std::memory_order_release);
|
||||
void InitializeWaits() {
|
||||
std::unique_lock lock(mutex);
|
||||
for (auto* condition : {&wake, &space, &idle}) static_cast<void>(condition->wait_for(lock, std::chrono::nanoseconds(0)));
|
||||
}
|
||||
|
||||
static int Sink(std::FILE* stream, const char* text, std::size_t length) {
|
||||
@@ -383,6 +394,12 @@ inline void FlushAsyncLog_nid_no_patch() {
|
||||
AsyncLog::Get().Flush();
|
||||
}
|
||||
|
||||
inline void FlushAsyncLogAtEnd_nid_no_patch() {
|
||||
AsyncLog::FlushAtEnd();
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> HoldAsyncLog();
|
||||
|
||||
template<typename... TArgs>
|
||||
void LogCoalescedStderr_nid_no_patch(const char* format, TArgs... args) {
|
||||
char small[1024];
|
||||
|
||||
@@ -30,9 +30,11 @@ public:
|
||||
static bool& Active();
|
||||
static std::atomic<std::uint64_t>& EpochToken();
|
||||
static void FollowEpoch(std::uint64_t token);
|
||||
static unsigned Committers();
|
||||
|
||||
void Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress = 0, std::vector<std::byte> labelBytes = {});
|
||||
void Drain(DrainReason reason, std::uint32_t opcode = 0x100);
|
||||
void Stop();
|
||||
static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode);
|
||||
bool Busy() const { return outstanding.load(std::memory_order_acquire) != 0; }
|
||||
std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); }
|
||||
@@ -60,6 +62,7 @@ private:
|
||||
std::uint32_t idleWaiters = 0;
|
||||
bool committerWaiting = false;
|
||||
bool stopped = false;
|
||||
bool closing = false;
|
||||
std::thread thread;
|
||||
std::uint64_t commits = 0;
|
||||
std::uint64_t commitNs = 0;
|
||||
|
||||
@@ -103,6 +103,7 @@ private:
|
||||
static bool& completionDeferred();
|
||||
void deferCompletion(const Submission& submission);
|
||||
void submitOpenWork();
|
||||
void wakeQueueZeroForCompletions();
|
||||
void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites);
|
||||
static bool waitFree(const Submission& submission);
|
||||
bool queue0Before(std::uint64_t received) const;
|
||||
|
||||
@@ -119,6 +119,7 @@ void AwaitRegistrationPreparation();
|
||||
void SubmitLibraryPrebuild(std::function<void()> task);
|
||||
void AwaitLibraryPrebuilds();
|
||||
void CloseRegistrationPreparation();
|
||||
unsigned RegistrationPreparationThreads();
|
||||
|
||||
void ResolvePreparedGraphics(const ShaderSnapshot& front, const std::shared_ptr<const ShaderSnapshot>& fragment, std::uint32_t primitiveType, const ShaderRecompiler::SpirvTarget& target);
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ struct Commitment {
|
||||
bool whole = false;
|
||||
};
|
||||
Commitment DescribeCommitted(std::uint64_t address, std::size_t bytes, bool writable = false);
|
||||
void CopyGuestBytes(void* destination, const void* source, std::size_t bytes);
|
||||
void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t alignment = 1);
|
||||
// Variants for resources in heaps the guest commits on demand (partially resident textures, GPU
|
||||
// scratch heaps): uncommitted pages read as zeros, are not compared, and are never stored.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_HANDOFFSPIN_HPP
|
||||
|
||||
#include "prx/libc/include/CpuTopology.hpp"
|
||||
#include <algorithm>
|
||||
#include <bitset>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
#include <immintrin.h>
|
||||
#endif
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
inline void SpinPause() {
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
_mm_pause();
|
||||
#endif
|
||||
}
|
||||
|
||||
inline unsigned UsableProcessors() {
|
||||
const auto count = static_cast<unsigned>(std::bitset<64>(CpuTopology::Get().process).count());
|
||||
return count != 0 ? count : std::thread::hardware_concurrency();
|
||||
}
|
||||
|
||||
inline std::chrono::nanoseconds HandoffSpinWindowFor(const char* configured, unsigned usableProcessors) {
|
||||
constexpr std::chrono::microseconds automaticWindow{150};
|
||||
constexpr unsigned automaticProcessorsAbove = 8;
|
||||
constexpr long long longestWindowUs = 10000;
|
||||
if (configured != nullptr) return std::chrono::microseconds(std::clamp<long long>(std::strtoll(configured, nullptr, 10), 0, longestWindowUs));
|
||||
return usableProcessors > automaticProcessorsAbove ? std::chrono::nanoseconds(automaticWindow) : std::chrono::nanoseconds::zero();
|
||||
}
|
||||
|
||||
inline std::chrono::nanoseconds HandoffSpinWindow() {
|
||||
static const std::chrono::nanoseconds window = HandoffSpinWindowFor(std::getenv("APS5_HANDOFF_SPIN_US"), UsableProcessors());
|
||||
return window;
|
||||
}
|
||||
|
||||
template<typename TReady>
|
||||
bool SpinUntil(std::chrono::nanoseconds window, TReady&& ready) {
|
||||
if (ready()) return true;
|
||||
if (window.count() <= 0) return false;
|
||||
const auto until = std::chrono::steady_clock::now() + window;
|
||||
do {
|
||||
SpinPause();
|
||||
if (ready()) return true;
|
||||
} while (std::chrono::steady_clock::now() < until);
|
||||
return false;
|
||||
}
|
||||
|
||||
template<typename TMutex>
|
||||
void LockSpinning(TMutex& mutex, std::chrono::nanoseconds window) {
|
||||
if (window.count() > 0 && SpinUntil(window, [&] { return mutex.try_lock(); })) return;
|
||||
mutex.lock();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -21,6 +21,13 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver {
|
||||
class TraceThread;
|
||||
}
|
||||
|
||||
extern "C" bool AgcDriverTraceRecording_nid_postfix();
|
||||
extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix();
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
class VulkanDevice;
|
||||
@@ -532,23 +539,26 @@ public:
|
||||
}
|
||||
|
||||
static bool Recording() {
|
||||
return recording.load(std::memory_order_relaxed);
|
||||
return AgcDriverTraceRecording_nid_postfix();
|
||||
}
|
||||
|
||||
static TraceThread* Thread() {
|
||||
return local;
|
||||
return AgcDriverTraceThread_nid_postfix();
|
||||
}
|
||||
|
||||
static void StopOutput();
|
||||
|
||||
private:
|
||||
friend bool ::AgcDriverTraceRecording_nid_postfix();
|
||||
friend TraceThread* ::AgcDriverTraceThread_nid_postfix();
|
||||
|
||||
static void OpenInterval(std::chrono::steady_clock::time_point now, std::uint64_t present);
|
||||
static void FlushInterval(std::chrono::steady_clock::time_point now, std::uint32_t width, std::uint32_t height, const VulkanDevice* device);
|
||||
|
||||
inline static std::atomic<bool> recording{false};
|
||||
static std::atomic<bool> recording;
|
||||
inline static std::atomic<bool> buffering{false};
|
||||
inline static std::atomic<std::uint64_t> presentCount{0};
|
||||
inline static thread_local TraceThread* local = nullptr;
|
||||
static thread_local TraceThread* local;
|
||||
inline static std::mutex gpuMutex;
|
||||
inline static std::vector<TraceGpuRange> gpuRanges;
|
||||
inline static std::uint64_t gpuLost = 0;
|
||||
|
||||
@@ -47,7 +47,7 @@ struct Recipe {
|
||||
std::uint64_t dataWordsHash = 0;
|
||||
// The template's PresyncSurfaces() as of the attach and the host-import identity they were
|
||||
// computed under (a surface is CPU-read only while its memory has no import).
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> presyncSurfaces;
|
||||
std::vector<Graphics::PresyncSurface> presyncSurfaces;
|
||||
Graphics::HostImportsProof presyncProof;
|
||||
// Both false for every recipe (only reusable objects get one); kept for the asserts.
|
||||
bool needsCompletion = false;
|
||||
|
||||
@@ -56,6 +56,7 @@ public:
|
||||
ShaderRecompiler::SpirvTarget Target() const;
|
||||
Graphics::SampleOrder HostSampleOrder(const Graphics::State& graphics) const;
|
||||
std::optional<ShaderRecompiler::GeometryStageLimits> GeometryLimits() const;
|
||||
std::uint32_t MaxFragmentInputComponents() const;
|
||||
const std::uint32_t* WorkGroupCountLimit() const;
|
||||
std::uint32_t VendorId() const;
|
||||
ShaderRecompiler::SpirvTarget ComputeTarget(std::uint32_t waveSize) const;
|
||||
@@ -186,6 +187,10 @@ public:
|
||||
std::uint64_t created, underGpuMutex;
|
||||
};
|
||||
static ComputePipelineCreation ComputePipelineCounts();
|
||||
struct DispatchMoveCount {
|
||||
std::uint64_t checked, exact, moved, refused, filtered, rebuilt, kept, served;
|
||||
};
|
||||
static DispatchMoveCount DispatchMoveCounts();
|
||||
void ResolveMemory(std::uint64_t address, std::size_t bytes, bool writable);
|
||||
void* Window() const;
|
||||
void Resize(std::uint32_t width, std::uint32_t height);
|
||||
@@ -245,6 +250,10 @@ public:
|
||||
double gpuBusyMs, gpuGapMs, lastSubmitAfterFlipMs, blitSubmitAfterFlipMs;
|
||||
};
|
||||
static PresentStatistics PresentCounts();
|
||||
struct PresyncStatistics {
|
||||
std::uint64_t checked, presyncs, keyChecks, keyWaits;
|
||||
};
|
||||
static PresyncStatistics PresyncCounts();
|
||||
// Stage A of a dispatch's resource build, run WITHOUT GuestMemory::GpuMutex before Dispatch or
|
||||
// DispatchIndirect: the binding plan, data buffers, imports already serving the guest buffers,
|
||||
// read-only copies and the descriptor set (see ShaderResources). The dispatch completes it
|
||||
@@ -338,7 +347,7 @@ private:
|
||||
// Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups.
|
||||
IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe);
|
||||
// The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none.
|
||||
std::uint64_t presync(std::span<const std::pair<std::uint64_t, std::uint64_t>> surfaces);
|
||||
std::uint64_t presync(std::span<const Graphics::PresyncSurface> surfaces);
|
||||
std::shared_ptr<ComputePipelineObjects> computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
|
||||
// A DISPATCH_INDIRECT's argument path (see DispatchIndirect): GPU-side from the host import, or
|
||||
// the counts read on the CPU (the record then continues as a direct dispatch).
|
||||
|
||||
@@ -272,8 +272,10 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
||||
bool verifyHit = false;
|
||||
const bool stageEntries = useDrawEntries && !(graphics.stages.tessellation && !drawParameters.indirect && drawParameters.instanceCount > 1u);
|
||||
lookupDraw(submission, localDevice, graphics, pixel, programs, roles, vertexInfos, stageEntries, registerKey, profile, drawKey, entry, matched, matchedRegions, drawHit, verifyHit, phaseTiming, phaseMs, dataEntry, dataCandidates, stageData);
|
||||
for (auto& variant : matched) {
|
||||
if (variant != nullptr) applyShaderDataPatches(*variant);
|
||||
for (std::size_t i = 0; i < matched.size(); ++i) {
|
||||
if (matched[i] == nullptr || matched[i]->shaderDataPatches.empty()) continue;
|
||||
applyShaderDataPatches(*matched[i]);
|
||||
if (stageData[i].compiled != nullptr) stageData[i].compiled = patchDataWords(*matched[i], stageData[i].words);
|
||||
}
|
||||
const auto relocatedDecode = decode;
|
||||
if (!userPatches.empty()) {
|
||||
@@ -434,15 +436,16 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
||||
phaseTiming.Phase(DrawRowVectors);
|
||||
require(programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr, "rect-list requires vertex and fragment programs");
|
||||
auto rectangle = DrawRectangle(*programs[0].snapshot, programs[1].snapshot, programResults[0]->variantId, programResults[1]->variantId, localDevice->Target());
|
||||
auto& control = graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? rectangle.lastVertexControl : rectangle.control;
|
||||
if (rectListBuilt) {
|
||||
results[rectIndex] = std::move(rectangle.control);
|
||||
results[rectIndex] = std::move(control);
|
||||
results[rectIndex + 1] = std::move(rectangle.evaluation);
|
||||
phaseTiming.Phase(DrawRowRectList);
|
||||
return;
|
||||
}
|
||||
require(stages.size() == 2, "rect-list requires vertex and fragment programs");
|
||||
rectIndex = results.size();
|
||||
results.push_back(std::move(rectangle.control));
|
||||
results.push_back(std::move(control));
|
||||
results.push_back(std::move(rectangle.evaluation));
|
||||
stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[rectIndex], 0}, {Stage::TessellationEvaluation, &results[rectIndex + 1], 0}});
|
||||
rectListBuilt = true;
|
||||
@@ -451,8 +454,18 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
||||
if (graphics.rectList) buildRectList();
|
||||
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
|
||||
require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it");
|
||||
results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), localDevice->GeometryLimits()));
|
||||
stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0});
|
||||
const auto geometryLimits = localDevice->GeometryLimits();
|
||||
if (geometryLimits || !localDevice->Target().tessellation) {
|
||||
results.push_back(ShaderRecompiler::BuildBarycentricGeometryShader(*programResults[0], *programResults[1], localDevice->Target(), geometryLimits));
|
||||
stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0});
|
||||
} else {
|
||||
require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart");
|
||||
auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents());
|
||||
const auto first = results.size();
|
||||
results.push_back(std::move(patches.control));
|
||||
results.push_back(std::move(patches.evaluation));
|
||||
stages.insert(stages.begin() + 1, {{Stage::TessellationControl, &results[first], 0}, {Stage::TessellationEvaluation, &results[first + 1], 0}});
|
||||
}
|
||||
}
|
||||
std::vector<Graphics::GuestMemorySnapshot> snapshots;
|
||||
const auto snapshot = [&] {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -14,6 +15,8 @@ namespace {
|
||||
|
||||
constexpr std::array<const char*, static_cast<std::size_t>(DrawPipeline::DrainReason::Count)> DrainNames{"packet", "flush", "labels", "capture", "indirect", "submission"};
|
||||
|
||||
std::atomic<unsigned> runningCommitters{0};
|
||||
|
||||
std::uint64_t elapsedNs(std::chrono::steady_clock::time_point since) {
|
||||
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - since).count());
|
||||
}
|
||||
@@ -59,6 +62,10 @@ void DrawPipeline::FollowEpoch(std::uint64_t token) {
|
||||
GuestMemory::BumpCollectEpoch();
|
||||
}
|
||||
|
||||
unsigned DrawPipeline::Committers() {
|
||||
return runningCommitters.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void DrawPipeline::rethrowFailure() {
|
||||
if (failure == nullptr) return;
|
||||
auto error = std::exchange(failure, nullptr);
|
||||
@@ -69,6 +76,11 @@ void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
std::unique_lock lock(mutex);
|
||||
rethrowFailure();
|
||||
if (closing) {
|
||||
lock.unlock();
|
||||
commit();
|
||||
return;
|
||||
}
|
||||
if (items.size() >= Depth()) {
|
||||
if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline full while the enqueuing thread holds the GPU lock");
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
@@ -82,7 +94,10 @@ void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64
|
||||
++enqueued;
|
||||
items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes)});
|
||||
outstanding.fetch_add(1, std::memory_order_release);
|
||||
if (!thread.joinable()) thread = std::thread([this] { run(); });
|
||||
if (!thread.joinable()) {
|
||||
thread = std::thread([this] { run(); });
|
||||
runningCommitters.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
if (committerWaiting) wake.notify_one();
|
||||
if (!profile) return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
@@ -119,11 +134,13 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) {
|
||||
void DrawPipeline::run() {
|
||||
for (;;) {
|
||||
Commit* commit = nullptr;
|
||||
SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; });
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
committerWaiting = true;
|
||||
wake.wait(lock, [&] { return !items.empty(); });
|
||||
wake.wait(lock, [&] { return !items.empty() || closing; });
|
||||
committerWaiting = false;
|
||||
if (items.empty()) break;
|
||||
commit = stopped ? nullptr : &items.front().commit;
|
||||
}
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
@@ -155,6 +172,20 @@ void DrawPipeline::run() {
|
||||
outstanding.fetch_sub(1, std::memory_order_release);
|
||||
if (idleWaiters != 0) idle.notify_all();
|
||||
}
|
||||
runningCommitters.fetch_sub(1, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
void DrawPipeline::Stop() {
|
||||
std::thread committer;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
closing = true;
|
||||
committer = std::move(thread);
|
||||
}
|
||||
wake.notify_all();
|
||||
if (!committer.joinable()) return;
|
||||
if (committer.get_id() == std::this_thread::get_id()) committer.detach();
|
||||
else committer.join();
|
||||
}
|
||||
|
||||
void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) {
|
||||
@@ -162,6 +193,7 @@ void DrawPipeline::Drain(DrainReason reason, std::uint32_t opcode) {
|
||||
if (!Busy()) return;
|
||||
if (GuestMemory::GpuMutex().HeldByThisThread()) throw std::runtime_error("draw pipeline drain under the GPU lock (reason " + std::to_string(static_cast<unsigned>(reason)) + ", opcode " + std::to_string(opcode) + ")");
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
SpinUntil(HandoffSpinWindow() * 2, [&] { return !Busy(); });
|
||||
std::unique_lock lock(mutex);
|
||||
++idleWaiters;
|
||||
idle.wait(lock, [&] { return items.empty(); });
|
||||
|
||||
@@ -217,6 +217,7 @@ void Driver::relocateDrawEntry(const QueueState& queue, std::uint64_t alias, std
|
||||
copy->words = variant->words;
|
||||
copy->forgetSerial = variant->forgetSerial;
|
||||
copy->compiled = variant->compiled;
|
||||
copy->meshIndexDescriptor = variant->meshIndexDescriptor;
|
||||
copy->shader = variant->shader;
|
||||
copy->dataPositions = variant->dataPositions;
|
||||
copy->dataSlots = variant->dataSlots;
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ParallelCompare.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "ShaderDiskCache.hpp"
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -69,8 +73,7 @@ bool StackHoldsImport() {
|
||||
const auto low = bounds.low;
|
||||
const auto high = bounds.high;
|
||||
#endif
|
||||
const volatile unsigned char marker = 0;
|
||||
const auto frame = reinterpret_cast<std::uintptr_t>(&marker);
|
||||
const auto frame = reinterpret_cast<std::uintptr_t>(__builtin_frame_address(0));
|
||||
return frame >= low && frame < high && frame - low >= InlineImportStackBytes;
|
||||
}
|
||||
|
||||
@@ -115,15 +118,19 @@ void Driver::stop() {
|
||||
for (auto& [queue, worker] : workers) {
|
||||
if (worker.thread.joinable()) worker.thread.join();
|
||||
}
|
||||
DrawPipeline::Queue0().Stop();
|
||||
StopWorkerSampler();
|
||||
CloseRegistrationPreparation();
|
||||
Graphics::StopPipelineLibraryWorker();
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
device.Reset();
|
||||
replacedDevices.clear();
|
||||
Graphics::FlushPipelineJournal();
|
||||
Graphics::ShutdownGuestBufferWorkers();
|
||||
Graphics::StopCompareHelpers();
|
||||
ShaderRecompiler::ShaderDiskCache::Shutdown();
|
||||
PerformanceTrace::StopOutput();
|
||||
FlushAsyncLog_nid_no_patch();
|
||||
FlushAsyncLogAtEnd_nid_no_patch();
|
||||
StopProfileOutput_nid_no_patch();
|
||||
stopped = true;
|
||||
}
|
||||
@@ -222,3 +229,11 @@ bool& Driver::onWorkerThread() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
std::unique_lock<std::mutex> HoldAsyncLog() {
|
||||
return AsyncLog::Get().Hold();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -411,7 +411,10 @@ void Driver::execute(const Submission& submission) {
|
||||
return;
|
||||
}
|
||||
if ((std::atomic_ref<std::uint32_t>(*const_cast<std::uint32_t*>(submission.rewindTail - 1)).load(std::memory_order_acquire) & 0x80000000u) != 0) {
|
||||
DrawPipeline::Queue0().Enqueue([this] { submitOpenWork(); }, {});
|
||||
DrawPipeline::Queue0().Enqueue([this] {
|
||||
submitOpenWork();
|
||||
wakeQueueZeroForCompletions();
|
||||
}, {});
|
||||
executeRewindTail(submission);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,12 @@ void Driver::submitOpenWork() {
|
||||
if (localDevice != nullptr) localDevice->SubmitRecorded(true);
|
||||
}
|
||||
|
||||
void Driver::wakeQueueZeroForCompletions() {
|
||||
if (!completionsPending()) return;
|
||||
std::lock_guard lock(mutex);
|
||||
changed.notify_all();
|
||||
}
|
||||
|
||||
void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites) {
|
||||
struct Complete {
|
||||
Driver& driver;
|
||||
@@ -98,7 +104,7 @@ void Driver::completeSubmission(std::uint64_t serial, std::uint64_t received, st
|
||||
driver.forgetUnfinishedWrites(driver.workers.at(0), labelWrites);
|
||||
const auto found = std::find(driver.queue0Uncommitted.begin(), driver.queue0Uncommitted.end(), received);
|
||||
if (found != driver.queue0Uncommitted.end()) driver.queue0Uncommitted.erase(found);
|
||||
notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0;
|
||||
notify = driver.idleWaiters != 0 || driver.orderHolders.load(std::memory_order_acquire) != 0 || completionsPending();
|
||||
}
|
||||
if (notify) driver.changed.notify_all();
|
||||
}
|
||||
@@ -214,9 +220,9 @@ void Driver::run(std::uint32_t id) noexcept {
|
||||
&& ((!storeIdleResults && Graphics::StorageTexture::AnyPending()) || idleEpoch.load(std::memory_order_acquire) != idleEpochAtCheck)) continue;
|
||||
if (id == 0) queue0Dormant.store(true, std::memory_order_relaxed);
|
||||
const auto idleEpochAtWait = idleEpoch.load(std::memory_order_acquire);
|
||||
changed.wait(lock, [&] { return ready() || (id == 0 && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait); });
|
||||
changed.wait(lock, [&] { return ready() || (id == 0 && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())); });
|
||||
if (id == 0) queue0Dormant.store(false, std::memory_order_relaxed);
|
||||
if (id == 0 && !ready() && idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait) continue;
|
||||
if (id == 0 && !ready() && (idleEpoch.load(std::memory_order_acquire) != idleEpochAtWait || completionsPending())) continue;
|
||||
break;
|
||||
}
|
||||
if (poll(ready)) break;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "CompiledVariant.hpp"
|
||||
#include "Optimization/ResourceProgram.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
@@ -933,7 +934,8 @@ class RegistrationPreparePool {
|
||||
public:
|
||||
static RegistrationPreparePool& Instance() {
|
||||
static auto* pool = [] {
|
||||
auto* created = new RegistrationPreparePool(std::max(2u, std::thread::hardware_concurrency() / 2u));
|
||||
std::lock_guard lifecycle(Lifecycle());
|
||||
auto* created = new RegistrationPreparePool(Closing() ? 0u : std::max(2u, std::thread::hardware_concurrency() / 2u));
|
||||
PreparationSlot().store(created);
|
||||
return created;
|
||||
}();
|
||||
@@ -947,7 +949,8 @@ public:
|
||||
static RegistrationPreparePool& Prebuild() {
|
||||
static auto* pool = [] {
|
||||
const char* text = std::getenv("APS5_PREBUILD_THREADS");
|
||||
auto* created = new RegistrationPreparePool(text != nullptr ? std::max(1u, static_cast<unsigned>(std::strtoul(text, nullptr, 10))) : 1u);
|
||||
std::lock_guard lifecycle(Lifecycle());
|
||||
auto* created = new RegistrationPreparePool(Closing() ? 0u : text != nullptr ? std::max(1u, static_cast<unsigned>(std::strtoul(text, nullptr, 10))) : 1u);
|
||||
PrebuildSlot().store(created);
|
||||
return created;
|
||||
}();
|
||||
@@ -958,7 +961,33 @@ public:
|
||||
return PrebuildSlot().load();
|
||||
}
|
||||
|
||||
static unsigned LiveWorkers() {
|
||||
return liveWorkers.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
static void CloseAll() {
|
||||
std::array<RegistrationPreparePool*, 2> pools{};
|
||||
{
|
||||
std::lock_guard lifecycle(Lifecycle());
|
||||
Closing() = true;
|
||||
pools = {Existing(), ExistingPrebuild()};
|
||||
}
|
||||
for (auto* pool : pools) {
|
||||
if (pool != nullptr) pool->Close();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static std::mutex& Lifecycle() {
|
||||
static std::mutex lifecycle;
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
static bool& Closing() {
|
||||
static bool closing = false;
|
||||
return closing;
|
||||
}
|
||||
|
||||
static std::atomic<RegistrationPreparePool*>& PreparationSlot() {
|
||||
static std::atomic<RegistrationPreparePool*> slot{nullptr};
|
||||
return slot;
|
||||
@@ -1002,9 +1031,17 @@ public:
|
||||
}
|
||||
|
||||
void Close() {
|
||||
std::unique_lock lock(mutex);
|
||||
closed = true;
|
||||
idle.wait(lock, [&] { return tasks.empty() && running == 0; });
|
||||
std::vector<pthread_t> joined;
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
closed = true;
|
||||
idle.wait(lock, [&] { return tasks.empty() && running == 0; });
|
||||
joined.swap(workers);
|
||||
}
|
||||
ready.notify_all();
|
||||
for (const auto worker : joined) {
|
||||
if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -1015,8 +1052,9 @@ private:
|
||||
for (unsigned index = 0; index < count; ++index) {
|
||||
pthread_t thread;
|
||||
if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) {
|
||||
pthread_detach(thread);
|
||||
workers.push_back(thread);
|
||||
++threads;
|
||||
liveWorkers.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
}
|
||||
pthread_attr_destroy(&attributes);
|
||||
@@ -1029,7 +1067,11 @@ private:
|
||||
Task task;
|
||||
{
|
||||
std::unique_lock lock(pool.mutex);
|
||||
pool.ready.wait(lock, [&] { return !pool.tasks.empty(); });
|
||||
pool.ready.wait(lock, [&] { return !pool.tasks.empty() || pool.closed; });
|
||||
if (pool.tasks.empty()) {
|
||||
liveWorkers.fetch_sub(1, std::memory_order_acq_rel);
|
||||
return nullptr;
|
||||
}
|
||||
task = std::move(pool.tasks.front());
|
||||
pool.tasks.pop_front();
|
||||
++pool.running;
|
||||
@@ -1062,7 +1104,9 @@ private:
|
||||
std::condition_variable ready;
|
||||
std::condition_variable idle;
|
||||
std::deque<Task> tasks;
|
||||
std::vector<pthread_t> workers;
|
||||
unsigned threads = 0;
|
||||
inline static std::atomic<unsigned> liveWorkers{0};
|
||||
unsigned running = 0;
|
||||
std::uint64_t submitted = 0;
|
||||
std::set<std::uint64_t> unfinished;
|
||||
@@ -1124,8 +1168,11 @@ void AwaitLibraryPrebuilds() {
|
||||
}
|
||||
|
||||
void CloseRegistrationPreparation() {
|
||||
RegistrationPreparePool::Instance().Close();
|
||||
if (auto* prebuild = RegistrationPreparePool::ExistingPrebuild()) prebuild->Close();
|
||||
RegistrationPreparePool::CloseAll();
|
||||
}
|
||||
|
||||
unsigned RegistrationPreparationThreads() {
|
||||
return RegistrationPreparePool::LiveWorkers();
|
||||
}
|
||||
|
||||
std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decoded, const ShaderRecompiler::SpirvTarget& target) {
|
||||
@@ -1198,7 +1245,16 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
|
||||
return prepared;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void BindTitleThread() {
|
||||
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("title", TraceThread::DefaultCapacity / 8);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
|
||||
BindTitleThread();
|
||||
PerformanceContext timingContext(FrameTiming::Preparation());
|
||||
PerformanceTimer timing("Shader.ResolveGraphicsStagesAbi");
|
||||
ShaderPreparationTransaction transaction;
|
||||
@@ -1275,6 +1331,7 @@ void Driver::ResolveGraphicsStagesAbi(std::span<const Shader* const> stages, std
|
||||
}
|
||||
|
||||
void Driver::ResolveShaderAbi(const Shader* shader, std::span<const ShaderRegister> context, std::span<const ShaderRegister> primitive) {
|
||||
BindTitleThread();
|
||||
PerformanceContext timingContext(FrameTiming::Preparation());
|
||||
PerformanceTimer timing("Shader.ResolveAbi");
|
||||
ShaderPreparationTransaction transaction;
|
||||
@@ -1346,6 +1403,7 @@ std::uint64_t NullPixelProgramAddress() {
|
||||
}
|
||||
|
||||
void Driver::ResolveGraphicsAbi(const Shader* vertex, const Shader* pixel, std::uint32_t primitiveType) {
|
||||
BindTitleThread();
|
||||
PerformanceContext timingContext(FrameTiming::Preparation());
|
||||
PerformanceTimer timing("Shader.ResolveGraphicsAbi");
|
||||
CheckFailure();
|
||||
@@ -1472,6 +1530,7 @@ std::shared_ptr<const ShaderRegistry> Driver::RegisteredShaders() {
|
||||
}
|
||||
|
||||
void Driver::RegisterShader(const Shader* shader) {
|
||||
BindTitleThread();
|
||||
PerformanceContext timingContext(FrameTiming::Preparation());
|
||||
PerformanceTimer timing("Shader.Register");
|
||||
ShaderPreparationTransaction transaction;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/ProfileOutput.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/WriteWatchCoverage.hpp"
|
||||
#include "ThreadOwned.hpp"
|
||||
@@ -657,8 +658,7 @@ bool onOwnLiveStack(std::uintptr_t address, std::size_t bytes) {
|
||||
}
|
||||
};
|
||||
thread_local const Bounds bounds;
|
||||
const volatile unsigned char marker = 0;
|
||||
const auto frame = reinterpret_cast<std::uintptr_t>(&marker);
|
||||
const auto frame = reinterpret_cast<std::uintptr_t>(__builtin_frame_address(0));
|
||||
return bounds.high != 0 && frame >= bounds.low && frame < bounds.high && address >= frame && address < bounds.high && bytes <= bounds.high - address;
|
||||
}
|
||||
|
||||
@@ -1931,12 +1931,12 @@ void GpuMutexType::lock() {
|
||||
if (!GpuLockProfiled()) {
|
||||
auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr;
|
||||
if (trace == nullptr) {
|
||||
mutex.lock();
|
||||
LockSpinning(mutex, HandoffSpinWindow());
|
||||
} else {
|
||||
const auto site = static_cast<std::size_t>(std::exchange(LockStats().nextSite, GpuLockSite::Other));
|
||||
if (!mutex.try_lock()) {
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
mutex.lock();
|
||||
LockSpinning(mutex, HandoffSpinWindow());
|
||||
trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, std::chrono::steady_clock::now());
|
||||
}
|
||||
}
|
||||
@@ -1954,7 +1954,7 @@ void GpuMutexType::lock() {
|
||||
HolderMatrixValues before{};
|
||||
if (holders) Holders().Snapshot(before);
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
mutex.lock();
|
||||
LockSpinning(mutex, HandoffSpinWindow());
|
||||
acquired();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (auto* trace = PerformanceTrace::Recording() ? PerformanceTrace::Thread() : nullptr) trace->Add("GpuMutex.wait", GpuLockSiteNames[site], start, now);
|
||||
@@ -2100,9 +2100,26 @@ void Read(std::uint64_t address, std::span<std::byte> destination, std::size_t a
|
||||
if (trace) std::fprintf(stderr, "[unreadable] read from +0x%llx\n", ModuleOffset(__builtin_return_address(0)));
|
||||
throw;
|
||||
}
|
||||
std::memcpy(destination.data(), source, destination.size());
|
||||
CopyGuestBytes(destination.data(), source, destination.size());
|
||||
}
|
||||
|
||||
#ifdef ANYPS5_ENABLE_ASAN
|
||||
[[gnu::no_sanitize("address")]] void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) {
|
||||
auto* to = static_cast<unsigned char*>(destination);
|
||||
const auto* from = static_cast<const unsigned char*>(source);
|
||||
std::size_t index = 0;
|
||||
for (; index + sizeof(std::uint64_t) <= bytes; index += sizeof(std::uint64_t)) {
|
||||
const std::uint64_t word = *reinterpret_cast<const volatile std::uint64_t*>(from + index);
|
||||
std::memcpy(to + index, &word, sizeof(word));
|
||||
}
|
||||
for (; index < bytes; ++index) to[index] = static_cast<const volatile unsigned char*>(from)[index];
|
||||
}
|
||||
#else
|
||||
void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) {
|
||||
std::memcpy(destination, source, bytes);
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
void copyCommitted(std::uint64_t address, std::span<std::byte> destination) {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
std::atomic<bool> PerformanceTrace::recording{false};
|
||||
thread_local TraceThread* PerformanceTrace::local = nullptr;
|
||||
|
||||
}
|
||||
|
||||
extern "C" bool AgcDriverTraceRecording_nid_postfix() {
|
||||
return AgcDriver::PerformanceTrace::recording.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
extern "C" AgcDriver::TraceThread* AgcDriverTraceThread_nid_postfix() {
|
||||
return AgcDriver::PerformanceTrace::local;
|
||||
}
|
||||
@@ -219,7 +219,7 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
|
||||
const auto index = static_cast<std::size_t>((address % PageBytes) / sizeof(*value));
|
||||
if (!page.valid.Test(index) && page.lazy) {
|
||||
const auto first = index / BlockWords * BlockWords;
|
||||
std::memcpy(&page.words[first], reinterpret_cast<const void*>((address & ~static_cast<std::uint64_t>(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t));
|
||||
GuestMemory::CopyGuestBytes(&page.words[first], reinterpret_cast<const void*>((address & ~static_cast<std::uint64_t>(PageBytes - 1)) + first * sizeof(std::uint32_t)), BlockWords * sizeof(std::uint32_t));
|
||||
page.valid.SetBlock(index);
|
||||
}
|
||||
if (!page.valid.Test(index)) {
|
||||
|
||||
@@ -281,6 +281,7 @@ struct VulkanDevice::State {
|
||||
VkDeviceSize uploadSize = 0;
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
std::string driverInfo;
|
||||
bool individuallyFreeDescriptorSets = false;
|
||||
PFN_vkGetCalibratedTimestampsKHR calibratedTimestamps = nullptr;
|
||||
PFN_vkCmdSetCheckpointNV checkpointMarker = nullptr;
|
||||
bool deviceLostHook = false;
|
||||
@@ -304,6 +305,7 @@ struct VulkanDevice::State {
|
||||
bool float16DenormPreserve = false;
|
||||
bool demoteToHelperInvocation = false;
|
||||
bool earlyFragmentSampleMaskTestBeforeSampleCounting = false;
|
||||
bool robustBufferAccess2 = 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;
|
||||
@@ -1134,6 +1136,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties);
|
||||
state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo;
|
||||
state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE;
|
||||
}
|
||||
auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), availableExtensions);
|
||||
auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), availableExtensions);
|
||||
@@ -1844,9 +1847,12 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
VkPhysicalDeviceFeatures2 queried{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness2};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &queried);
|
||||
}
|
||||
if (robustness2.nullDescriptor == VK_TRUE) {
|
||||
state->robustBufferAccess2 = robustness2.robustBufferAccess2 == VK_TRUE && enabled.robustBufferAccess == VK_TRUE;
|
||||
if (robustness2.nullDescriptor == VK_TRUE || state->robustBufferAccess2) {
|
||||
const bool nullDescriptor = robustness2.nullDescriptor == VK_TRUE;
|
||||
robustness2 = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
|
||||
robustness2.nullDescriptor = VK_TRUE;
|
||||
robustness2.nullDescriptor = nullDescriptor ? VK_TRUE : VK_FALSE;
|
||||
robustness2.robustBufferAccess2 = state->robustBufferAccess2 ? VK_TRUE : VK_FALSE;
|
||||
robustness2.pNext = bdaFeatures.pNext;
|
||||
bdaFeatures.pNext = &robustness2;
|
||||
deviceExtensions.push_back(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME);
|
||||
@@ -3507,6 +3513,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
|
||||
target.spannedWaves = state->spannedWaves;
|
||||
target.fixedPushSlots = state->graphicsPipelineLibrary;
|
||||
target.registrationModules = state->graphicsPipelineLibrary;
|
||||
target.robustBufferAccess2 = state->robustBufferAccess2;
|
||||
target.srgbDecodeFormats = state->srgbDecodeFormats;
|
||||
target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0;
|
||||
if (state->meshShader) {
|
||||
@@ -3523,6 +3530,10 @@ std::optional<ShaderRecompiler::GeometryStageLimits> VulkanDevice::GeometryLimit
|
||||
return ShaderRecompiler::GeometryStageLimits{limits.maxGeometryInputComponents, limits.maxGeometryOutputComponents, limits.maxGeometryOutputVertices, limits.maxGeometryTotalOutputComponents, limits.maxFragmentInputComponents};
|
||||
}
|
||||
|
||||
std::uint32_t VulkanDevice::MaxFragmentInputComponents() const {
|
||||
return state->properties.limits.maxFragmentInputComponents;
|
||||
}
|
||||
|
||||
bool VulkanDevice::PrimitiveListRestart() const {
|
||||
return state->primitiveListRestart;
|
||||
}
|
||||
@@ -3587,6 +3598,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
state->hostImportAlignment
|
||||
};
|
||||
context.pipelineCacheSynchronization = state->pipelineCache ? &state->pipelineCache->Synchronization() : nullptr;
|
||||
context.individuallyFreeDescriptorSets = state->individuallyFreeDescriptorSets;
|
||||
context.dmaBufImport = state->dmaBufImport;
|
||||
context.sparseBinding = state->sparseBinding;
|
||||
context.queueFamily = state->queueFamily;
|
||||
@@ -3649,6 +3661,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
context.shaderStencilExport = state->shaderStencilExport;
|
||||
context.memoryProperties2 =state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
|
||||
context.singlePassStorage = state->storageImageWithoutFormat;
|
||||
context.cacheReclaim = Graphics::ReleaseTextureCaches;
|
||||
return context;
|
||||
}
|
||||
|
||||
@@ -3736,11 +3749,24 @@ bool TemplateDataRefresh() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool DispatchMove() {
|
||||
static const bool enabled = std::getenv("APS5_NO_DISPATCH_MOVE") == nullptr && TemplateDataRefresh();
|
||||
return enabled;
|
||||
}
|
||||
|
||||
std::size_t DispatchMoveBytes() {
|
||||
static const std::size_t bytes = [] {
|
||||
const char* text = std::getenv("APS5_DISPMOVE_MAX_KIB");
|
||||
return static_cast<std::size_t>(text != nullptr ? std::strtoull(text, nullptr, 10) : 1024ull) * 1024u;
|
||||
}();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
// The content key of a compute stage, naming the device: the cache is process-wide and the driver
|
||||
// replaces the headless device with the windowed one while workers may still use the old one, so
|
||||
// the key names the device that built the entry (its descriptor set and pooled buffers belong to it).
|
||||
ResourceCache::Key DispatchContentKey(const Graphics::CompiledShader& shader, VkDevice device) {
|
||||
auto key = Graphics::ShaderResources::ContentKey(shader, !TemplateDataRefresh());
|
||||
ResourceCache::Key DispatchContentKey(const Graphics::CompiledShader& shader, VkDevice device, bool movable = true) {
|
||||
auto key = Graphics::ShaderResources::ContentKey(shader, !TemplateDataRefresh(), movable && DispatchMove());
|
||||
const auto deviceHandle = reinterpret_cast<std::uint64_t>(device);
|
||||
key.push_back(static_cast<std::uint32_t>(deviceHandle));
|
||||
key.push_back(static_cast<std::uint32_t>(deviceHandle >> 32u));
|
||||
@@ -3771,6 +3797,7 @@ struct PreparedDispatch {
|
||||
std::shared_ptr<Graphics::ShaderResources> resources;
|
||||
std::shared_ptr<Graphics::ShaderResources> cached;
|
||||
ResourceCache::Key key;
|
||||
bool exact = false;
|
||||
// APS5_PROFILE_DRAW: stage A's time, added to the [dispatch] phase totals by the dispatch, and
|
||||
// the prepare's parts for the driver's 'prepare:' rows (PreparePhase order).
|
||||
double prepareMs = 0;
|
||||
@@ -3803,6 +3830,8 @@ struct PresyncCounters {
|
||||
std::atomic<std::uint64_t> checked{0};
|
||||
std::atomic<std::uint64_t> presyncs{0};
|
||||
std::atomic<std::uint64_t> waitedUs{0};
|
||||
std::atomic<std::uint64_t> keyChecks{0};
|
||||
std::atomic<std::uint64_t> keyWaits{0};
|
||||
std::atomic<std::int64_t> lastReport{0};
|
||||
};
|
||||
|
||||
@@ -3968,6 +3997,63 @@ RecipeCounters& Recipes() {
|
||||
return counters;
|
||||
}
|
||||
|
||||
constexpr std::size_t MoveRefusals = static_cast<std::size_t>(Graphics::ShaderResources::MoveRefusal::Count);
|
||||
|
||||
struct DispatchMoveCounters {
|
||||
std::atomic<std::uint64_t> checked{0}, exact{0}, moved{0}, buffers{0}, bytes{0};
|
||||
std::atomic<std::uint64_t> rebuilt{0}, kept{0}, served{0};
|
||||
std::array<std::atomic<std::uint64_t>, 5> sizes{};
|
||||
std::array<std::atomic<std::uint64_t>, MoveRefusals> refused{};
|
||||
std::array<std::atomic<std::uint64_t>, MoveRefusals> filtered{};
|
||||
std::atomic<std::int64_t> lastReport{0};
|
||||
};
|
||||
|
||||
DispatchMoveCounters& DispatchMoves() {
|
||||
static DispatchMoveCounters counters;
|
||||
return counters;
|
||||
}
|
||||
|
||||
void reportDispatchMove() {
|
||||
auto& moves = DispatchMoves();
|
||||
const auto nowMs = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
auto last = moves.lastReport.load();
|
||||
if (nowMs - last < 10000 || !moves.lastReport.compare_exchange_strong(last, nowMs)) return;
|
||||
const auto take = [](std::atomic<std::uint64_t>& counter) { return static_cast<unsigned long long>(counter.exchange(0, std::memory_order_relaxed)); };
|
||||
const auto list = [&](std::array<std::atomic<std::uint64_t>, MoveRefusals>& counters) {
|
||||
std::string text;
|
||||
unsigned long long total = 0;
|
||||
char item[80];
|
||||
for (std::size_t i = 1; i < MoveRefusals; ++i) {
|
||||
const auto count = take(counters[i]);
|
||||
total += count;
|
||||
if (count == 0) continue;
|
||||
std::snprintf(item, sizeof(item), "%s %s %llu", text.empty() ? "" : ",", Graphics::ShaderResources::MoveRefusalName(static_cast<Graphics::ShaderResources::MoveRefusal>(i)), count);
|
||||
text += item;
|
||||
}
|
||||
std::snprintf(item, sizeof(item), "%llu (", total);
|
||||
return std::string(item) + text + ")";
|
||||
};
|
||||
const auto checked = take(moves.checked);
|
||||
const auto exact = take(moves.exact);
|
||||
const auto moved = take(moves.moved);
|
||||
const auto buffers = take(moves.buffers);
|
||||
const auto bytes = take(moves.bytes);
|
||||
const auto rebuilt = take(moves.rebuilt);
|
||||
const auto kept = take(moves.kept);
|
||||
const auto served = take(moves.served);
|
||||
unsigned long long sizes[5];
|
||||
for (std::size_t i = 0; i < 5; ++i) sizes[i] = take(moves.sizes[i]);
|
||||
const auto refused = list(moves.refused);
|
||||
const auto filtered = list(moves.filtered);
|
||||
AgcDriver::ProfilePrint_nid_no_patch("[dispmove] (10 s) %llu template hits checked: %llu with every read-only buffer where it was built, %llu with %llu moved ones (%.2f MiB snapshotted; per dispatch up to 1 KiB %llu, 16 KiB %llu, 256 KiB %llu, 4 MiB %llu, more %llu); refused %s at stage A (the hit was dropped before the record), %s at record; after a refusal: %llu rebuilt, %llu inserted beside the movable template (kept), %llu served by an exact entry\n", checked, exact, moved, buffers, static_cast<double>(bytes) / 1048576.0, sizes[0], sizes[1], sizes[2], sizes[3], sizes[4], filtered.c_str(), refused.c_str(), rebuilt, kept, served);
|
||||
}
|
||||
|
||||
void noteDispatchMoveSize(std::size_t bytes) {
|
||||
auto& moves = DispatchMoves();
|
||||
const std::size_t bucket = bytes <= 1024 ? 0 : bytes <= 16384 ? 1 : bytes <= 262144 ? 2 : bytes <= 4194304 ? 3 : 4;
|
||||
moves.sizes[bucket].fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void reportRecipes() {
|
||||
auto& recipes = Recipes();
|
||||
const auto nowMs = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
@@ -4118,12 +4204,23 @@ struct RecordedDispatch {
|
||||
// Whether the data refresh recorded anything.
|
||||
bool refreshed = false;
|
||||
bool resourceKeyMatched = false;
|
||||
std::shared_ptr<Graphics::ShaderResources::DrawBindings> bindings{};
|
||||
};
|
||||
|
||||
VkDevice VulkanDevice::Device() const {
|
||||
return state->device;
|
||||
}
|
||||
|
||||
VulkanDevice::DispatchMoveCount VulkanDevice::DispatchMoveCounts() {
|
||||
auto& moves = DispatchMoves();
|
||||
DispatchMoveCount count{moves.checked.load(), moves.exact.load(), moves.moved.load(), 0, 0, moves.rebuilt.load(), moves.kept.load(), moves.served.load()};
|
||||
for (std::size_t i = 1; i < MoveRefusals; ++i) {
|
||||
count.refused += moves.refused[i].load();
|
||||
count.filtered += moves.filtered[i].load();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool VulkanDevice::DispatchRecipes() {
|
||||
static const bool noDispatchRecipe = std::getenv("APS5_NO_DISPATCH_RECIPE") != nullptr;
|
||||
return !noDispatchRecipe;
|
||||
@@ -4185,23 +4282,31 @@ RecipeOutcome VulkanDevice::DrawFromRecipe(const Graphics::State& graphics, cons
|
||||
return RecipeOutcome::Recorded;
|
||||
}
|
||||
|
||||
std::uint64_t VulkanDevice::presync(std::span<const std::pair<std::uint64_t, std::uint64_t>> surfaces) {
|
||||
std::uint64_t VulkanDevice::presync(std::span<const Graphics::PresyncSurface> surfaces) {
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
auto& counters = Presyncs();
|
||||
bool overlaps = false;
|
||||
for (const auto& [address, bytes] : surfaces) overlaps = overlaps || Graphics::Recorder::SnapshotWriteOverlaps(address, static_cast<std::size_t>(bytes));
|
||||
for (const auto& surface : surfaces) {
|
||||
const auto bytes = static_cast<std::size_t>(surface.bytes);
|
||||
if (surface.dccKeys) counters.keyChecks.fetch_add(1, std::memory_order_relaxed);
|
||||
overlaps = overlaps || (surface.dccKeys ? Graphics::DccKeyWritersPending(surface.address, bytes) : Graphics::Recorder::SnapshotWriteOverlaps(surface.address, bytes));
|
||||
}
|
||||
std::uint64_t serial = 0;
|
||||
bool texelsPending = false;
|
||||
if (overlaps) {
|
||||
// No reap here (SubmitAndEpoch's would run completions, which a compute worker must not do
|
||||
// on queue 0's behalf, see SubmitRecorded): the dispatch reaps after the wait.
|
||||
std::lock_guard lock(GuestMemory::GpuMutex());
|
||||
if (state->recorder) {
|
||||
bool open = false;
|
||||
for (const auto& [address, bytes] : surfaces) {
|
||||
const auto info = state->recorder->DescribePendingWrite(address, static_cast<std::size_t>(bytes));
|
||||
for (const auto& surface : surfaces) {
|
||||
const auto bytes = static_cast<std::size_t>(surface.bytes);
|
||||
if (surface.dccKeys && !Graphics::DccKeyWriterNeedsSync(surface.address, bytes)) continue;
|
||||
const auto info = state->recorder->DescribePendingWrite(surface.address, bytes);
|
||||
if (!info.has_value()) continue;
|
||||
open = open || info->open;
|
||||
serial = std::max(serial, info->serial);
|
||||
texelsPending = texelsPending || !surface.dccKeys;
|
||||
}
|
||||
if (open) serial = state->recorder->SubmitAndEpoch();
|
||||
}
|
||||
@@ -4210,16 +4315,25 @@ std::uint64_t VulkanDevice::presync(std::span<const std::pair<std::uint64_t, std
|
||||
const auto waitStart = std::chrono::steady_clock::now();
|
||||
WaitRecorded(serial);
|
||||
counters.presyncs.fetch_add(1, std::memory_order_relaxed);
|
||||
if (!texelsPending) counters.keyWaits.fetch_add(1, std::memory_order_relaxed);
|
||||
counters.waitedUs.fetch_add(static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(std::chrono::steady_clock::now() - waitStart).count()), std::memory_order_relaxed);
|
||||
}
|
||||
if (profile) {
|
||||
const auto nowMs = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
auto last = counters.lastReport.load();
|
||||
if (nowMs - last >= 10000 && counters.lastReport.compare_exchange_strong(last, nowMs)) AgcDriver::ProfilePrint_nid_no_patch("[presync] %llu dispatches checked, %llu pre-syncs waited %.0f ms (cumulative)\n", static_cast<unsigned long long>(counters.checked.load()), static_cast<unsigned long long>(counters.presyncs.load()), counters.waitedUs.load() / 1000.0);
|
||||
if (nowMs - last >= 10000 && counters.lastReport.compare_exchange_strong(last, nowMs)) {
|
||||
const auto keyWaitCounts = Graphics::KeyWaitCounts();
|
||||
AgcDriver::ProfilePrint_nid_no_patch("[presync] %llu dispatches checked, %llu pre-syncs waited %.0f ms (cumulative); key ranges %llu checked, %llu pre-syncs that only the keys required; WaitForKeyWriters %llu calls: %llu without a pending writer, %llu known keys, %llu fills leaving uncompressed, %llu synced\n", static_cast<unsigned long long>(counters.checked.load()), static_cast<unsigned long long>(counters.presyncs.load()), counters.waitedUs.load() / 1000.0, static_cast<unsigned long long>(counters.keyChecks.load()), static_cast<unsigned long long>(counters.keyWaits.load()), static_cast<unsigned long long>(keyWaitCounts.calls), static_cast<unsigned long long>(keyWaitCounts.noPending), static_cast<unsigned long long>(keyWaitCounts.known), static_cast<unsigned long long>(keyWaitCounts.fillsUncompressed), static_cast<unsigned long long>(keyWaitCounts.synced));
|
||||
}
|
||||
}
|
||||
return serial;
|
||||
}
|
||||
|
||||
VulkanDevice::PresyncStatistics VulkanDevice::PresyncCounts() {
|
||||
const auto& counters = Presyncs();
|
||||
return {counters.checked.load(std::memory_order_relaxed), counters.presyncs.load(std::memory_order_relaxed), counters.keyChecks.load(std::memory_order_relaxed), counters.keyWaits.load(std::memory_order_relaxed)};
|
||||
}
|
||||
|
||||
std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderRecompiler::RecompileResult& shader, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t dispatchThreads, std::uint64_t programAddress) {
|
||||
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
|
||||
// APS5_LOCKED_BUILD=1: the whole build under the mutex, as before the split.
|
||||
@@ -4246,6 +4360,15 @@ std::shared_ptr<PreparedDispatch> VulkanDevice::PrepareDispatch(const ShaderReco
|
||||
prepared->key = DispatchContentKey(compute, context.device);
|
||||
phase(PreparedDispatch::PrepareKey);
|
||||
cached = state->resourceCache.Find(prepared->key);
|
||||
if (cached != nullptr && DispatchMove()) {
|
||||
Graphics::ShaderResources::MoveProof proof;
|
||||
if (!cached->MovedDispatchBuffers(compute, nullptr, DispatchMoveBytes(), proof).has_value()) {
|
||||
DispatchMoves().filtered[static_cast<std::size_t>(proof.refusal)].fetch_add(1, std::memory_order_relaxed);
|
||||
prepared->key = DispatchContentKey(compute, context.device, false);
|
||||
prepared->exact = true;
|
||||
cached = state->resourceCache.Find(prepared->key);
|
||||
}
|
||||
}
|
||||
prepared->cached = cached;
|
||||
phase(PreparedDispatch::PrepareFind);
|
||||
}
|
||||
@@ -4443,6 +4566,10 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
|
||||
recorder.Keep(record.objects);
|
||||
recorder.Keep(record.resources);
|
||||
if (record.resources != nullptr) recorder.KeepBytes(record.resources.get(), record.resources->CopiedBytes());
|
||||
if (record.bindings != nullptr) {
|
||||
recorder.Keep(record.bindings);
|
||||
for (const auto& snapshot : record.bindings->snapshots) recorder.KeepBytes(snapshot.buffer.get(), snapshot.buffer->Bytes().size());
|
||||
}
|
||||
recordStep(PhaseRecordKeeps);
|
||||
if (record.dataRefresh != RecordedDispatch::DataRefresh::None) {
|
||||
// The template's data buffers take this dispatch's words: a transfer write the pre-dispatch
|
||||
@@ -4492,7 +4619,9 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
|
||||
++d.preBarriersRecorded;
|
||||
}
|
||||
context.Resolved(&Graphics::DeviceFunctions::cmdBindPipeline, "vkCmdBindPipeline")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->pipeline);
|
||||
resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout);
|
||||
if (record.bindings != nullptr) Graphics::Require(record.bindings->allocation.set != VK_NULL_HANDLE, "dispatch overlay has no descriptor set");
|
||||
if (record.bindings != nullptr) context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, 0, 1, &record.bindings->allocation.set, 0, nullptr);
|
||||
else resources.Bind(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout);
|
||||
if (const auto dwords = record.shader->program->workgroupMemoryDwords; dwords != 0) {
|
||||
auto memory = state->WorkgroupMemoryFor(context, record.x, record.y, record.z, dwords);
|
||||
context.Resolved(&Graphics::DeviceFunctions::cmdBindDescriptorSets, "vkCmdBindDescriptorSets")(commands, VK_PIPELINE_BIND_POINT_COMPUTE, record.objects->layout, ShaderRecompiler::WorkgroupMemoryDescriptorSet, 1, &memory->set, 0, nullptr);
|
||||
@@ -4705,7 +4834,9 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
// 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;
|
||||
bool refusedMove = false;
|
||||
double revalidateMs = 0;
|
||||
std::shared_ptr<Graphics::ShaderResources::DrawBindings> overlay;
|
||||
// The batches the pre-sync (PrepareDispatch) waited for are reaped first: their completions
|
||||
// (CPU write-backs) land before stage B or a cached object's Revalidate reads, and the flush
|
||||
// hook then finds nothing pending over their surfaces.
|
||||
@@ -4725,6 +4856,11 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
// The image lookups of this call by outcome (stage B, a Revalidate or a locked build), for the
|
||||
// [indirect] line: reset here, read after the resources phase.
|
||||
if (profile && arguments != 0) Graphics::ThreadLookupOutcomes() = {};
|
||||
const auto noteRefusalBuild = [](bool inserted) {
|
||||
auto& moves = DispatchMoves();
|
||||
moves.rebuilt.fetch_add(1, std::memory_order_relaxed);
|
||||
if (inserted) moves.kept.fetch_add(1, std::memory_order_relaxed);
|
||||
};
|
||||
// The resource cache insert evicts the oldest entry, destroying its ShaderResources here when the
|
||||
// cache was the last owner: timed apart from the build.
|
||||
const auto insert = [&] {
|
||||
@@ -4773,6 +4909,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
}
|
||||
if (cacheable) {
|
||||
++d.cacheMisses;
|
||||
if (prepared->exact) noteRefusalBuild(resources->Reusable());
|
||||
if (resources->Reusable()) insert();
|
||||
}
|
||||
} else if (cacheable) {
|
||||
@@ -4780,13 +4917,54 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
// object it found: revalidated as a map hit would be (an entry replaced meanwhile is left
|
||||
// alone by the pointer-conditional Remove). APS5_NO_PREPARED_FIND=1 looks the key up again.
|
||||
static const bool preparedFind = std::getenv("APS5_NO_PREPARED_FIND") == nullptr;
|
||||
const bool exactEntry = prepared != nullptr && prepared->exact;
|
||||
contentKey = prepared != nullptr && !prepared->key.empty() ? std::move(prepared->key) : DispatchContentKey(shaders[0], context.device);
|
||||
auto cached = preparedFind && prepared != nullptr && prepared->cached != nullptr ? std::move(prepared->cached) : state->resourceCache.Find(contentKey);
|
||||
if (cached != nullptr) {
|
||||
if (cached->Revalidate(shaders[0])) {
|
||||
resources = std::move(cached);
|
||||
fromCache = true;
|
||||
++d.cacheHits;
|
||||
bool usable = true;
|
||||
if (DispatchMove() && !exactEntry) {
|
||||
auto& moves = DispatchMoves();
|
||||
Graphics::ShaderResources::MoveProof proof;
|
||||
auto moved = cached->MovedDispatchBuffers(shaders[0], state->recorder.get(), DispatchMoveBytes(), proof);
|
||||
moves.checked.fetch_add(1, std::memory_order_relaxed);
|
||||
if (moved.has_value() && !moved->empty()) {
|
||||
overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true);
|
||||
if (overlay != nullptr && overlay->allocation.set == VK_NULL_HANDLE) overlay.reset();
|
||||
if (overlay == nullptr) {
|
||||
proof.refusal = Graphics::ShaderResources::MoveRefusal::Bindings;
|
||||
moved.reset();
|
||||
}
|
||||
}
|
||||
if (!moved.has_value()) {
|
||||
usable = false;
|
||||
moves.refused[static_cast<std::size_t>(proof.refusal)].fetch_add(1, std::memory_order_relaxed);
|
||||
} else if (moved->empty()) {
|
||||
moves.exact.fetch_add(1, std::memory_order_relaxed);
|
||||
} else {
|
||||
moves.moved.fetch_add(1, std::memory_order_relaxed);
|
||||
moves.buffers.fetch_add(proof.buffers, std::memory_order_relaxed);
|
||||
moves.bytes.fetch_add(proof.bytes, std::memory_order_relaxed);
|
||||
noteDispatchMoveSize(proof.bytes);
|
||||
}
|
||||
}
|
||||
if (!usable) {
|
||||
contentKey = DispatchContentKey(shaders[0], context.device, false);
|
||||
refusedMove = true;
|
||||
cached = state->resourceCache.Find(contentKey);
|
||||
if (cached != nullptr && cached->Revalidate(shaders[0])) usable = true;
|
||||
else if (cached != nullptr) {
|
||||
state->resourceCache.Remove(contentKey, cached.get());
|
||||
++d.cacheInvalidated;
|
||||
state->recorder->Keep(std::move(cached));
|
||||
}
|
||||
}
|
||||
if (usable) {
|
||||
if (refusedMove || exactEntry) DispatchMoves().served.fetch_add(1, std::memory_order_relaxed);
|
||||
resources = std::move(cached);
|
||||
fromCache = true;
|
||||
++d.cacheHits;
|
||||
}
|
||||
} else {
|
||||
state->resourceCache.Remove(contentKey, cached.get());
|
||||
++d.cacheInvalidated;
|
||||
@@ -4805,6 +4983,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
if (profile) timer.add(PhaseResourcesFullBuild, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - buildStart).count());
|
||||
if (cacheable) {
|
||||
++d.cacheMisses;
|
||||
if (refusedMove) noteRefusalBuild(resources->Reusable());
|
||||
if (resources->Reusable()) insert();
|
||||
}
|
||||
}
|
||||
@@ -4827,6 +5006,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
AgcDriver::ProfilePrint_nid_no_patch("[rescache] %llu hits, %llu misses, %llu invalidated, %zu entries (dispatch + draw); template hits: %llu refreshed the data buffers (revalidate %.1f ms), %llu had the same words; Find calls %llu, Touch calls %llu\n", static_cast<unsigned long long>(d.cacheHits), static_cast<unsigned long long>(d.cacheMisses), static_cast<unsigned long long>(d.cacheInvalidated), state->resourceCache.Size(), static_cast<unsigned long long>(d.templateRefreshed), d.templateRevalidateMs, static_cast<unsigned long long>(d.templateSameWords), static_cast<unsigned long long>(ResourceCache::Finds()), static_cast<unsigned long long>(ResourceCache::Touches()));
|
||||
AgcDriver::ProfilePrint_nid_no_patch("[vk] deviceProc lookups inside the device call: %llu (%.2f per dispatch); pre-dispatch barriers recorded %llu, skipped %llu; compute pipelines created %llu (%llu under the GPU mutex)\n", static_cast<unsigned long long>(d.procLookups), d.profiledDispatches != 0 ? static_cast<double>(d.procLookups) / static_cast<double>(d.profiledDispatches) : 0.0, static_cast<unsigned long long>(d.preBarriersRecorded), static_cast<unsigned long long>(d.preBarriersSkipped), static_cast<unsigned long long>(computePipelinesCreated.load(std::memory_order_relaxed)), static_cast<unsigned long long>(computePipelinesCreatedLocked.load(std::memory_order_relaxed)));
|
||||
reportRecipes();
|
||||
reportDispatchMove();
|
||||
}
|
||||
timing.Mark("shader_resources");
|
||||
timer.phase(PhaseResources);
|
||||
@@ -4848,6 +5028,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
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;
|
||||
record.bindings = overlay;
|
||||
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;
|
||||
@@ -4901,7 +5082,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
|
||||
// object the resource cache serves under this content key (reusable: no lease, no copied
|
||||
// writes, every direct region import- or mirror-served), so a hit's proof is the template's
|
||||
// Revalidate and nothing needs completion work.
|
||||
if (recipeOut != nullptr && DispatchRecipes() && cacheable && resources->Reusable() && !resources->NeedsCompletion() && objects != nullptr && !contentKey.empty()) {
|
||||
if (recipeOut != nullptr && DispatchRecipes() && cacheable && overlay == nullptr && resources->Reusable() && !resources->NeedsCompletion() && objects != nullptr && !contentKey.empty()) {
|
||||
auto recipe = std::make_shared<Recipe>();
|
||||
recipe->device = state->device;
|
||||
recipe->templateRef = resources;
|
||||
|
||||
@@ -54,7 +54,8 @@ struct SlabSlot {
|
||||
class BufferPool {
|
||||
public:
|
||||
explicit BufferPool(const Context& context);
|
||||
static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory);
|
||||
static VkDeviceSize DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported = nullptr);
|
||||
static VkDeviceSize ReclaimTarget(VkDeviceSize bytes);
|
||||
~BufferPool();
|
||||
BufferPool(const BufferPool&) = delete;
|
||||
BufferPool& operator=(const BufferPool&) = delete;
|
||||
@@ -116,6 +117,7 @@ private:
|
||||
// when shared; the device tier for device-local memory while it has a budget).
|
||||
Tier& tierFor(std::size_t capacity, VkMemoryPropertyFlags properties);
|
||||
static bool DeviceTierEnabled();
|
||||
static VkDeviceSize deviceBudget(const Context& context);
|
||||
static bool DeviceTiered(VkMemoryPropertyFlags properties);
|
||||
static std::size_t NextCapacity(std::size_t capacity);
|
||||
VkDeviceSize destroy(const BufferAllocation& allocation) noexcept;
|
||||
@@ -127,6 +129,7 @@ private:
|
||||
// The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once.
|
||||
static std::size_t MaxSlots();
|
||||
std::size_t maxSlots;
|
||||
VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes);
|
||||
VkDevice device;
|
||||
PFN_vkUnmapMemory unmap;
|
||||
PFN_vkDestroyBuffer destroyBuffer;
|
||||
|
||||
@@ -172,6 +172,7 @@ struct Context {
|
||||
std::uint32_t settleFamily = 0;
|
||||
std::mutex* settleQueueMutex = nullptr;
|
||||
PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr;
|
||||
VkDeviceSize (*cacheReclaim)(VkDeviceSize bytes) = nullptr;
|
||||
RenderCache* renderCache = nullptr;
|
||||
DrawQueue* drawQueue = nullptr;
|
||||
GraphicsPipelineCache* graphicsPipelines = nullptr;
|
||||
@@ -211,6 +212,7 @@ struct Context {
|
||||
bool variableDescriptorCount = false;
|
||||
std::uint32_t imageTableCapacity = 0;
|
||||
std::uint32_t samplerTableCapacity = 0;
|
||||
bool individuallyFreeDescriptorSets = false;
|
||||
bool geometryShader = false;
|
||||
bool sampleRateShading = false;
|
||||
bool storageImageMultisample = false;
|
||||
@@ -322,6 +324,7 @@ inline void EndRenderPass(const Context& context, VkCommandBuffer commands) {
|
||||
}
|
||||
|
||||
std::uint32_t ImageTableCapacity(const Context& context);
|
||||
std::uint32_t StorageTableCapacity(const Context& context);
|
||||
std::uint32_t SamplerTableCapacity(const Context& context);
|
||||
|
||||
}
|
||||
|
||||
@@ -99,6 +99,16 @@ DccKeys ProvedCurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceByt
|
||||
void ReadTextureSurface(const GuestTextureResource& resource, DccKeys keys, std::span<std::byte> bytes);
|
||||
void NoteKeysFillOnGpu(std::uint64_t begin, std::size_t count, DccKeys keys);
|
||||
std::optional<DccKeys> WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes);
|
||||
bool DccKeyWritersPending(std::uint64_t metaAddress, std::size_t count);
|
||||
bool DccKeyWriterNeedsSync(std::uint64_t metaAddress, std::size_t count);
|
||||
struct DccKeyWaitCounts {
|
||||
std::uint64_t calls;
|
||||
std::uint64_t noPending;
|
||||
std::uint64_t known;
|
||||
std::uint64_t fillsUncompressed;
|
||||
std::uint64_t synced;
|
||||
};
|
||||
DccKeyWaitCounts KeyWaitCounts();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ void RequireNoNativeDepthFeedback_nid_no_patch(const Context& context, const Dep
|
||||
void ClearDepthSurfaces(VkDevice device);
|
||||
void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes);
|
||||
bool DepthSurfaceAt(std::uint64_t address);
|
||||
bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes);
|
||||
bool DepthSurfaceAt(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource);
|
||||
std::uint64_t HtileDepthClearAddress(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads);
|
||||
std::size_t HtileDepthClearBytes_nid_no_patch(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const ShaderRecompiler::ShaderComputeStageInfo& compute, std::span<const std::uint32_t> packet, std::uint64_t indirectArguments);
|
||||
|
||||
@@ -94,6 +94,15 @@ bool DrawRecipes();
|
||||
enum class IndirectDrawPath : std::uint8_t { Gpu, NotFolded, FetchUnknown, NonVertexPath, IndxOffset, DrawIndex, VertexRange, FeatureGap, PendingImage, PendingLabelOrCopy, NotImported, Disabled, Count };
|
||||
void CountIndirectDraw(IndirectDrawPath path, double readMs, bool rewritten = false);
|
||||
std::array<std::uint64_t, static_cast<std::size_t>(IndirectDrawPath::Count)> IndirectDrawCounts();
|
||||
|
||||
struct DrawPassCounts {
|
||||
std::uint64_t draws = 0;
|
||||
std::uint64_t passesBegun = 0;
|
||||
std::uint64_t passesContinued = 0;
|
||||
std::uint64_t keyStoreFlushes = 0;
|
||||
std::uint64_t storeFlushes = 0;
|
||||
};
|
||||
std::array<DrawPassCounts, 2> DrawPassTotals();
|
||||
const char* IndirectDrawPathName(IndirectDrawPath path);
|
||||
|
||||
// Draw packets that drew nothing, for the [draws] line: Nothing (an empty count, or no color writes
|
||||
|
||||
@@ -16,6 +16,8 @@ struct CompareSpan {
|
||||
void CompareSpans(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal);
|
||||
void CompareSpansFrom(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal, std::size_t parallelBytes);
|
||||
unsigned CompareHelpers();
|
||||
void StopCompareHelpers();
|
||||
unsigned CompareHelpersRunning();
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ bool HasStencil(VkFormat format);
|
||||
void ValidateViewport(const Context& context, const VkViewport& viewport);
|
||||
void ValidateDepthBounds(const Context& context, const State& state);
|
||||
void ValidateProvokingVertex(const Context& context, const State& state, std::span<const CompiledShader> shaders = {});
|
||||
bool BarycentricPatches(const State& state, std::span<const CompiledShader> shaders);
|
||||
bool ValidateSampleLocations(const Context& context, const State& state);
|
||||
|
||||
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
|
||||
|
||||
@@ -48,6 +48,9 @@ void WaitForOptimizedPipelines(VkDevice device);
|
||||
|
||||
void ClearPipelineLibraries(VkDevice device);
|
||||
|
||||
void StopPipelineLibraryWorker();
|
||||
unsigned PipelineLibraryWorkers();
|
||||
|
||||
struct PipelineLibraryCounters {
|
||||
std::array<std::uint64_t, 4> built{};
|
||||
std::uint64_t linked = 0;
|
||||
|
||||
@@ -64,6 +64,7 @@ private:
|
||||
VkBufferUsageFlags usage;
|
||||
std::shared_ptr<BufferPool> cache;
|
||||
bool hostPlaced = false;
|
||||
bool ready = false;
|
||||
};
|
||||
|
||||
// Records a whole-range buffer copy.
|
||||
|
||||
@@ -28,10 +28,12 @@ class Recorder;
|
||||
|
||||
void FlushCachedTextures(VkDevice device);
|
||||
void ClearCachedTextures(VkDevice device);
|
||||
std::uint64_t ReleaseTextureCaches(std::uint64_t bytes);
|
||||
std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory);
|
||||
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported);
|
||||
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes);
|
||||
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes);
|
||||
bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::chrono::steady_clock::duration sinceReport);
|
||||
std::uint64_t StorageTextureCacheBudget(const Context& context);
|
||||
struct TextureCacheUse {
|
||||
std::size_t sampledEntries = 0;
|
||||
std::uint64_t sampledBytes = 0;
|
||||
@@ -42,6 +44,7 @@ std::uint64_t SampledTextureCacheBudget(const Context& context);
|
||||
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words);
|
||||
bool SampledTexturesShareEntry(std::span<const std::uint32_t> first, std::span<const std::uint32_t> second);
|
||||
bool CompletePendingCpuWrites(const Context& context, std::uint64_t address, std::size_t bytes);
|
||||
bool SpirvStoresThroughAddresses(std::span<const std::uint32_t> words);
|
||||
|
||||
// The cached storage image of a surface (render targets use it as their resident image); brought up
|
||||
// to date with guest memory before it is returned.
|
||||
@@ -98,6 +101,7 @@ private:
|
||||
PFN_vkDestroyDescriptorSetLayout destroyLayout;
|
||||
PFN_vkDestroyDescriptorPool destroyPool;
|
||||
PFN_vkResetDescriptorPool resetPool;
|
||||
PFN_vkFreeDescriptorSets freeDescriptorSets = nullptr;
|
||||
mutable std::mutex mutex;
|
||||
std::map<std::vector<std::uint32_t>, VkDescriptorSetLayout> layouts;
|
||||
struct Pool {
|
||||
@@ -122,6 +126,12 @@ struct ImageTableReuseCounts {
|
||||
};
|
||||
ImageTableReuseCounts ImageTableReuses();
|
||||
|
||||
struct PresyncSurface {
|
||||
std::uint64_t address;
|
||||
std::uint64_t bytes;
|
||||
bool dccKeys;
|
||||
};
|
||||
|
||||
class ShaderResources {
|
||||
public:
|
||||
ShaderResources(const Context& context, const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment, const ColorTarget& target, std::uint64_t indexAddress, std::size_t indexBytes);
|
||||
@@ -168,8 +178,16 @@ public:
|
||||
std::size_t size;
|
||||
std::vector<std::uint32_t> words;
|
||||
};
|
||||
std::shared_ptr<DrawBindings> PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved = {}) const;
|
||||
std::shared_ptr<DrawBindings> PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved = {}, bool movedOnly = false) const;
|
||||
std::optional<std::vector<MovedBuffer>> MovedReadOnlyBuffers(std::span<const CompiledShader> shaders, Recorder& recorder) const;
|
||||
enum class MoveRefusal : std::size_t { None, Shape, NotGuest, Written, Limit, Inaccessible, Overlap, Bindings, Count };
|
||||
struct MoveProof {
|
||||
MoveRefusal refusal = MoveRefusal::None;
|
||||
std::size_t buffers = 0;
|
||||
std::size_t bytes = 0;
|
||||
};
|
||||
static const char* MoveRefusalName(MoveRefusal refusal);
|
||||
std::optional<std::vector<MovedBuffer>> MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const;
|
||||
static std::size_t MaxDrawSnapshotBytes();
|
||||
void WriteBack();
|
||||
// Deferred completion: MarkGpuWrites registers the results the recorded work leaves on the GPU
|
||||
@@ -182,6 +200,7 @@ public:
|
||||
// Whether a written buffer was copied (its results reach guest memory by the CPU write-back).
|
||||
bool HasCopiedWrites() const { return guestMemory.HasCopiedWrites(); }
|
||||
bool HoldsLease() const { return guestMemory.HoldsLease(); }
|
||||
bool BdaReadOnly() const { return usesBda && bdaReadOnly; }
|
||||
bool UsesAddressTable() const { return usesBda; }
|
||||
std::size_t CopiedBytes() const { return guestMemory.CopiedBytes(); }
|
||||
bool WritesOverlap(std::uint64_t address, std::size_t bytes) const { return guestMemory.WritesOverlap(address, bytes); }
|
||||
@@ -226,6 +245,7 @@ public:
|
||||
bool Reusable() const { return reusable; }
|
||||
const std::vector<std::uint64_t>& MultisampleSources() const { return multisampleSources; }
|
||||
static bool NeverReusable(std::span<const CompiledShader> shaders);
|
||||
static std::uint64_t DepthGuardRefusals();
|
||||
const std::vector<std::shared_ptr<Texture>>& SampledTextures() const { return textures; }
|
||||
bool ChecksFaults() const { return bda != nullptr; }
|
||||
void CheckFaults() const;
|
||||
@@ -280,7 +300,7 @@ public:
|
||||
// Usable right after the deferred constructor, for address-based builds too (it decodes the
|
||||
// compiled shader's bindings itself when stage A has not run), and on a completed object from
|
||||
// the resource cache (its Revalidate repeats the same lookups), without the device lock.
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> PresyncSurfaces() const;
|
||||
std::vector<PresyncSurface> PresyncSurfaces() const;
|
||||
|
||||
private:
|
||||
friend class AgcDriver::VulkanDevice;
|
||||
@@ -338,12 +358,15 @@ private:
|
||||
std::uint32_t set = 0;
|
||||
std::size_t firstSampler = 0;
|
||||
std::size_t samplerCount = 0;
|
||||
bool sampled = false;
|
||||
bool storage = false;
|
||||
std::vector<std::size_t> elementTextures;
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> samplerRejections;
|
||||
};
|
||||
std::vector<TableStage> tableStages;
|
||||
std::array<std::size_t, 2> tableBindings{std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::size_t>::max()};
|
||||
std::array<std::uint32_t, 2> tableElements{};
|
||||
std::array<std::size_t, 2> storageTableBindings{std::numeric_limits<std::size_t>::max(), std::numeric_limits<std::size_t>::max()};
|
||||
TableStage& tableStage(const CompiledShader& shader);
|
||||
static const ShaderRecompiler::DescriptorBinding* tableImages(std::span<const CompiledShader> shaders, ShaderRecompiler::ShaderStage stage);
|
||||
std::uint32_t rejectTableElement(TableStage& table, std::span<const std::uint32_t> words, const std::string& message);
|
||||
@@ -375,6 +398,7 @@ private:
|
||||
// same way but is left out of the write set (no write-back, no pending-write note). `atomic`
|
||||
// is its bufferAtomic (see GuestBufferMemory::AddWritable).
|
||||
std::size_t addGuestBuffer(std::span<const std::uint32_t> words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read);
|
||||
std::uint64_t guestBufferBytes(std::span<const std::uint32_t> words) const;
|
||||
std::size_t addDataBuffer(std::span<const std::uint32_t> words);
|
||||
// Stage A: the layout entry of an image binding (samplers are taken at once, the sampler cache
|
||||
// locks itself); stage B looks the sampled textures and storage images up (resolveImageBinding).
|
||||
@@ -486,6 +510,7 @@ private:
|
||||
std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes();
|
||||
std::unique_ptr<BdaResources> bda;
|
||||
bool usesBda = false;
|
||||
bool bdaReadOnly = true;
|
||||
std::vector<std::uint64_t> multisampleSources;
|
||||
bool usesFaultBuffer = false;
|
||||
bool faultChecks = false;
|
||||
|
||||
@@ -186,6 +186,7 @@ public:
|
||||
void MarkDirty();
|
||||
void MarkSliceDirty(std::uint32_t slice);
|
||||
void Flush();
|
||||
bool Dirty() const;
|
||||
// Stores every pending image overlapping the range, except `except`; returns whether any was.
|
||||
// Stores into host-imported memory are recorded (not waited for): a CPU reader syncs afterwards.
|
||||
// Then the unit shadows over the range are published into their imports as `scope` says (see
|
||||
|
||||
@@ -84,6 +84,11 @@ inline constexpr TextureSwizzleEquation kTextureSwizzleEquations[] = {
|
||||
{0x109u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x00004000u, 0x00000004u, 0x04000000u, 0x00008000u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}},
|
||||
{0x109u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x00004000u, 0x00000008u, 0x08000000u, 0x00008000u, 0x00000010u}},
|
||||
{0x109u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x00004000u, 0x00000004u, 0x08000000u, 0x00008000u, 0x00000008u}},
|
||||
{0x115u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
|
||||
{0x115u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
|
||||
{0x115u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
|
||||
{0x115u, 8u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x00000002u, 0x02000000u, 0x20002020u, 0x10010004u, 0x04008010u, 0x08004008u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
|
||||
{0x115u, 16u, {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00001000u, 0x00000001u, 0x02000000u, 0x20002010u, 0x10010002u, 0x04008008u, 0x08004004u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}},
|
||||
{0x119u, 1u, {0x00000001u, 0x00000002u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000004u, 0x04000000u, 0x40004040u, 0x20020008u, 0x08010020u, 0x10008010u, 0x00000010u, 0x10000000u, 0x00010000u, 0x00000020u}},
|
||||
{0x119u, 2u, {0x00000000u, 0x00000001u, 0x01000000u, 0x00001000u, 0x02000000u, 0x00002000u, 0x00000002u, 0x04000000u, 0x40004020u, 0x20020004u, 0x08010010u, 0x10008008u, 0x00000008u, 0x10000000u, 0x00010000u, 0x00000010u}},
|
||||
{0x119u, 4u, {0x00000000u, 0x00000000u, 0x00000001u, 0x00001000u, 0x01000000u, 0x00002000u, 0x00000002u, 0x02000000u, 0x20004020u, 0x10020004u, 0x04010010u, 0x08008008u, 0x00000008u, 0x08000000u, 0x00010000u, 0x00000010u}},
|
||||
|
||||
@@ -36,10 +36,10 @@ constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
|
||||
|
||||
}
|
||||
|
||||
BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")), allocateMemory(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")), mapMemory(context.Function<PFN_vkMapMemory>("vkMapMemory")), maxSlots(std::max<std::size_t>(1, std::min<std::size_t>(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))) {
|
||||
BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")), allocateMemory(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")), mapMemory(context.Function<PFN_vkMapMemory>("vkMapMemory")), maxSlots(std::max<std::size_t>(1, std::min<std::size_t>(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim) {
|
||||
smallTier.budget = smallBudget;
|
||||
largeTier.budget = budget;
|
||||
deviceTier.budget = DeviceTierEnabled() ? DeviceBudget(context.memory) : 0;
|
||||
deviceTier.budget = deviceBudget(context);
|
||||
}
|
||||
|
||||
BufferPool::~BufferPool() {
|
||||
@@ -57,7 +57,9 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation
|
||||
const auto initialResult = result;
|
||||
unsigned attempts = 1;
|
||||
VkDeviceSize reclaimed = 0;
|
||||
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY || result == VK_ERROR_OUT_OF_HOST_MEMORY) {
|
||||
VkDeviceSize released = 0;
|
||||
const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY; };
|
||||
if (exhausted(result)) {
|
||||
reclaimed = trim();
|
||||
if (reclaimed != 0) {
|
||||
memory = VK_NULL_HANDLE;
|
||||
@@ -65,16 +67,28 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation
|
||||
++attempts;
|
||||
}
|
||||
}
|
||||
if (exhausted(result)) {
|
||||
if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize));
|
||||
if (released != 0) {
|
||||
memory = VK_NULL_HANDLE;
|
||||
result = allocateMemory(device, &allocation, nullptr, &memory);
|
||||
++attempts;
|
||||
}
|
||||
}
|
||||
if (result == VK_SUCCESS) {
|
||||
if (attempts == 2) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu initialResult=%d attempts=%u\n", operation, static_cast<unsigned long long>(allocation.allocationSize), static_cast<unsigned long long>(reclaimed), static_cast<int>(initialResult), attempts);
|
||||
if (attempts > 1) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu released=%llu initialResult=%d attempts=%u\n", operation, static_cast<unsigned long long>(allocation.allocationSize), static_cast<unsigned long long>(reclaimed), static_cast<unsigned long long>(released), static_cast<int>(initialResult), attempts);
|
||||
return memory;
|
||||
}
|
||||
char details[512];
|
||||
std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu initialResult=%d attempts=%u", operation, static_cast<unsigned long long>(allocation.allocationSize), allocation.memoryTypeIndex, static_cast<unsigned long long>(reclaimed), static_cast<int>(initialResult), attempts);
|
||||
std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu released=%llu initialResult=%d attempts=%u", operation, static_cast<unsigned long long>(allocation.allocationSize), allocation.memoryTypeIndex, static_cast<unsigned long long>(reclaimed), static_cast<unsigned long long>(released), static_cast<int>(initialResult), attempts);
|
||||
Check(result, details);
|
||||
return VK_NULL_HANDLE;
|
||||
}
|
||||
|
||||
VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) {
|
||||
return bytes + std::max<VkDeviceSize>(bytes, VkDeviceSize{256} << 20u);
|
||||
}
|
||||
|
||||
bool BufferPool::SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom) {
|
||||
if (capacity >= classLimit || size > capacity || !std::has_single_bit(capacity)) return false;
|
||||
const auto alignmentOk = alignment == 0 || (std::has_single_bit(alignment) && alignment <= capacity);
|
||||
@@ -180,18 +194,27 @@ bool BufferPool::DeviceTierEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory) {
|
||||
VkDeviceSize BufferPool::DeviceBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) {
|
||||
static const char* value = std::getenv("APS5_STAGING_POOL_MIB");
|
||||
if (value != nullptr) return static_cast<VkDeviceSize>(std::strtoull(value, nullptr, 10)) << 20u;
|
||||
VkDeviceSize largest = 0;
|
||||
bool systemMemory = false;
|
||||
std::optional<std::uint32_t> largest;
|
||||
for (std::uint32_t heap = 0; heap < memory.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) {
|
||||
if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) == 0 || memory.memoryHeaps[heap].size <= largest) continue;
|
||||
largest = memory.memoryHeaps[heap].size;
|
||||
systemMemory = systemMemoryHeap(memory, heap);
|
||||
if ((memory.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && (!largest || memory.memoryHeaps[heap].size > memory.memoryHeaps[*largest].size)) largest = heap;
|
||||
}
|
||||
if (systemMemory) return std::max<VkDeviceSize>(VkDeviceSize{128} << 20u, largest / 64u);
|
||||
return std::max<VkDeviceSize>(VkDeviceSize{512} << 20u, largest / 8u);
|
||||
VkDeviceSize size = largest ? memory.memoryHeaps[*largest].size : 0;
|
||||
if (largest && reported != nullptr && reported->heapBudget[*largest] != 0) size = std::min(size, reported->heapBudget[*largest]);
|
||||
if (largest && systemMemoryHeap(memory, *largest)) return std::max<VkDeviceSize>(VkDeviceSize{128} << 20u, size / 64u);
|
||||
return std::max<VkDeviceSize>(VkDeviceSize{512} << 20u, size / 8u);
|
||||
}
|
||||
|
||||
VkDeviceSize BufferPool::deviceBudget(const Context& context) {
|
||||
if (!DeviceTierEnabled()) return 0;
|
||||
if (context.memoryProperties2 == nullptr || context.physical == VK_NULL_HANDLE) return DeviceBudget(context.memory);
|
||||
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
|
||||
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2};
|
||||
properties.pNext = &reported;
|
||||
context.memoryProperties2(context.physical, &properties);
|
||||
return DeviceBudget(context.memory, &reported);
|
||||
}
|
||||
|
||||
VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept {
|
||||
@@ -228,7 +251,7 @@ VkBufferUsageFlags BufferPool::Usage(VkBufferUsageFlags usage, VkMemoryPropertyF
|
||||
}
|
||||
|
||||
BufferPool::Tier& BufferPool::tierFor(std::size_t capacity, VkMemoryPropertyFlags properties) {
|
||||
if (DeviceTiered(properties)) return deviceTier;
|
||||
if (DeviceTiered(properties) && (sharedTiers() || capacity >= classLimit)) return deviceTier;
|
||||
return !sharedTiers() && capacity < classLimit ? smallTier : largeTier;
|
||||
}
|
||||
|
||||
|
||||
@@ -761,13 +761,62 @@ bool PendingFillsLeaveUncompressed(const Recorder& recorder, const GuestTextureR
|
||||
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
struct KeyWaitTotals {
|
||||
std::atomic<std::uint64_t> calls{0};
|
||||
std::atomic<std::uint64_t> noPending{0};
|
||||
std::atomic<std::uint64_t> known{0};
|
||||
std::atomic<std::uint64_t> fillsUncompressed{0};
|
||||
std::atomic<std::uint64_t> synced{0};
|
||||
};
|
||||
|
||||
KeyWaitTotals& KeyWaits() {
|
||||
static KeyWaitTotals totals;
|
||||
return totals;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
DccKeyWaitCounts KeyWaitCounts() {
|
||||
const auto& totals = KeyWaits();
|
||||
return {totals.calls.load(std::memory_order_relaxed), totals.noPending.load(std::memory_order_relaxed), totals.known.load(std::memory_order_relaxed), totals.fillsUncompressed.load(std::memory_order_relaxed), totals.synced.load(std::memory_order_relaxed)};
|
||||
}
|
||||
|
||||
bool DccKeyWritersPending(std::uint64_t metaAddress, std::size_t count) {
|
||||
if (metaAddress == 0 || count == 0 || !Recorder::SnapshotWriteOverlaps(metaAddress, count)) return false;
|
||||
auto& memo = Memo();
|
||||
std::lock_guard lock(memo.mutex);
|
||||
MemoRecorder(memo);
|
||||
return std::none_of(memo.entries.begin(), memo.entries.end(), [&](const GpuKeyStore& store) { return store.note != 0 && store.begin <= metaAddress && metaAddress + count <= store.end; });
|
||||
}
|
||||
|
||||
bool DccKeyWriterNeedsSync(std::uint64_t metaAddress, std::size_t count) {
|
||||
if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return false;
|
||||
auto* recorder = Recorder::Active();
|
||||
return recorder != nullptr && recorder->PendingWriteOverlaps(metaAddress, count) && !PendingStoreKeys(*recorder, metaAddress, count).has_value();
|
||||
}
|
||||
|
||||
std::optional<DccKeys> WaitForKeyWriters(const GuestTextureResource& resource, std::uint64_t guestBytes) {
|
||||
const auto count = static_cast<std::size_t>(guestBytes / KeyBytes);
|
||||
const auto metaAddress = resource.dccAddress;
|
||||
if (metaAddress == 0 || count == 0 || !GuestMemory::GpuMutex().HeldByThisThread()) return std::nullopt;
|
||||
auto& waits = KeyWaits();
|
||||
waits.calls.fetch_add(1, std::memory_order_relaxed);
|
||||
auto* recorder = Recorder::Active();
|
||||
if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count) || PendingStoreKeys(*recorder, metaAddress, count).has_value()) return std::nullopt;
|
||||
if (PendingFillsLeaveUncompressed(*recorder, resource, count)) return DccKeys::Uncompressed;
|
||||
if (recorder == nullptr || !recorder->PendingWriteOverlaps(metaAddress, count)) {
|
||||
waits.noPending.fetch_add(1, std::memory_order_relaxed);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (PendingStoreKeys(*recorder, metaAddress, count).has_value()) {
|
||||
waits.known.fetch_add(1, std::memory_order_relaxed);
|
||||
return std::nullopt;
|
||||
}
|
||||
if (PendingFillsLeaveUncompressed(*recorder, resource, count)) {
|
||||
waits.fillsUncompressed.fetch_add(1, std::memory_order_relaxed);
|
||||
return DccKeys::Uncompressed;
|
||||
}
|
||||
waits.synced.fetch_add(1, std::memory_order_relaxed);
|
||||
Recorder::CountSync(2);
|
||||
recorder->SyncThrough(metaAddress, count);
|
||||
return std::nullopt;
|
||||
|
||||
@@ -257,7 +257,9 @@ public:
|
||||
const VkImageSubresourceRange range{aspects, 0, 1, 0, 1};
|
||||
constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, access, VK_ACCESS_TRANSFER_WRITE_BIT);
|
||||
const VkClearDepthStencilValue clear{clearDepth, clearStencil};
|
||||
const VkClearDepthStencilValue clear{depthFill.value_or(clearDepth), stencilFill.value_or(clearStencil)};
|
||||
depthFill.reset();
|
||||
stencilFill.reset();
|
||||
context.Function<PFN_vkCmdClearDepthStencilImage>("vkCmdClearDepthStencilImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &clear, 1, &range);
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, access);
|
||||
if (batch) batch->SubmitAndWait();
|
||||
@@ -322,8 +324,25 @@ public:
|
||||
return texture;
|
||||
}
|
||||
|
||||
void ForgetClear(VkImageAspectFlags aspects) {
|
||||
pendingClear &= ~aspects;
|
||||
if ((aspects & VK_IMAGE_ASPECT_DEPTH_BIT) != 0) depthFill.reset();
|
||||
if ((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) stencilFill.reset();
|
||||
}
|
||||
|
||||
void NoteHtileClear(VkImageAspectFlags aspects) {
|
||||
ForgetClear(aspects);
|
||||
pendingClear |= aspects;
|
||||
}
|
||||
|
||||
void NotePlaneFill(VkImageAspectFlagBits aspect, float depth, std::uint8_t stencil) {
|
||||
pendingClear |= aspect;
|
||||
if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) depthFill = depth;
|
||||
else stencilFill = stencil;
|
||||
}
|
||||
|
||||
void CopyAspectFrom(const DepthSurface& source, VkImageAspectFlagBits aspect) {
|
||||
pendingClear &= ~static_cast<VkImageAspectFlags>(aspect);
|
||||
ForgetClear(aspect);
|
||||
auto* recorder = RecorderFor(context);
|
||||
std::unique_ptr<CommandBatch> batch;
|
||||
if (recorder == nullptr) batch = std::make_unique<CommandBatch>(context);
|
||||
@@ -370,6 +389,8 @@ public:
|
||||
VkImageAspectFlags pendingClear = 0;
|
||||
float clearDepth = 0.0f;
|
||||
std::uint8_t clearStencil = 0;
|
||||
std::optional<float> depthFill;
|
||||
std::optional<std::uint8_t> stencilFill;
|
||||
|
||||
bool StorageViewMatches(std::span<const std::uint32_t> words, const GuestTextureResource& resource) const {
|
||||
if (resource.width != target.extent.width || resource.height != target.extent.height) return false;
|
||||
@@ -951,8 +972,13 @@ void takePlane(DepthSurface& surface, PlaneOwners& owners, std::uint64_t address
|
||||
auto& entry = owners[{surface.context.device, address}];
|
||||
auto* owner = entry.surface;
|
||||
if (owner != nullptr && owner->active->load(std::memory_order_acquire) && owner != &surface && owner->target.format == surface.target.format && owner->target.samples == surface.target.samples && owner->target.extent.width == surface.target.extent.width && owner->target.extent.height == surface.target.extent.height) {
|
||||
if ((owner->pendingClear & aspect) != 0) surface.pendingClear |= aspect;
|
||||
else surface.CopyAspectFrom(*owner, aspect);
|
||||
if ((owner->pendingClear & aspect) != 0) {
|
||||
surface.pendingClear |= aspect;
|
||||
if (aspect == VK_IMAGE_ASPECT_DEPTH_BIT) surface.depthFill = owner->depthFill;
|
||||
else surface.stencilFill = owner->stencilFill;
|
||||
} else {
|
||||
surface.CopyAspectFrom(*owner, aspect);
|
||||
}
|
||||
}
|
||||
entry = {&surface, ++planeBinds};
|
||||
}
|
||||
@@ -1222,9 +1248,9 @@ VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target)
|
||||
if (created && !inherits && target.htileAddress == 0) surface->TakeStorage();
|
||||
if (cleared && (!created || inherits)) {
|
||||
if (target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented";
|
||||
surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
}
|
||||
surface->pendingClear |= htileCleared;
|
||||
surface->NoteHtileClear(htileCleared);
|
||||
surface->ApplyFastClear();
|
||||
if (!created || inherits) surface->written = ++depthWrites;
|
||||
surface->NoteMemory();
|
||||
@@ -1316,7 +1342,7 @@ void NoteHtileDepthClear(std::uint64_t htileAddress, std::size_t bytes, std::siz
|
||||
}
|
||||
if (!HtileFillCovers(surface->target.htileAddress, surface->target.extent, htileAddress, bytes)) continue;
|
||||
if (surface->target.samples != 1u) surface->sampling.refusal = "native multisample depth fast-clear transition is not implemented";
|
||||
surface->pendingClear |= VK_IMAGE_ASPECT_DEPTH_BIT;
|
||||
surface->NoteHtileClear(VK_IMAGE_ASPECT_DEPTH_BIT);
|
||||
existing = true;
|
||||
}
|
||||
if (!existing) clearedHtiles()[htileAddress] = bytes;
|
||||
@@ -1444,6 +1470,26 @@ bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t addre
|
||||
return address + bytes - htile >= HtileMetadataBytes(extent);
|
||||
}
|
||||
|
||||
bool PlaneFillCovers(std::uint64_t plane, std::uint64_t planeBytes, std::uint64_t address, std::size_t bytes) {
|
||||
return plane != 0 && plane >= address && plane + planeBytes <= address + bytes;
|
||||
}
|
||||
|
||||
std::optional<std::uint8_t> StencilFillValue(std::uint32_t pattern) {
|
||||
const auto byte = static_cast<std::uint8_t>(pattern & 0xffu);
|
||||
if (pattern != byte * 0x01010101u) return std::nullopt;
|
||||
return byte;
|
||||
}
|
||||
|
||||
std::optional<float> DepthFillValue(std::uint32_t pattern, bool d16) {
|
||||
if (d16) {
|
||||
if ((pattern >> 16u) != (pattern & 0xffffu)) return std::nullopt;
|
||||
return static_cast<float>(pattern & 0xffffu) / 65535.0f;
|
||||
}
|
||||
const auto value = std::bit_cast<float>(pattern);
|
||||
if (!(value >= 0.0f && value <= 1.0f)) return std::nullopt;
|
||||
return value;
|
||||
}
|
||||
|
||||
void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32_t pattern) {
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
forgetHtileReads(address, bytes);
|
||||
@@ -1451,9 +1497,20 @@ void NoteDepthMetadataFill(std::uint64_t address, std::size_t bytes, std::uint32
|
||||
if (!surface->active->load(std::memory_order_acquire)) continue;
|
||||
const auto& target = surface->target;
|
||||
if (target.samples != 1u && target.htileAddress != 0u && address < target.htileAddress + HtileMetadataBytes(target.extent) && target.htileAddress < address + bytes) surface->sampling.refusal = "native multisample depth metadata fill transition is not implemented";
|
||||
if (!HtileFillCovers(target.htileAddress, target.extent, address, bytes)) continue;
|
||||
const VkImageAspectFlags written = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u);
|
||||
surface->pendingClear = (surface->pendingClear & ~written) | HtileFillClears(pattern, target.htileStencil);
|
||||
if (HtileFillCovers(target.htileAddress, target.extent, address, bytes)) {
|
||||
const VkImageAspectFlags written = VK_IMAGE_ASPECT_DEPTH_BIT | (target.htileStencil ? VK_IMAGE_ASPECT_STENCIL_BIT : 0u);
|
||||
surface->ForgetClear(written);
|
||||
surface->NoteHtileClear(HtileFillClears(pattern, target.htileStencil));
|
||||
continue;
|
||||
}
|
||||
if (target.samples != 1) continue;
|
||||
const bool d16 = target.format == VK_FORMAT_D16_UNORM || target.format == VK_FORMAT_D16_UNORM_S8_UINT;
|
||||
if (PlaneFillCovers(target.address, DepthSliceBytes(target.extent, d16 ? 2u : 4u), address, bytes)) {
|
||||
if (const auto depth = DepthFillValue(pattern, d16)) surface->NotePlaneFill(VK_IMAGE_ASPECT_DEPTH_BIT, *depth, 0);
|
||||
}
|
||||
if (PlaneFillCovers(target.stencilAddress, DepthSliceBytes(target.extent, 1u), address, bytes)) {
|
||||
if (const auto stencil = StencilFillValue(pattern)) surface->NotePlaneFill(VK_IMAGE_ASPECT_STENCIL_BIT, 0.0f, *stencil);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1545,6 +1602,11 @@ bool DepthSurfaceAt(std::uint64_t address) {
|
||||
return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && !surface->retired && (surface->target.address == address || surface->target.stencilAddress == address); });
|
||||
}
|
||||
|
||||
bool DepthSurfaceOverlaps(VkDevice device, std::uint64_t address, std::uint64_t bytes) {
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
return std::any_of(surfaces().begin(), surfaces().end(), [&](const auto& surface) { return surface->active->load(std::memory_order_acquire) && !surface->retired && surface->context.device == device && surface->Overlaps(address, bytes); });
|
||||
}
|
||||
|
||||
void RetireDepthSurfaces(VkDevice device, std::uint64_t address, std::uint64_t bytes) {
|
||||
std::lock_guard lock(surfacesMutex());
|
||||
for (const auto& surface : surfaces()) {
|
||||
|
||||
@@ -354,6 +354,9 @@ struct DrawOutcome {
|
||||
// A recorded draw began a render pass of its own, or continued the previous draw's.
|
||||
bool passBegun = false;
|
||||
bool passContinued = false;
|
||||
bool keyStoresFlushed = false;
|
||||
bool storesFlushed = false;
|
||||
bool addressDraw = false;
|
||||
SyncReason reason = SyncNone;
|
||||
// Shader validation memo (see CachedFragmentOutputs).
|
||||
bool validateMemoized = false;
|
||||
@@ -426,6 +429,7 @@ struct DrawProfile {
|
||||
// Render passes recorded draws began, and draws that continued the previous draw's pass.
|
||||
std::uint64_t passesBegun = 0;
|
||||
std::uint64_t passesContinued = 0;
|
||||
std::array<DrawPassCounts, 2> passTotals{};
|
||||
std::array<std::uint64_t, SyncCount> reasons{};
|
||||
// The CPU inside Graphics::Draw, split by outcome: a synchronous draw costs ten times a recorded
|
||||
// one, so one average would only show the synchronous population.
|
||||
@@ -496,6 +500,14 @@ void reportDraw(const std::array<double, PhaseCount>& us, const ShaderResources:
|
||||
if (outcome.completion) ++profile.completion;
|
||||
if (outcome.passBegun) ++profile.passesBegun;
|
||||
if (outcome.passContinued) ++profile.passesContinued;
|
||||
if (outcome.recorded) {
|
||||
auto& totals = profile.passTotals[outcome.addressDraw ? 1 : 0];
|
||||
++totals.draws;
|
||||
if (outcome.passBegun) ++totals.passesBegun;
|
||||
if (outcome.passContinued) ++totals.passesContinued;
|
||||
if (outcome.keyStoresFlushed) ++totals.keyStoreFlushes;
|
||||
if (outcome.storesFlushed) ++totals.storeFlushes;
|
||||
}
|
||||
++profile.reasons[outcome.reason];
|
||||
(outcome.recorded ? (outcome.waited ? profile.waitedUs : profile.recordedUs) : profile.synchronousUs) += drawUs;
|
||||
profile.targetLookups += outcome.targetLookups;
|
||||
@@ -841,6 +853,12 @@ std::array<std::uint64_t, IndirectPathCount> IndirectDrawCounts() {
|
||||
return stats.indirect;
|
||||
}
|
||||
|
||||
std::array<DrawPassCounts, 2> DrawPassTotals() {
|
||||
auto& stats = Profile();
|
||||
std::lock_guard lock(stats.mutex);
|
||||
return stats.passTotals;
|
||||
}
|
||||
|
||||
void CountDrawSkip(DrawSkip kind, double us) {
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
if (!profile) return;
|
||||
@@ -1049,6 +1067,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
|
||||
if (draw.indexed) GuestMemory::CheckRange(reinterpret_cast<const void*>(draw.indexAddress), static_cast<std::size_t>(indexBytes), draw.indexSize);
|
||||
APS5_LOG_CHARS_OUT_DEBUG("Index buffer range OK");
|
||||
Require(!draw.indexed || !state.hasColorTarget || draw.indexAddress + indexBytes <= state.color.address || state.color.address + state.color.bytes <= draw.indexAddress, "index buffer aliases the render target");
|
||||
if (draw.indexed) RequireNoMultisampleTarget(draw.indexAddress, static_cast<std::size_t>(indexBytes), "an index buffer read");
|
||||
APS5_LOG_CHARS_OUT_DEBUG("ValidateShaders");
|
||||
if (recipe != nullptr) {
|
||||
inputs.fragmentOutputs = recipe->fragmentOutputs;
|
||||
@@ -1198,6 +1217,7 @@ DrawInputs prepareDrawInputs(const Context& context, const State& state, const P
|
||||
const auto [begin, end] = plan.copies[range];
|
||||
const auto bytes = static_cast<std::size_t>(end - begin);
|
||||
GuestMemory::CheckRange(reinterpret_cast<const void*>(begin), bytes, 1);
|
||||
RequireNoMultisampleTarget(begin, bytes, "a vertex buffer read");
|
||||
if (const auto* import = InPlaceDrawInput(context, begin, bytes, plan.alignments[range], &inputs.vertexInputBehindGpuWrite)) {
|
||||
rangeHandles.push_back(import->buffer);
|
||||
rangeOffsets.push_back(begin - import->base);
|
||||
@@ -1755,9 +1775,15 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
if (!gpuIndirect) return false;
|
||||
return (begin < args->arguments + args->RangeBytes() && args->arguments < end) || (args->countIndirect && begin < args->countAddress + 4 && args->countAddress < end);
|
||||
};
|
||||
if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) recorder->FlushKeyStores();
|
||||
if (recorder->HasQueuedKeyStores() && (resources.HoldsLease() || recorder->AnyQueuedKeyStore(touches))) {
|
||||
recorder->FlushKeyStores();
|
||||
outcome.keyStoresFlushed = true;
|
||||
}
|
||||
// A queued label store over such memory likewise (Recorder::RecordStore).
|
||||
if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) recorder->FlushStores();
|
||||
if (recorder->HasQueuedStores() && (resources.HoldsLease() || recorder->AnyQueuedStore(touches))) {
|
||||
recorder->FlushStores();
|
||||
outcome.storesFlushed = true;
|
||||
}
|
||||
const auto drawBindings = resources.PrepareDrawBindings(*recorder, record.moved);
|
||||
const bool capture = CaptureInputsEnabled();
|
||||
const bool meshIndirect = state.stages.mesh && args != nullptr;
|
||||
@@ -1774,7 +1800,11 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
if (target != nullptr) passAttachments.emplace_back(target->Image(), true);
|
||||
}
|
||||
const bool unknownMemory = addressBased || resources.UsesAddressTable() || resources.HasCopiedWrites();
|
||||
const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownMemory};
|
||||
static const bool addressReadPasses = std::getenv("APS5_NO_ADDRESS_READ_PASSES") == nullptr;
|
||||
const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly();
|
||||
outcome.addressDraw = addressBased;
|
||||
const bool unknownWrites = ((addressBased || resources.UsesAddressTable()) && !readOnlyBda) || resources.HasCopiedWrites();
|
||||
const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites};
|
||||
const bool fenced = state.depth.has_value() || !record.proxies.empty();
|
||||
const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess);
|
||||
const bool continued = start.continued;
|
||||
|
||||
@@ -160,6 +160,16 @@ struct ImportFailure {
|
||||
GuestAllocations::Mapped ranges;
|
||||
};
|
||||
|
||||
struct ImportSettle {
|
||||
VkBuffer settleTarget = VK_NULL_HANDLE;
|
||||
VkDeviceMemory settleMemory = VK_NULL_HANDLE;
|
||||
VkCommandBuffer settleCommands = VK_NULL_HANDLE;
|
||||
VkFence settleFence = VK_NULL_HANDLE;
|
||||
PFN_vkDestroyFence destroyFence = nullptr;
|
||||
PFN_vkDestroyBuffer settleDestroyBuffer = nullptr;
|
||||
PFN_vkFreeMemory settleFreeMemory = nullptr;
|
||||
};
|
||||
|
||||
// Imports persist across draws, keyed by allocation base, and are dropped when their allocation leaves
|
||||
// the registered set; a dropped import is destroyed once the recorded work that may read it completed.
|
||||
struct HostImports {
|
||||
@@ -184,14 +194,7 @@ struct HostImports {
|
||||
bool unwatchImports = false;
|
||||
bool unwatchDmaBufImports = false;
|
||||
bool watchPrivateImports = false;
|
||||
VkDevice settleDevice = VK_NULL_HANDLE;
|
||||
VkBuffer settleTarget = VK_NULL_HANDLE;
|
||||
VkDeviceMemory settleMemory = VK_NULL_HANDLE;
|
||||
VkCommandBuffer settleCommands = VK_NULL_HANDLE;
|
||||
VkFence settleFence = VK_NULL_HANDLE;
|
||||
PFN_vkDestroyFence destroyFence = nullptr;
|
||||
PFN_vkDestroyBuffer settleDestroyBuffer = nullptr;
|
||||
PFN_vkFreeMemory settleFreeMemory = nullptr;
|
||||
std::map<VkDevice, ImportSettle> settles;
|
||||
std::uint64_t useCounter = 0;
|
||||
std::uint64_t buildFloor = 0;
|
||||
};
|
||||
@@ -678,51 +681,45 @@ std::atomic<std::uint64_t> settledImports{0};
|
||||
|
||||
const char* settleImport(const Context& context, HostImports& state, const HostImport& entry, VkResult& result) {
|
||||
std::unique_lock queueLock(*context.settleQueueMutex);
|
||||
if (state.settleDevice != context.device) {
|
||||
state.settleDevice = context.device;
|
||||
state.settleTarget = VK_NULL_HANDLE;
|
||||
state.settleMemory = VK_NULL_HANDLE;
|
||||
state.settleCommands = VK_NULL_HANDLE;
|
||||
state.settleFence = VK_NULL_HANDLE;
|
||||
state.destroyFence = context.Function<PFN_vkDestroyFence>("vkDestroyFence");
|
||||
state.settleDestroyBuffer = context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer");
|
||||
state.settleFreeMemory = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
|
||||
}
|
||||
if (state.settleTarget == VK_NULL_HANDLE) {
|
||||
auto& settle = state.settles[context.device];
|
||||
settle.destroyFence = context.Function<PFN_vkDestroyFence>("vkDestroyFence");
|
||||
settle.settleDestroyBuffer = context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer");
|
||||
settle.settleFreeMemory = context.Function<PFN_vkFreeMemory>("vkFreeMemory");
|
||||
if (settle.settleTarget == VK_NULL_HANDLE) {
|
||||
VkBufferCreateInfo info{VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO};
|
||||
info.size = 256;
|
||||
info.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
if ((result = context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &state.settleTarget)) != VK_SUCCESS) {
|
||||
state.settleTarget = VK_NULL_HANDLE;
|
||||
if ((result = context.Function<PFN_vkCreateBuffer>("vkCreateBuffer")(context.device, &info, nullptr, &settle.settleTarget)) != VK_SUCCESS) {
|
||||
settle.settleTarget = VK_NULL_HANDLE;
|
||||
return "vkCreateBuffer (settle)";
|
||||
}
|
||||
VkMemoryRequirements requirements{};
|
||||
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, state.settleTarget, &requirements);
|
||||
context.Function<PFN_vkGetBufferMemoryRequirements>("vkGetBufferMemoryRequirements")(context.device, settle.settleTarget, &requirements);
|
||||
VkMemoryAllocateInfo allocation{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
allocation.allocationSize = requirements.size;
|
||||
allocation.memoryTypeIndex = static_cast<std::uint32_t>(std::countr_zero(requirements.memoryTypeBits));
|
||||
if ((result = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &state.settleMemory)) != VK_SUCCESS || (result = context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, state.settleTarget, state.settleMemory, 0)) != VK_SUCCESS) {
|
||||
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, state.settleTarget, nullptr);
|
||||
if (state.settleMemory != VK_NULL_HANDLE) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, state.settleMemory, nullptr);
|
||||
state.settleTarget = VK_NULL_HANDLE;
|
||||
state.settleMemory = VK_NULL_HANDLE;
|
||||
if ((result = context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &settle.settleMemory)) != VK_SUCCESS || (result = context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, settle.settleTarget, settle.settleMemory, 0)) != VK_SUCCESS) {
|
||||
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, settle.settleTarget, nullptr);
|
||||
if (settle.settleMemory != VK_NULL_HANDLE) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, settle.settleMemory, nullptr);
|
||||
settle.settleTarget = VK_NULL_HANDLE;
|
||||
settle.settleMemory = VK_NULL_HANDLE;
|
||||
return "the settle target";
|
||||
}
|
||||
VkCommandBufferAllocateInfo allocate{VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO};
|
||||
allocate.commandPool = context.settlePool;
|
||||
allocate.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
|
||||
allocate.commandBufferCount = 1;
|
||||
if ((result = context.Function<PFN_vkAllocateCommandBuffers>("vkAllocateCommandBuffers")(context.device, &allocate, &state.settleCommands)) != VK_SUCCESS) {
|
||||
state.settleCommands = VK_NULL_HANDLE;
|
||||
if ((result = context.Function<PFN_vkAllocateCommandBuffers>("vkAllocateCommandBuffers")(context.device, &allocate, &settle.settleCommands)) != VK_SUCCESS) {
|
||||
settle.settleCommands = VK_NULL_HANDLE;
|
||||
return "vkAllocateCommandBuffers (settle)";
|
||||
}
|
||||
const VkFenceCreateInfo fenceInfo{VK_STRUCTURE_TYPE_FENCE_CREATE_INFO};
|
||||
if ((result = context.Function<PFN_vkCreateFence>("vkCreateFence")(context.device, &fenceInfo, nullptr, &state.settleFence)) != VK_SUCCESS) {
|
||||
state.settleFence = VK_NULL_HANDLE;
|
||||
if ((result = context.Function<PFN_vkCreateFence>("vkCreateFence")(context.device, &fenceInfo, nullptr, &settle.settleFence)) != VK_SUCCESS) {
|
||||
settle.settleFence = VK_NULL_HANDLE;
|
||||
return "vkCreateFence (settle)";
|
||||
}
|
||||
}
|
||||
if (state.settleCommands == VK_NULL_HANDLE || state.settleFence == VK_NULL_HANDLE) return "the settle commands";
|
||||
if (settle.settleCommands == VK_NULL_HANDLE || settle.settleFence == VK_NULL_HANDLE) return "the settle commands";
|
||||
VkBuffer source = entry.buffer;
|
||||
VkBuffer alias = VK_NULL_HANDLE;
|
||||
const auto run = [&]() -> const char* {
|
||||
@@ -738,19 +735,19 @@ const char* settleImport(const Context& context, HostImports& state, const HostI
|
||||
source = alias;
|
||||
}
|
||||
#endif
|
||||
if ((result = context.Function<PFN_vkResetFences>("vkResetFences")(context.device, 1, &state.settleFence)) != VK_SUCCESS) return "vkResetFences (settle)";
|
||||
if ((result = context.Function<PFN_vkResetCommandBuffer>("vkResetCommandBuffer")(state.settleCommands, 0)) != VK_SUCCESS) return "vkResetCommandBuffer (settle)";
|
||||
if ((result = context.Function<PFN_vkResetFences>("vkResetFences")(context.device, 1, &settle.settleFence)) != VK_SUCCESS) return "vkResetFences (settle)";
|
||||
if ((result = context.Function<PFN_vkResetCommandBuffer>("vkResetCommandBuffer")(settle.settleCommands, 0)) != VK_SUCCESS) return "vkResetCommandBuffer (settle)";
|
||||
VkCommandBufferBeginInfo begin{VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO};
|
||||
begin.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
|
||||
if ((result = context.Function<PFN_vkBeginCommandBuffer>("vkBeginCommandBuffer")(state.settleCommands, &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer (settle)";
|
||||
if ((result = context.Function<PFN_vkBeginCommandBuffer>("vkBeginCommandBuffer")(settle.settleCommands, &begin)) != VK_SUCCESS) return "vkBeginCommandBuffer (settle)";
|
||||
const VkBufferCopy region{0, 0, 4};
|
||||
context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(state.settleCommands, source, state.settleTarget, 1, ®ion);
|
||||
if ((result = context.Function<PFN_vkEndCommandBuffer>("vkEndCommandBuffer")(state.settleCommands)) != VK_SUCCESS) return "vkEndCommandBuffer (settle)";
|
||||
context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(settle.settleCommands, source, settle.settleTarget, 1, ®ion);
|
||||
if ((result = context.Function<PFN_vkEndCommandBuffer>("vkEndCommandBuffer")(settle.settleCommands)) != VK_SUCCESS) return "vkEndCommandBuffer (settle)";
|
||||
VkSubmitInfo submit{VK_STRUCTURE_TYPE_SUBMIT_INFO};
|
||||
submit.commandBufferCount = 1;
|
||||
submit.pCommandBuffers = &state.settleCommands;
|
||||
if ((result = context.Function<PFN_vkQueueSubmit>("vkQueueSubmit")(context.settleQueue, 1, &submit, state.settleFence)) != VK_SUCCESS) return "vkQueueSubmit (settle)";
|
||||
if ((result = context.Function<PFN_vkWaitForFences>("vkWaitForFences")(context.device, 1, &state.settleFence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences (settle)";
|
||||
submit.pCommandBuffers = &settle.settleCommands;
|
||||
if ((result = context.Function<PFN_vkQueueSubmit>("vkQueueSubmit")(context.settleQueue, 1, &submit, settle.settleFence)) != VK_SUCCESS) return "vkQueueSubmit (settle)";
|
||||
if ((result = context.Function<PFN_vkWaitForFences>("vkWaitForFences")(context.device, 1, &settle.settleFence, VK_TRUE, 10'000'000'000ull)) != VK_SUCCESS) return "vkWaitForFences (settle)";
|
||||
return nullptr;
|
||||
};
|
||||
const char* step = run();
|
||||
@@ -1025,6 +1022,7 @@ ImportRun windowRun(const HostImports& state, ImportRun run, std::uint64_t begin
|
||||
return run;
|
||||
}
|
||||
|
||||
const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end);
|
||||
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end);
|
||||
bool importsStale(const Context& context, const HostImports& state);
|
||||
const HostImport* importRunAt(const Context& context, HostImports& state, ImportRun run, bool joined, std::uint64_t address, const GuestAllocations::Lease& lease, std::uint64_t end, bool evict);
|
||||
@@ -1057,8 +1055,8 @@ const HostImport* importAllocation(const Context& context, HostImports& state, s
|
||||
}
|
||||
|
||||
const HostImport* importJoined(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
|
||||
if (const auto* low = findImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base;
|
||||
if (const auto* high = findImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes;
|
||||
if (const auto* low = peekImport(state, begin, begin + 1); low != nullptr && low->joined) begin = low->base;
|
||||
if (const auto* high = peekImport(state, end - 1, end); high != nullptr && high->joined) end = high->base + high->bytes;
|
||||
auto run = joinedRun(lease, begin, end);
|
||||
if (run.ranges.empty()) return nullptr;
|
||||
if (const auto failed = state.failedJoins.find(run.base); failed != state.failedJoins.end() && sameRun(failed->second, run)) return nullptr;
|
||||
@@ -1158,6 +1156,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
|
||||
state.unaliasedSpans.insert(base);
|
||||
return importAllocation(context, state, address, lease, end, evict);
|
||||
}
|
||||
if (entry.alias == nullptr) return nullptr;
|
||||
}
|
||||
#else
|
||||
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(base), static_cast<std::size_t>(bytes), true)) {
|
||||
@@ -1308,14 +1307,18 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
|
||||
for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it);
|
||||
}
|
||||
|
||||
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) {
|
||||
const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end) {
|
||||
auto found = state.imports.upper_bound(begin);
|
||||
if (found == state.imports.begin()) return nullptr;
|
||||
--found;
|
||||
const auto& entry = found->second;
|
||||
if (begin < entry.base || end > entry.base + entry.bytes) return nullptr;
|
||||
entry.lastUse = ++state.useCounter;
|
||||
return &entry;
|
||||
return begin < entry.base || end > entry.base + entry.bytes ? nullptr : &entry;
|
||||
}
|
||||
|
||||
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end) {
|
||||
const auto* entry = peekImport(state, begin, end);
|
||||
if (entry != nullptr) entry->lastUse = ++state.useCounter;
|
||||
return entry;
|
||||
}
|
||||
|
||||
const HostImport* importAccess(const Context& context, HostImports& state, std::uint64_t begin, std::uint64_t end, const GuestAllocations::Lease& lease) {
|
||||
@@ -2331,15 +2334,12 @@ void ClearHostImports(VkDevice device) {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
state.parked.erase(device);
|
||||
if (state.settleDevice == device) {
|
||||
if (state.settleFence != VK_NULL_HANDLE) state.destroyFence(device, state.settleFence, nullptr);
|
||||
if (state.settleTarget != VK_NULL_HANDLE) state.settleDestroyBuffer(device, state.settleTarget, nullptr);
|
||||
if (state.settleMemory != VK_NULL_HANDLE) state.settleFreeMemory(device, state.settleMemory, nullptr);
|
||||
state.settleDevice = VK_NULL_HANDLE;
|
||||
state.settleTarget = VK_NULL_HANDLE;
|
||||
state.settleMemory = VK_NULL_HANDLE;
|
||||
state.settleCommands = VK_NULL_HANDLE;
|
||||
state.settleFence = VK_NULL_HANDLE;
|
||||
if (const auto found = state.settles.find(device); found != state.settles.end()) {
|
||||
const auto& settle = found->second;
|
||||
if (settle.settleFence != VK_NULL_HANDLE) settle.destroyFence(device, settle.settleFence, nullptr);
|
||||
if (settle.settleTarget != VK_NULL_HANDLE) settle.settleDestroyBuffer(device, settle.settleTarget, nullptr);
|
||||
if (settle.settleMemory != VK_NULL_HANDLE) settle.settleFreeMemory(device, settle.settleMemory, nullptr);
|
||||
state.settles.erase(found);
|
||||
}
|
||||
if (state.device != device) return;
|
||||
for (const auto& [address, entry] : state.imports) {
|
||||
@@ -2680,7 +2680,7 @@ bool HostImportCovers(const Context& context, std::uint64_t address, std::size_t
|
||||
std::lock_guard lock(state.mutex);
|
||||
// Imports of a replaced device are not this device's; a stale registry only means an import
|
||||
// may be dropped at the next reconcile, which the binding path handles.
|
||||
return state.device == context.device && findImport(state, address, address + bytes) != nullptr;
|
||||
return state.device == context.device && peekImport(state, address, address + bytes) != nullptr;
|
||||
}
|
||||
|
||||
GuestBufferMemory::GuestBufferMemory(const Context& context) : context(context) {}
|
||||
@@ -3743,7 +3743,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
|
||||
const auto& lease = importRanges();
|
||||
const auto index = leasedIndexContaining(lease, region.begin);
|
||||
if (entry == nullptr && index != lease.size() && lease[index]->address == region.begin) {
|
||||
const auto* low = findImport(state, region.begin, region.begin + 1);
|
||||
const auto* low = peekImport(state, region.begin, region.begin + 1);
|
||||
const auto joinBase = low != nullptr && low->joined ? low->base : region.begin;
|
||||
const auto joins = state.sparseRunJoins.find(joinBase);
|
||||
const auto made = joins == state.sparseRunJoins.end() ? 0u : joins->second;
|
||||
@@ -3851,7 +3851,7 @@ void GuestBufferMemory::copyRegion(Region& region, bool addressable) {
|
||||
if (profile && context.hostImportAlignment != 0) {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
inImport = findImport(state, region.begin, region.end) != nullptr;
|
||||
inImport = peekImport(state, region.begin, region.end) != nullptr;
|
||||
}
|
||||
const auto allocateStart = std::chrono::steady_clock::now();
|
||||
if (region.sparse && region.buffer == nullptr && (region.hostBacked || region.writable)) {
|
||||
|
||||
@@ -368,7 +368,7 @@ private:
|
||||
for (const auto& range : dccRanges) {
|
||||
Require(GuestMemory::Watched(range.address, range.bytes), "multisampled DCC target reuse requires watched guest memory");
|
||||
GuestMemory::CollectWritesUncached(range.address, range.bytes);
|
||||
Require(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented");
|
||||
Require(GuestMemory::UnchangedSince(range.address, range.bytes, range.generation) || !GuestMemory::StoredOver(range.address, range.bytes, range.generation), "writes to a resident multisampled DCC target or its metadata are not implemented");
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr) Require(!recorder->PendingWriteOverlaps(range.address, range.bytes), "pending writes to a resident multisampled DCC target or its metadata are not implemented");
|
||||
}
|
||||
}
|
||||
@@ -707,6 +707,25 @@ VkImageView MultisampleTargetView(const Context& context, const ColorTarget& tar
|
||||
return acquire(context, target).view;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool redBlueSwapped(VkFormat rendered, VkFormat sampled) {
|
||||
const auto pair = [&](VkFormat a, VkFormat b) { return (rendered == a && sampled == b) || (rendered == b && sampled == a); };
|
||||
return pair(VK_FORMAT_B8G8R8A8_UNORM, VK_FORMAT_R8G8B8A8_UNORM) || pair(VK_FORMAT_B8G8R8A8_SRGB, VK_FORMAT_R8G8B8A8_SRGB) || pair(VK_FORMAT_B8G8R8A8_SNORM, VK_FORMAT_R8G8B8A8_SNORM) || pair(VK_FORMAT_B8G8R8A8_UINT, VK_FORMAT_R8G8B8A8_UINT) || pair(VK_FORMAT_B8G8R8A8_SINT, VK_FORMAT_R8G8B8A8_SINT);
|
||||
}
|
||||
|
||||
VkComponentMapping swapRedBlue(VkComponentMapping components) {
|
||||
const auto swapped = [](VkComponentSwizzle swizzle, VkComponentSwizzle identity) {
|
||||
if (swizzle == VK_COMPONENT_SWIZZLE_IDENTITY) swizzle = identity;
|
||||
if (swizzle == VK_COMPONENT_SWIZZLE_R) return VK_COMPONENT_SWIZZLE_B;
|
||||
if (swizzle == VK_COMPONENT_SWIZZLE_B) return VK_COMPONENT_SWIZZLE_R;
|
||||
return swizzle;
|
||||
};
|
||||
return {swapped(components.r, VK_COMPONENT_SWIZZLE_R), swapped(components.g, VK_COMPONENT_SWIZZLE_G), swapped(components.b, VK_COMPONENT_SWIZZLE_B), swapped(components.a, VK_COMPONENT_SWIZZLE_A)};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
const auto format = ResolveTextureFormat(resource.format);
|
||||
@@ -715,7 +734,9 @@ std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const
|
||||
if (!target->active->load(std::memory_order_acquire)) continue;
|
||||
if (target->context.device != context.device || target->target.surfaceAddress != resource.baseAddress) continue;
|
||||
const auto& rendered = target->target;
|
||||
if (rendered.format == format && rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0) return target->Sampled(components);
|
||||
const bool sameShape = rendered.extent.width == resource.width && rendered.extent.height == resource.height && rendered.samples == resource.samples && rendered.mip == 0;
|
||||
if (sameShape && rendered.format == format) return target->Sampled(components);
|
||||
if (sameShape && redBlueSwapped(rendered.format, format)) return target->Sampled(swapRedBlue(components));
|
||||
other = target.get();
|
||||
}
|
||||
if (other == nullptr) {
|
||||
|
||||
@@ -5,14 +5,21 @@
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
namespace {
|
||||
|
||||
std::atomic<unsigned> runningHelpers{0};
|
||||
|
||||
class ComparePool {
|
||||
public:
|
||||
explicit ComparePool(unsigned count) {
|
||||
for (unsigned i = 0; i < count; ++i) std::thread([this] { work(); }).detach();
|
||||
stopping = count == 0;
|
||||
for (unsigned i = 0; i < count; ++i) {
|
||||
workers.emplace_back([this] { work(); });
|
||||
runningHelpers.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
}
|
||||
|
||||
bool Run(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal) {
|
||||
@@ -20,6 +27,7 @@ public:
|
||||
if (!caller.owns_lock()) return false;
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
if (stopping) return false;
|
||||
finished.wait(lock, [&] { return working == 0; });
|
||||
jobSpans = spans;
|
||||
jobEqual = equal;
|
||||
@@ -36,6 +44,21 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
std::vector<std::thread> joined;
|
||||
{
|
||||
std::lock_guard caller(callers);
|
||||
std::lock_guard lock(mutex);
|
||||
stopping = true;
|
||||
joined.swap(workers);
|
||||
}
|
||||
wake.notify_all();
|
||||
for (auto& worker : joined) {
|
||||
if (worker.get_id() == std::this_thread::get_id()) worker.detach();
|
||||
else worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
void drain(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal) {
|
||||
std::size_t compared = 0;
|
||||
@@ -51,7 +74,8 @@ private:
|
||||
std::uint64_t seen = 0;
|
||||
std::unique_lock lock(mutex);
|
||||
for (;;) {
|
||||
wake.wait(lock, [&] { return generation != seen; });
|
||||
wake.wait(lock, [&] { return generation != seen || stopping; });
|
||||
if (stopping) break;
|
||||
seen = generation;
|
||||
const auto spans = jobSpans;
|
||||
const auto equal = jobEqual;
|
||||
@@ -62,6 +86,7 @@ private:
|
||||
--working;
|
||||
if (working == 0) finished.notify_all();
|
||||
}
|
||||
runningHelpers.fetch_sub(1, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
std::mutex callers;
|
||||
@@ -74,10 +99,32 @@ private:
|
||||
std::span<std::uint8_t> jobEqual;
|
||||
std::atomic<std::size_t> next{0};
|
||||
std::atomic<std::size_t> done{0};
|
||||
std::vector<std::thread> workers;
|
||||
bool stopping = false;
|
||||
};
|
||||
|
||||
std::mutex& poolLifecycle() {
|
||||
static std::mutex lifecycle;
|
||||
return lifecycle;
|
||||
}
|
||||
|
||||
bool& poolStopped() {
|
||||
static bool stopped = false;
|
||||
return stopped;
|
||||
}
|
||||
|
||||
std::atomic<ComparePool*>& createdPool() {
|
||||
static std::atomic<ComparePool*> created{nullptr};
|
||||
return created;
|
||||
}
|
||||
|
||||
ComparePool* sharedPool() {
|
||||
static ComparePool* const pool = new ComparePool(CompareHelpers());
|
||||
static ComparePool* const pool = [] {
|
||||
std::lock_guard lifecycle(poolLifecycle());
|
||||
auto* created = new ComparePool(poolStopped() ? 0u : CompareHelpers());
|
||||
createdPool().store(created, std::memory_order_release);
|
||||
return created;
|
||||
}();
|
||||
return pool;
|
||||
}
|
||||
|
||||
@@ -97,6 +144,20 @@ void CompareSpansFrom(std::span<const CompareSpan> spans, std::span<std::uint8_t
|
||||
for (std::size_t index = 0; index < spans.size(); ++index) equal[index] = std::memcmp(spans[index].first, spans[index].second, spans[index].bytes) == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
void StopCompareHelpers() {
|
||||
ComparePool* pool = nullptr;
|
||||
{
|
||||
std::lock_guard lifecycle(poolLifecycle());
|
||||
poolStopped() = true;
|
||||
pool = createdPool().load(std::memory_order_acquire);
|
||||
}
|
||||
if (pool != nullptr) pool->Stop();
|
||||
}
|
||||
|
||||
unsigned CompareHelpersRunning() {
|
||||
return runningHelpers.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
void CompareSpans(std::span<const CompareSpan> spans, std::span<std::uint8_t> equal) {
|
||||
CompareSpansFrom(spans, equal, std::size_t{1} << 20u);
|
||||
}
|
||||
|
||||
@@ -98,6 +98,10 @@ void ValidateDepthBounds(const Context& context, const State& state) {
|
||||
Require(!state.depthBoundsTest || context.depthRangeUnrestricted || (state.minDepthBounds >= 0.0f && state.minDepthBounds <= 1.0f && state.maxDepthBounds >= 0.0f && state.maxDepthBounds <= 1.0f), "depth bounds outside [0, 1] require VK_EXT_depth_range_unrestricted");
|
||||
}
|
||||
|
||||
bool BarycentricPatches(const State& state, std::span<const CompiledShader> shaders) {
|
||||
return !state.rectList && state.stages.path == ShaderPath::Vertex && std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::TessellationControl; });
|
||||
}
|
||||
|
||||
void ValidateProvokingVertex(const Context& context, const State& state, std::span<const CompiledShader> shaders) {
|
||||
Require(state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT || state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT, "invalid provoking vertex mode");
|
||||
if (state.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT) {
|
||||
@@ -108,8 +112,8 @@ void ValidateProvokingVertex(const Context& context, const State& state, std::sp
|
||||
return;
|
||||
}
|
||||
Require(context.provokingVertexLast, "last provoking vertex requires VK_EXT_provoking_vertex with provokingVertexLast enabled");
|
||||
Require(!state.rectList && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines");
|
||||
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology");
|
||||
Require(!BarycentricPatches(state, shaders) && state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "last provoking vertex is unsupported for generated primitive pipelines");
|
||||
Require(state.rectList || state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "last provoking vertex is unsupported for this primitive topology");
|
||||
for (const auto& shader : shaders) {
|
||||
if (shader.stage != ShaderRecompiler::ShaderStage::Fragment) continue;
|
||||
Require(shader.program != nullptr, "missing compiled fragment shader");
|
||||
@@ -194,7 +198,7 @@ void appendKey(std::vector<std::byte>& key, const TValue& value) {
|
||||
|
||||
bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State& state, const VertexInputLayout& input, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders) {
|
||||
using Stage = ShaderRecompiler::ShaderStage;
|
||||
if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || SplitsAlternateAlpha(state)) return false;
|
||||
if (!context.graphicsPipelineLibrary || context.pipelineExecutableInfo || state.depthBiasPerFace || state.rectList || BarycentricPatches(state, shaders) || SplitsAlternateAlpha(state)) return false;
|
||||
keys = {};
|
||||
for (const auto& shader : shaders) {
|
||||
const auto identity = shader.program->Identity();
|
||||
@@ -321,6 +325,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
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");
|
||||
ValidateProvokingVertex(context, state, shaders);
|
||||
if (state.rectList) Require(context.tessellationShader && context.limits.maxTessellationPatchSize >= 4, "rect-list requires tessellation with four output control points");
|
||||
const bool barycentricPatches = BarycentricPatches(state, shaders);
|
||||
if (barycentricPatches) Require(context.tessellationShader && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !state.primitiveRestart, "emulated barycentrics through tessellation require a triangle list without primitive restart");
|
||||
if (std::any_of(shaders.begin(), shaders.end(), [](const CompiledShader& shader) { return shader.stage == ShaderRecompiler::ShaderStage::Geometry; })) Require(context.geometryShader, "device does not support geometry shaders");
|
||||
if (state.stages.tessellation) {
|
||||
Require(context.tessellationShader, "device does not support tessellation shaders");
|
||||
@@ -436,7 +442,7 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
input.vertexAttributeDescriptionCount = static_cast<std::uint32_t>(vertexInput.attributes.size());
|
||||
input.pVertexAttributeDescriptions = vertexInput.attributes.data();
|
||||
VkPipelineInputAssemblyStateCreateInfo assembly{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO};
|
||||
assembly.topology = state.topology;
|
||||
assembly.topology = BarycentricPatches(state, shaders) ? VK_PRIMITIVE_TOPOLOGY_PATCH_LIST : state.topology;
|
||||
const bool listTopology = state.topology == VK_PRIMITIVE_TOPOLOGY_POINT_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_LINE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
assembly.primitiveRestartEnable = state.primitiveRestart && (!listTopology || context.primitiveListRestart) ? VK_TRUE : VK_FALSE;
|
||||
// Viewport and scissor are set per draw (Begin), so they do not multiply pipelines; the depth
|
||||
@@ -523,8 +529,8 @@ Pipeline::Pipeline(const Context& context, const State& state, const VertexInput
|
||||
pipelineInfo.stageCount = static_cast<std::uint32_t>(stages.size());
|
||||
pipelineInfo.pStages = stages.data();
|
||||
VkPipelineTessellationStateCreateInfo tessellation{VK_STRUCTURE_TYPE_PIPELINE_TESSELLATION_STATE_CREATE_INFO};
|
||||
if (state.rectList || state.stages.tessellation) {
|
||||
tessellation.patchControlPoints = state.rectList ? 3u : state.stages.tessellation->inputControlPoints;
|
||||
if (state.rectList || state.stages.tessellation || barycentricPatches) {
|
||||
tessellation.patchControlPoints = state.stages.tessellation ? state.stages.tessellation->inputControlPoints : 3u;
|
||||
pipelineInfo.pTessellationState = &tessellation;
|
||||
}
|
||||
pipelineInfo.pVertexInputState = state.stages.mesh ? nullptr : &input;
|
||||
@@ -926,6 +932,7 @@ struct PipelineStore {
|
||||
// The device's buffer pool at insertion: it is made and reset with the device, so it tells
|
||||
// the device instance apart from a later one the loader gave the same handle value.
|
||||
std::weak_ptr<BufferPool> pool;
|
||||
bool tracksPool;
|
||||
std::uint64_t hash;
|
||||
std::vector<std::byte> key;
|
||||
std::shared_ptr<Pipeline> pipeline;
|
||||
@@ -951,8 +958,12 @@ PipelineStore& Pipelines() {
|
||||
|
||||
// Whether the entry's objects belong to the device the context names. A context without a buffer
|
||||
// pool (tests) is identified by the handle alone.
|
||||
bool alive(const PipelineStore::Entry& entry, const Context& context) {
|
||||
bool alive(PipelineStore::Entry& entry, const Context& context) {
|
||||
if (entry.device != context.device) return false;
|
||||
if (!entry.tracksPool && context.bufferPool != nullptr) {
|
||||
entry.pool = context.bufferPool;
|
||||
entry.tracksPool = true;
|
||||
}
|
||||
return context.bufferPool == nullptr || entry.pool.lock() == context.bufferPool;
|
||||
}
|
||||
|
||||
@@ -1063,10 +1074,10 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
|
||||
// the title's own pipelines evict it first, and a full store takes none.
|
||||
if (replaying && store.entries.size() >= bound) return pipeline;
|
||||
if (replaying) {
|
||||
store.entries.push_front({context.device, context.bufferPool, hash, std::vector<std::byte>(key), pipeline});
|
||||
store.entries.push_front({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector<std::byte>(key), pipeline});
|
||||
store.index[hash] = store.entries.begin();
|
||||
} else {
|
||||
store.entries.push_back({context.device, context.bufferPool, hash, std::vector<std::byte>(key), pipeline});
|
||||
store.entries.push_back({context.device, context.bufferPool, context.bufferPool != nullptr, hash, std::vector<std::byte>(key), pipeline});
|
||||
store.index[hash] = std::prev(store.entries.end());
|
||||
}
|
||||
while (store.entries.size() > bound) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineJournal.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <cstdlib>
|
||||
#include <deque>
|
||||
@@ -54,9 +55,12 @@ struct LibraryStore {
|
||||
std::deque<std::pair<VkDevice, std::function<void()>>> jobs;
|
||||
std::map<VkDevice, std::size_t> pending;
|
||||
std::map<VkDevice, std::size_t> activeBuilds;
|
||||
bool started = false;
|
||||
std::thread worker;
|
||||
bool stopping = false;
|
||||
};
|
||||
|
||||
std::atomic<unsigned> runningWorkers{0};
|
||||
|
||||
LibraryStore& Libraries() {
|
||||
static auto* store = new LibraryStore();
|
||||
return *store;
|
||||
@@ -65,7 +69,8 @@ LibraryStore& Libraries() {
|
||||
void runJobs(LibraryStore& store) {
|
||||
std::unique_lock lock(store.mutex);
|
||||
for (;;) {
|
||||
store.work.wait(lock, [&] { return !store.jobs.empty(); });
|
||||
store.work.wait(lock, [&] { return !store.jobs.empty() || store.stopping; });
|
||||
if (store.stopping) break;
|
||||
auto [device, job] = std::move(store.jobs.front());
|
||||
store.jobs.pop_front();
|
||||
lock.unlock();
|
||||
@@ -73,15 +78,18 @@ void runJobs(LibraryStore& store) {
|
||||
lock.lock();
|
||||
if (--store.pending[device] == 0) store.idle.notify_all();
|
||||
}
|
||||
runningWorkers.fetch_sub(1, std::memory_order_acq_rel);
|
||||
}
|
||||
|
||||
void enqueue(LibraryStore& store, VkDevice device, std::function<void()> job) {
|
||||
if (!store.started) {
|
||||
std::thread(runJobs, std::ref(store)).detach();
|
||||
store.started = true;
|
||||
if (store.stopping) return;
|
||||
if (!store.worker.joinable()) {
|
||||
store.worker = std::thread(runJobs, std::ref(store));
|
||||
runningWorkers.fetch_add(1, std::memory_order_acq_rel);
|
||||
}
|
||||
++store.pending[device];
|
||||
auto& pending = store.pending[device];
|
||||
store.jobs.emplace_back(device, std::move(job));
|
||||
++pending;
|
||||
store.work.notify_one();
|
||||
}
|
||||
|
||||
@@ -335,6 +343,27 @@ void ClearPipelineLibraries(VkDevice device) {
|
||||
store.devices.erase(found);
|
||||
}
|
||||
|
||||
void StopPipelineLibraryWorker() {
|
||||
auto& store = Libraries();
|
||||
std::thread worker;
|
||||
{
|
||||
std::lock_guard lock(store.mutex);
|
||||
store.stopping = true;
|
||||
for (const auto& [device, job] : store.jobs) --store.pending[device];
|
||||
store.jobs.clear();
|
||||
worker = std::move(store.worker);
|
||||
}
|
||||
store.idle.notify_all();
|
||||
store.work.notify_all();
|
||||
if (!worker.joinable()) return;
|
||||
if (worker.get_id() == std::this_thread::get_id()) worker.detach();
|
||||
else worker.join();
|
||||
}
|
||||
|
||||
unsigned PipelineLibraryWorkers() {
|
||||
return runningWorkers.load(std::memory_order_acquire);
|
||||
}
|
||||
|
||||
PipelineLibraryCounters PipelineLibraryCountersOf(VkDevice device) {
|
||||
auto& store = Libraries();
|
||||
std::lock_guard lock(store.mutex);
|
||||
|
||||
@@ -151,7 +151,10 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa
|
||||
memory = allocation->memory;
|
||||
allocationBytes = allocation->allocationBytes;
|
||||
capacity = allocation->bytes;
|
||||
if (allocation->bufferBytes == size) return;
|
||||
if (allocation->bufferBytes == size) {
|
||||
ready = true;
|
||||
return;
|
||||
}
|
||||
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, buffer, nullptr);
|
||||
buffer = VK_NULL_HANDLE;
|
||||
try {
|
||||
@@ -160,6 +163,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa
|
||||
release();
|
||||
throw;
|
||||
}
|
||||
ready = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -177,6 +181,7 @@ DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsa
|
||||
hostPlaced = allocation.memoryTypeIndex != context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
memory = cache->AllocateMemory(allocation, "vkAllocateMemory device buffer");
|
||||
Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, memory, 0), "vkBindBufferMemory device");
|
||||
ready = true;
|
||||
} catch (...) {
|
||||
release();
|
||||
throw;
|
||||
@@ -188,7 +193,7 @@ DeviceBuffer::~DeviceBuffer() {
|
||||
}
|
||||
|
||||
void DeviceBuffer::release() noexcept {
|
||||
if (buffer && memory && cache && !hostPlaced) {
|
||||
if (ready && buffer && memory && cache && !hostPlaced) {
|
||||
cache->Put({buffer, memory, nullptr, 0, allocationBytes, capacity, usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 0, false, size});
|
||||
return;
|
||||
}
|
||||
@@ -373,9 +378,9 @@ void CommandBatch::Submit() {
|
||||
submission.pCommandBuffers = &commands;
|
||||
timing.Mark("command_end");
|
||||
Check(context.Function<PFN_vkQueueSubmit>("vkQueueSubmit")(context.queue, 1, &submission, fence), "vkQueueSubmit graphics");
|
||||
timing.Mark("queue_submit");
|
||||
pending = true;
|
||||
submitted = true;
|
||||
timing.Mark("queue_submit");
|
||||
}
|
||||
|
||||
void CommandBatch::Wait() {
|
||||
|
||||
@@ -1,10 +1,119 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint32_t pointerOperandStored(std::uint32_t opcode, std::span<const std::uint32_t> instruction) {
|
||||
switch (static_cast<spv::Op>(opcode)) {
|
||||
case spv::OpStore:
|
||||
case spv::OpCopyMemory:
|
||||
case spv::OpCopyMemorySized:
|
||||
case spv::OpAtomicStore:
|
||||
case spv::OpAtomicFlagClear:
|
||||
case spv::OpCooperativeMatrixStoreKHR:
|
||||
case spv::OpCooperativeMatrixStoreNV:
|
||||
case spv::OpCooperativeMatrixStoreTensorNV:
|
||||
return instruction.size() > 1 ? instruction[1] : 0u;
|
||||
case spv::OpAtomicExchange:
|
||||
case spv::OpAtomicCompareExchange:
|
||||
case spv::OpAtomicCompareExchangeWeak:
|
||||
case spv::OpAtomicIIncrement:
|
||||
case spv::OpAtomicIDecrement:
|
||||
case spv::OpAtomicIAdd:
|
||||
case spv::OpAtomicISub:
|
||||
case spv::OpAtomicSMin:
|
||||
case spv::OpAtomicUMin:
|
||||
case spv::OpAtomicSMax:
|
||||
case spv::OpAtomicUMax:
|
||||
case spv::OpAtomicAnd:
|
||||
case spv::OpAtomicOr:
|
||||
case spv::OpAtomicXor:
|
||||
case spv::OpAtomicFlagTestAndSet:
|
||||
case spv::OpAtomicFMinEXT:
|
||||
case spv::OpAtomicFMaxEXT:
|
||||
case spv::OpAtomicFAddEXT:
|
||||
return instruction.size() > 3 ? instruction[3] : 0u;
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
bool definesNoPointerValue(std::uint32_t opcode) {
|
||||
switch (static_cast<spv::Op>(opcode)) {
|
||||
case spv::OpTypePointer:
|
||||
case spv::OpTypeUntypedPointerKHR:
|
||||
case spv::OpTypeForwardPointer:
|
||||
case spv::OpName:
|
||||
case spv::OpMemberName:
|
||||
case spv::OpDecorate:
|
||||
case spv::OpMemberDecorate:
|
||||
case spv::OpDecorateId:
|
||||
case spv::OpDecorateString:
|
||||
case spv::OpMemberDecorateString:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
template<typename TVisit>
|
||||
bool forEachInstruction(std::span<const std::uint32_t> words, const TVisit& visit) {
|
||||
for (std::size_t at = 5; at < words.size();) {
|
||||
const auto count = words[at] >> 16u;
|
||||
if (count == 0 || at + count > words.size()) return false;
|
||||
visit(words[at] & 0xffffu, words.subspan(at, count));
|
||||
at += count;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool storesThroughAddressesCached(const ShaderRecompiler::SharedSpirv& spirv) {
|
||||
static std::mutex mutex;
|
||||
static std::map<std::pair<std::uint64_t, std::uint64_t>, bool> known;
|
||||
const auto hash = spirv.ContentHash();
|
||||
const std::pair key{hash.low, hash.high};
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (const auto found = known.find(key); found != known.end()) return found->second;
|
||||
}
|
||||
const bool stores = SpirvStoresThroughAddresses(spirv.Words());
|
||||
std::lock_guard lock(mutex);
|
||||
known.emplace(key, stores);
|
||||
return stores;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
bool SpirvStoresThroughAddresses(std::span<const std::uint32_t> words) {
|
||||
if (words.size() < 5 || words[0] != spv::MagicNumber) return true;
|
||||
std::unordered_set<std::uint32_t> pointerTypes;
|
||||
const bool readable = forEachInstruction(words, [&](std::uint32_t opcode, std::span<const std::uint32_t> instruction) {
|
||||
if ((opcode == spv::OpTypePointer || opcode == spv::OpTypeUntypedPointerKHR) && instruction.size() > 2 && instruction[2] == spv::StorageClassPhysicalStorageBuffer) pointerTypes.insert(instruction[1]);
|
||||
});
|
||||
if (!readable) return true;
|
||||
if (pointerTypes.empty()) return false;
|
||||
std::unordered_set<std::uint32_t> pointers;
|
||||
forEachInstruction(words, [&](std::uint32_t opcode, std::span<const std::uint32_t> instruction) {
|
||||
if (instruction.size() > 2 && !definesNoPointerValue(opcode) && pointerTypes.contains(instruction[1])) pointers.insert(instruction[2]);
|
||||
});
|
||||
bool stores = false;
|
||||
forEachInstruction(words, [&](std::uint32_t opcode, std::span<const std::uint32_t> instruction) {
|
||||
const auto pointer = pointerOperandStored(opcode, instruction);
|
||||
const bool extendedWithPointer = opcode == spv::OpExtInst && std::any_of(instruction.begin() + static_cast<std::ptrdiff_t>(std::min<std::size_t>(5u, instruction.size())), instruction.end(), [&](std::uint32_t operand) { return pointers.contains(operand); });
|
||||
stores = stores || extendedWithPointer || (pointer != 0 && pointers.contains(pointer));
|
||||
});
|
||||
return stores;
|
||||
}
|
||||
|
||||
void ShaderResources::prepareAddressBindings(std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots) {
|
||||
static const bool loopGuarded = std::getenv("APS5_LOOP_GUARD") != nullptr;
|
||||
for (const auto& shader : shaders) {
|
||||
@@ -22,6 +131,7 @@ void ShaderResources::prepareAddressBindings(std::span<const CompiledShader> sha
|
||||
Require(shader.program->bdaAbiVersion == (faults == 0 ? 0u : ShaderRecompiler::BdaAbi::Version), "incompatible BDA ABI version");
|
||||
// Rect-list validation needs a fault buffer, but never accesses guest addresses.
|
||||
usesBda = usesBda || tables != 0;
|
||||
if (tables != 0 && storesThroughAddressesCached(shader.program->spirv)) bdaReadOnly = false;
|
||||
usesFaultBuffer = usesFaultBuffer || faults != 0;
|
||||
const bool tableFaultsOnly = std::any_of(shader.program->bindings.begin(), shader.program->bindings.end(), [](const ShaderRecompiler::DescriptorBinding& binding) { return binding.role == ShaderRecompiler::DescriptorRole::ImageTableMap; });
|
||||
faultChecks = faultChecks || tables != 0 || (faults != 0 && (!tableFaultsOnly || shader.program->imageTableFaults != 0 || shader.program->poisonedSrtReads != 0 || loopGuarded));
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
@@ -145,6 +146,7 @@ struct TextureCache {
|
||||
std::chrono::steady_clock::time_point budgetRead{};
|
||||
std::uint64_t budget = 0;
|
||||
std::uint64_t reportedBudget = 0;
|
||||
std::uint64_t reportedStorage = 0;
|
||||
std::chrono::steady_clock::time_point reportedAt{};
|
||||
std::uint64_t erasures = 0;
|
||||
};
|
||||
@@ -154,6 +156,26 @@ TextureCache& Textures() {
|
||||
return cache;
|
||||
}
|
||||
|
||||
thread_local unsigned sampledCacheHeld = 0;
|
||||
thread_local unsigned storageCacheHeld = 0;
|
||||
|
||||
struct CacheHold {
|
||||
unsigned& depth;
|
||||
explicit CacheHold(unsigned& depth) : depth(depth) { ++depth; }
|
||||
~CacheHold() { --depth; }
|
||||
CacheHold(const CacheHold&) = delete;
|
||||
CacheHold& operator=(const CacheHold&) = delete;
|
||||
};
|
||||
|
||||
bool AcquireCache(std::mutex& mutex) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(20);
|
||||
do {
|
||||
if (mutex.try_lock()) return true;
|
||||
std::this_thread::yield();
|
||||
} while (std::chrono::steady_clock::now() < deadline);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct UncachedImages {
|
||||
std::mutex mutex;
|
||||
std::vector<std::weak_ptr<StorageTexture>> images;
|
||||
@@ -516,7 +538,7 @@ std::uint64_t storageBudget(const Context& context) {
|
||||
context.memoryProperties2(context.physical, &properties);
|
||||
cached.device = context.physical;
|
||||
cached.read = now;
|
||||
cached.budget = StorageTextureBudget(context.memory, &reported);
|
||||
cached.budget = StorageTextureBudget(context.memory, &reported, SampledTextureMemory(), storageCacheBytes());
|
||||
return cached.budget;
|
||||
}
|
||||
|
||||
@@ -533,11 +555,17 @@ std::uint64_t sampledBudget(const Context& context, TextureCache& cache) {
|
||||
const auto storage = storageCacheBytes();
|
||||
const auto budget = SampledTextureBudget(context.memory, &reported, sampled + storage);
|
||||
const bool fresh = cache.budgetDevice != context.physical;
|
||||
if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt))) {
|
||||
const auto storageLimit = storageBudget(context);
|
||||
if (const auto heap = largestDeviceLocalHeap(context.memory); heap && (fresh || SampledBudgetReportDue(cache.reportedBudget, budget, now - cache.reportedAt) || SampledBudgetReportDue(cache.reportedStorage, storageLimit, now - cache.reportedAt))) {
|
||||
char was[48] = "";
|
||||
if (!fresh) std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast<unsigned long long>(cache.reportedBudget >> 20u));
|
||||
std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB\n", static_cast<unsigned long long>(budget >> 20u), was, *heap, static_cast<unsigned long long>(reported.heapBudget[*heap] >> 20u), static_cast<unsigned long long>(reported.heapUsage[*heap] >> 20u), static_cast<unsigned long long>(sampled >> 20u), static_cast<unsigned long long>(cache.bytes >> 20u), static_cast<unsigned long long>(storage >> 20u), static_cast<unsigned long long>(storageBudget(context) >> 20u));
|
||||
char wasStorage[48] = "";
|
||||
if (!fresh) {
|
||||
std::snprintf(was, sizeof(was), " (was %llu MiB)", static_cast<unsigned long long>(cache.reportedBudget >> 20u));
|
||||
std::snprintf(wasStorage, sizeof(wasStorage), " (was %llu MiB)", static_cast<unsigned long long>(cache.reportedStorage >> 20u));
|
||||
}
|
||||
std::fprintf(stderr, "[gpu] sampled texture cache budget %llu MiB%s: device-local heap %u has a VK_EXT_memory_budget budget of %llu MiB and uses %llu MiB, %llu MiB of it sampled textures (%llu MiB in the cache) and %llu MiB cached storage images; storage image cache budget %llu MiB%s\n", static_cast<unsigned long long>(budget >> 20u), was, *heap, static_cast<unsigned long long>(reported.heapBudget[*heap] >> 20u), static_cast<unsigned long long>(reported.heapUsage[*heap] >> 20u), static_cast<unsigned long long>(sampled >> 20u), static_cast<unsigned long long>(cache.bytes >> 20u), static_cast<unsigned long long>(storage >> 20u), static_cast<unsigned long long>(storageLimit >> 20u), wasStorage);
|
||||
cache.reportedBudget = budget;
|
||||
cache.reportedStorage = storageLimit;
|
||||
cache.reportedAt = now;
|
||||
}
|
||||
cache.budgetDevice = context.physical;
|
||||
@@ -713,6 +741,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
|
||||
auto& cache = Textures();
|
||||
const auto key = MakeTextureKey(context.device, words, components, depthCompare);
|
||||
std::lock_guard lock(cache.mutex);
|
||||
const CacheHold held(sampledCacheHeld);
|
||||
dropUncachedViews(cache);
|
||||
if (auto it = findTexture(cache, key); it != cache.entries.end()) {
|
||||
if (it->source != nullptr) {
|
||||
@@ -953,6 +982,7 @@ std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std
|
||||
std::shared_ptr<StorageTexture> texture;
|
||||
{
|
||||
std::lock_guard lock(cache.mutex);
|
||||
const CacheHold held(storageCacheHeld);
|
||||
if (resource.mipCount > AllocatedLevels(resource)) {
|
||||
auto allocated = resource;
|
||||
allocated.mipCount = AllocatedLevels(resource);
|
||||
@@ -1010,10 +1040,14 @@ std::uint64_t TextureCacheBudget(const VkPhysicalDeviceMemoryProperties& memory)
|
||||
return std::max<std::uint64_t>(2048ull << 20u, heap ? memory.memoryHeaps[*heap].size / 4u : 0u);
|
||||
}
|
||||
|
||||
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported) {
|
||||
std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t sampledBytes, std::uint64_t storageBytes) {
|
||||
const auto heap = largestDeviceLocalHeap(memory);
|
||||
if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory);
|
||||
return std::max<std::uint64_t>(512ull << 20u, std::min<std::uint64_t>(TextureCacheBudget(memory), reported->heapBudget[*heap] / 4u));
|
||||
const auto budget = reported->heapBudget[*heap];
|
||||
const auto usage = reported->heapUsage[*heap];
|
||||
const auto reserved = usage - std::min<std::uint64_t>(usage, sampledBytes + storageBytes) + std::max<std::uint64_t>(2048ull << 20u, sampledBytes) + budget / 8u;
|
||||
const auto cap = std::min<std::uint64_t>(TextureCacheBudget(memory), budget / 4u);
|
||||
return std::max<std::uint64_t>(512ull << 20u, std::min<std::uint64_t>(cap, budget > reserved ? budget - reserved : 0u));
|
||||
}
|
||||
|
||||
std::uint64_t SampledTextureBudget(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t textureBytes) {
|
||||
@@ -1030,6 +1064,10 @@ bool SampledBudgetReportDue(std::uint64_t reported, std::uint64_t budget, std::c
|
||||
return change > reported / 10u && sinceReport >= std::chrono::seconds(10);
|
||||
}
|
||||
|
||||
std::uint64_t StorageTextureCacheBudget(const Context& context) {
|
||||
return storageBudget(context);
|
||||
}
|
||||
|
||||
TextureCacheUse TextureCacheUsage() {
|
||||
TextureCacheUse use;
|
||||
{
|
||||
@@ -1109,6 +1147,90 @@ void ClearCachedTextures(VkDevice device) {
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t ReleaseTextureCaches(std::uint64_t bytes) {
|
||||
thread_local bool releasing = false;
|
||||
if (releasing) return 0;
|
||||
releasing = true;
|
||||
struct Done {
|
||||
bool& flag;
|
||||
~Done() { flag = false; }
|
||||
} done{releasing};
|
||||
std::uint64_t freed = 0;
|
||||
std::size_t sampledEntries = 0;
|
||||
std::size_t storageEntries = 0;
|
||||
std::size_t storageKept = 0;
|
||||
bool sampledSkipped = false;
|
||||
bool storageSkipped = false;
|
||||
{
|
||||
auto& cache = Textures();
|
||||
if (sampledCacheHeld != 0 || !AcquireCache(cache.mutex)) {
|
||||
sampledSkipped = true;
|
||||
} else {
|
||||
std::lock_guard lock(cache.mutex, std::adopt_lock);
|
||||
dropUncachedViews(cache);
|
||||
while (freed < bytes && !cache.entries.empty()) {
|
||||
const auto back = std::prev(cache.entries.end());
|
||||
const auto accounted = back->accounted;
|
||||
const bool last = back->texture.use_count() == 1;
|
||||
eraseTexture(cache, back);
|
||||
++cache.counts.evicted;
|
||||
++sampledEntries;
|
||||
if (last) freed += accounted;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (freed < bytes) {
|
||||
auto& cache = StorageTextures();
|
||||
std::unique_lock<GuestMemory::GpuMutexType> gpu(GuestMemory::GpuMutex(), std::defer_lock);
|
||||
if (storageCacheHeld != 0 || !gpu.try_lock()) {
|
||||
storageSkipped = true;
|
||||
} else {
|
||||
try {
|
||||
while (freed < bytes) {
|
||||
std::vector<std::pair<std::shared_ptr<StorageTexture>, std::uint64_t>> evicted;
|
||||
{
|
||||
if (!AcquireCache(cache.mutex)) {
|
||||
storageSkipped = true;
|
||||
break;
|
||||
}
|
||||
std::lock_guard lock(cache.mutex, std::adopt_lock);
|
||||
std::uint64_t batch = 0;
|
||||
storageKept = 0;
|
||||
for (auto it = cache.entries.end(); it != cache.entries.begin() && batch + freed < bytes;) {
|
||||
--it;
|
||||
if (it->texture->Dirty()) {
|
||||
++storageKept;
|
||||
continue;
|
||||
}
|
||||
const auto accounted = it->accounted;
|
||||
const auto next = std::next(it);
|
||||
evicted.emplace_back(evictStorage(cache, it), accounted);
|
||||
batch += accounted;
|
||||
it = next;
|
||||
}
|
||||
if (evicted.empty()) break;
|
||||
}
|
||||
for (auto& [image, accounted] : evicted) {
|
||||
std::vector<std::shared_ptr<ShaderResources>> resources;
|
||||
SharedResourceCache().RemoveImage(image.get(), &resources);
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) {
|
||||
for (auto& resource : resources) recorder->Keep(std::move(resource));
|
||||
}
|
||||
resources.clear();
|
||||
++storageEntries;
|
||||
if (image.use_count() == 1) freed += accounted;
|
||||
image.reset();
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "[gpu] texture cache release stopped: %s\n", error.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr, "[gpu] texture caches released %llu MiB of the %llu MiB asked for: %zu sampled entries%s, %zu storage images%s, %zu with pending results kept\n", static_cast<unsigned long long>(freed >> 20u), static_cast<unsigned long long>(bytes >> 20u), sampledEntries, sampledSkipped ? " (cache busy, skipped)" : "", storageEntries, storageSkipped ? " (cache or device busy, skipped)" : "", storageKept);
|
||||
return freed;
|
||||
}
|
||||
|
||||
bool StorageImageCached(const Context& context, const StorageTexture* image) {
|
||||
auto& cache = StorageTextures();
|
||||
std::lock_guard lock(cache.mutex);
|
||||
@@ -1160,6 +1282,7 @@ const char* roleName(ShaderRecompiler::DescriptorRole role) {
|
||||
case ShaderRecompiler::DescriptorRole::ImageTable: return "ImageTable";
|
||||
case ShaderRecompiler::DescriptorRole::SamplerTable: return "SamplerTable";
|
||||
case ShaderRecompiler::DescriptorRole::ImageTableMap: return "ImageTableMap";
|
||||
case ShaderRecompiler::DescriptorRole::ImageStorageTable: return "ImageStorageTable";
|
||||
}
|
||||
throw std::runtime_error("AGC graphics: unknown descriptor role");
|
||||
}
|
||||
@@ -1585,10 +1708,13 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
||||
};
|
||||
for (const auto& binding : shader.program->bindings) {
|
||||
Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shader.stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "unexpected descriptor set: a shader resource must use its stage's descriptor set");
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable) {
|
||||
Require(binding.binding == ShaderRecompiler::RuntimeAbi::ImageTableBinding && binding.kind == ShaderRecompiler::DescriptorKind::SampledImage && binding.guestDescriptor.size() == static_cast<std::size_t>(binding.count) * 8u && binding.imageShapes.size() == binding.count && binding.imageDepthCompare.size() == binding.count && binding.imageSamplers.size() == binding.count, "image table binding is malformed");
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable || binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) {
|
||||
const bool storage = binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable;
|
||||
Require(binding.binding == (storage ? ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding : ShaderRecompiler::RuntimeAbi::ImageTableBinding) && binding.kind == (storage ? ShaderRecompiler::DescriptorKind::StorageImage : ShaderRecompiler::DescriptorKind::SampledImage) && binding.guestDescriptor.size() == static_cast<std::size_t>(binding.count) * 8u && binding.imageShapes.size() == binding.count && binding.imageDepthCompare.size() == binding.count && binding.imageSamplers.size() == binding.count, "image table binding is malformed");
|
||||
auto& table = tableStage(shader);
|
||||
Require(table.images == nullptr, "a stage binds two image tables");
|
||||
Require(storage ? !table.storage : !table.sampled, "a stage binds two image tables");
|
||||
Require(table.images == nullptr || table.images->count == binding.count, "a stage's image tables disagree on their elements");
|
||||
(storage ? table.storage : table.sampled) = true;
|
||||
table.images = &binding;
|
||||
table.firstSampler = firstSampler;
|
||||
continue;
|
||||
@@ -1683,6 +1809,7 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
||||
}
|
||||
splitSets = SplitsSets(context, shaders);
|
||||
std::array<VkShaderStageFlags, 2> tableFlags{};
|
||||
std::array<VkShaderStageFlags, 2> storageTableFlags{};
|
||||
for (auto& table : tableStages) {
|
||||
Require(table.mapAllocation != std::numeric_limits<std::size_t>::max(), "an image table stage has no map");
|
||||
const auto& map = table.map;
|
||||
@@ -1697,7 +1824,13 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
||||
table.elementBase = tableElements[table.set];
|
||||
table.map[ShaderRecompiler::ImageTableAbi::HeaderElementBase] = table.elementBase;
|
||||
tableElements[table.set] += elements;
|
||||
tableFlags[table.set] |= VulkanStage(table.stage);
|
||||
if (table.sampled) tableFlags[table.set] |= VulkanStage(table.stage);
|
||||
if (table.storage) storageTableFlags[table.set] |= VulkanStage(table.stage);
|
||||
}
|
||||
for (std::uint32_t set = 0; set < storageTableFlags.size(); ++set) {
|
||||
if (storageTableFlags[set] == 0) continue;
|
||||
bindings.push_back({{ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, StorageTableCapacity(context), storageTableFlags[set], nullptr}, {}, {}, VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT, static_cast<std::int32_t>(set)});
|
||||
storageTableBindings[set] = bindings.size() - 1u;
|
||||
}
|
||||
for (std::uint32_t set = 0; set < tableFlags.size(); ++set) {
|
||||
if (tableFlags[set] == 0) continue;
|
||||
@@ -1798,7 +1931,8 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
||||
if (storageBuffers != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, static_cast<std::uint32_t>(storageBuffers)});
|
||||
const auto tableDescriptors = tableBindings[0] != std::numeric_limits<std::size_t>::max() ? tableDescriptorCount(0) : 0u;
|
||||
if (plannedSampledImages + tableDescriptors != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, plannedSampledImages + tableDescriptors});
|
||||
if (plannedStorageImages != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages});
|
||||
const auto storageTableDescriptors = storageTableBindings[0] != std::numeric_limits<std::size_t>::max() ? bindings[storageTableBindings[0]].layout.descriptorCount : 0u;
|
||||
if (plannedStorageImages + storageTableDescriptors != 0) sizes.push_back({VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, plannedStorageImages + storageTableDescriptors});
|
||||
std::uint32_t samplerDescriptors = 0;
|
||||
for (const auto& binding : bindings) {
|
||||
if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) samplerDescriptors += binding.layout.descriptorCount;
|
||||
@@ -1848,6 +1982,7 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span<const C
|
||||
const bool split = SplitsSets(context, shaders);
|
||||
std::array<std::vector<VkDescriptorBindingFlags>, 2> bindingFlags;
|
||||
std::array<VkShaderStageFlags, 2> tableFlags{};
|
||||
std::array<VkShaderStageFlags, 2> storageTableFlags{};
|
||||
bool updateAfterBind = false;
|
||||
std::uint64_t storageBuffers = 0;
|
||||
for (const auto& shader : shaders) {
|
||||
@@ -1861,6 +1996,10 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span<const C
|
||||
tableFlags[split && shader.stage == ShaderRecompiler::ShaderStage::Fragment ? 1 : 0] |= flags;
|
||||
continue;
|
||||
}
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) {
|
||||
storageTableFlags[split && shader.stage == ShaderRecompiler::ShaderStage::Fragment ? 1 : 0] |= flags;
|
||||
continue;
|
||||
}
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::SamplerTable) {
|
||||
descriptions[set].push_back({binding.binding, VK_DESCRIPTOR_TYPE_SAMPLER, context.samplerTableCapacity, flags, nullptr});
|
||||
bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT);
|
||||
@@ -1878,6 +2017,11 @@ DescribedSetLayouts DescribeSetLayouts(const Context& context, std::span<const C
|
||||
bindingFlags[set].push_back(0);
|
||||
}
|
||||
}
|
||||
for (std::size_t set = 0; set < storageTableFlags.size(); ++set) {
|
||||
if (storageTableFlags[set] == 0) continue;
|
||||
descriptions[set].push_back({ShaderRecompiler::RuntimeAbi::ImageStorageTableBinding, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, StorageTableCapacity(context), storageTableFlags[set], nullptr});
|
||||
bindingFlags[set].push_back(VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT);
|
||||
}
|
||||
for (std::size_t set = 0; set < tableFlags.size(); ++set) {
|
||||
if (tableFlags[set] == 0) continue;
|
||||
descriptions[set].push_back({ShaderRecompiler::RuntimeAbi::ImageTableBinding, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, context.imageTableCapacity, tableFlags[set], nullptr});
|
||||
@@ -1968,6 +2112,7 @@ void ShaderResources::buildComplete() {
|
||||
for (auto element = first; element < last; ++element) {
|
||||
const auto index = binding.imageAllocations[element];
|
||||
if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_SAMPLER) images.push_back({samplers[index]->Handle(), VK_NULL_HANDLE, VK_IMAGE_LAYOUT_UNDEFINED});
|
||||
else if (binding.layout.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_IMAGE) images.push_back({VK_NULL_HANDLE, storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL});
|
||||
else images.push_back({VK_NULL_HANDLE, textureFirstLayer[index] ? textures[index]->FirstLayerView() : textures[index]->View(), textures[index]->Layout()});
|
||||
}
|
||||
writes.push_back(write);
|
||||
@@ -2072,6 +2217,7 @@ void mixDataWords(std::uint64_t& hash, std::span<const std::uint32_t> words) {
|
||||
// ResourceCache::Find and Touch calls (the [rescache] line).
|
||||
std::atomic<std::uint64_t> resourceCacheFinds{0};
|
||||
std::atomic<std::uint64_t> resourceCacheTouches{0};
|
||||
std::atomic<std::uint64_t> depthGuardRefusals{0};
|
||||
|
||||
}
|
||||
|
||||
@@ -2114,6 +2260,10 @@ void ShaderResources::noteReusable() {
|
||||
reusable = true;
|
||||
}
|
||||
|
||||
std::uint64_t ShaderResources::DepthGuardRefusals() {
|
||||
return depthGuardRefusals.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool ShaderResources::NeverReusable(std::span<const CompiledShader> shaders) {
|
||||
const bool loopGuard = LoopGuardEnabled();
|
||||
return std::any_of(shaders.begin(), shaders.end(), [loopGuard](const CompiledShader& shader) {
|
||||
@@ -2584,6 +2734,10 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
|
||||
const auto& own = image->Descriptor();
|
||||
const auto address = own.baseAddress;
|
||||
const auto bytes = static_cast<std::size_t>(image->GuestBytes());
|
||||
if (DepthSurfaceOverlaps(context.device, address, bytes)) {
|
||||
depthGuardRefusals.fetch_add(1, std::memory_order_relaxed);
|
||||
return fail(FastFail::Depth);
|
||||
}
|
||||
if (GuestMemory::CollectWrites(address, bytes) == 0) return fail(FastFail::Collect);
|
||||
if (own.dccAddress != 0) {
|
||||
// Refresh's unchanged branch (its key compare against the keys the content was
|
||||
@@ -2687,6 +2841,10 @@ bool ShaderResources::fastRevalidateEach() {
|
||||
if (own.dccAddress != 0) return false;
|
||||
const auto address = own.baseAddress;
|
||||
const auto bytes = static_cast<std::size_t>(image->GuestBytes());
|
||||
if (DepthSurfaceOverlaps(context.device, address, bytes)) {
|
||||
depthGuardRefusals.fetch_add(1, std::memory_order_relaxed);
|
||||
return false;
|
||||
}
|
||||
if (GuestMemory::CollectWrites(address, bytes) == 0) return false;
|
||||
if (PendingStorageOverlaps(address, bytes, image)) return false;
|
||||
if (!StorageImageCached(context, image)) return false;
|
||||
@@ -3383,7 +3541,7 @@ ResourceCache& SharedResourceCache() {
|
||||
return *cache;
|
||||
}
|
||||
|
||||
DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")), destroyPool(context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")), resetPool(context.Function<PFN_vkResetDescriptorPool>("vkResetDescriptorPool")) {}
|
||||
DescriptorCache::DescriptorCache(const Context& context) : context(context), destroyLayout(context.Function<PFN_vkDestroyDescriptorSetLayout>("vkDestroyDescriptorSetLayout")), destroyPool(context.Function<PFN_vkDestroyDescriptorPool>("vkDestroyDescriptorPool")), resetPool(context.Function<PFN_vkResetDescriptorPool>("vkResetDescriptorPool")), freeDescriptorSets(context.individuallyFreeDescriptorSets ? context.Function<PFN_vkFreeDescriptorSets>("vkFreeDescriptorSets") : nullptr) {}
|
||||
|
||||
DescriptorCache::~DescriptorCache() {
|
||||
for (const auto& pool : pools) destroyPool(context.device, pool.handle, nullptr);
|
||||
@@ -3410,7 +3568,12 @@ VkDescriptorSetLayout DescriptorCache::Layout(std::span<const std::uint32_t> key
|
||||
ValidateDescriptorLayout(context, bindings, updateAfterBind);
|
||||
++stats.layoutMisses;
|
||||
const auto layout = CreateSetLayout(context, bindings, updateAfterBind, flags);
|
||||
layouts.emplace(std::move(keyCopy), layout);
|
||||
try {
|
||||
layouts.emplace(std::move(keyCopy), layout);
|
||||
} catch (...) {
|
||||
destroyLayout(context.device, layout, nullptr);
|
||||
throw;
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -3481,9 +3644,11 @@ DescriptorCache::SetAllocation DescriptorCache::Allocate(VkDescriptorSetLayout l
|
||||
}
|
||||
VkDescriptorPoolCreateInfo poolInfo{VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO};
|
||||
poolInfo.flags = updateAfterBind ? VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT : 0;
|
||||
if (context.individuallyFreeDescriptorSets) poolInfo.flags |= VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
|
||||
poolInfo.maxSets = ChainPoolSets;
|
||||
poolInfo.poolSizeCount = static_cast<std::uint32_t>(ChainPoolCreateSizes.size());
|
||||
poolInfo.pPoolSizes = ChainPoolCreateSizes.data();
|
||||
chain.reserve(chain.size() + 1);
|
||||
VkDescriptorPool pool = VK_NULL_HANDLE;
|
||||
Check(context.Function<PFN_vkCreateDescriptorPool>("vkCreateDescriptorPool")(context.device, &poolInfo, nullptr, &pool), "vkCreateDescriptorPool");
|
||||
chain.push_back({pool});
|
||||
@@ -3536,6 +3701,7 @@ void DescriptorCache::Free(const SetAllocation& allocation) noexcept {
|
||||
if (allocation.poolIndex >= chain.size()) std::terminate();
|
||||
auto& pool = chain[allocation.poolIndex];
|
||||
if (pool.handle != allocation.pool || pool.liveSets == 0) std::terminate();
|
||||
if (freeDescriptorSets != nullptr && freeDescriptorSets(context.device, allocation.pool, 1, &allocation.set) != VK_SUCCESS) std::terminate();
|
||||
if (--pool.liveSets != 0) return;
|
||||
if (resetPool(context.device, pool.handle, 0) != VK_SUCCESS) std::terminate();
|
||||
pool.exhausted = false;
|
||||
@@ -3546,11 +3712,8 @@ DescriptorCache::Stats DescriptorCache::Counters() const {
|
||||
return stats;
|
||||
}
|
||||
|
||||
std::size_t ShaderResources::addGuestBuffer(std::span<const std::uint32_t> words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read) {
|
||||
Require(words.size() == 4, "buffer descriptor must contain four DWORDs");
|
||||
Require((words[1] & 0x40000000u) == 0, "buffer descriptor has reserved bits set");
|
||||
std::uint64_t ShaderResources::guestBufferBytes(std::span<const std::uint32_t> words) const {
|
||||
const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}};
|
||||
Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type");
|
||||
const auto address = descriptor.Base48();
|
||||
auto byteSize = descriptor.GetSize();
|
||||
if (byteSize > context.limits.maxStorageBufferRange) {
|
||||
@@ -3558,22 +3721,33 @@ std::size_t ShaderResources::addGuestBuffer(std::span<const std::uint32_t> words
|
||||
if (mappedEnd <= address) byteSize = 0;
|
||||
else if (mappedEnd - address < context.limits.maxStorageBufferRange) byteSize = mappedEnd - address;
|
||||
}
|
||||
if (byteSize == 0 || address == 0) return 0;
|
||||
if (const std::uint64_t stride = descriptor.Stride(); stride != 0 && byteSize <= context.limits.maxStorageBufferRange) {
|
||||
constexpr std::uint64_t GuestPageBytes = 0x4000;
|
||||
const auto end = address + byteSize;
|
||||
const auto overhang = end % GuestPageBytes;
|
||||
if (overhang != 0 && overhang < stride) {
|
||||
const auto allocationEnd = RegisteredReadableEnd(address);
|
||||
if (allocationEnd > address && allocationEnd < end && end - allocationEnd < stride) byteSize = allocationEnd - address;
|
||||
}
|
||||
}
|
||||
return byteSize;
|
||||
}
|
||||
|
||||
std::size_t ShaderResources::addGuestBuffer(std::span<const std::uint32_t> words, const ColorTarget* target, std::uint64_t indexAddress, std::size_t indexBytes, bool written, bool atomic, bool read) {
|
||||
Require(words.size() == 4, "buffer descriptor must contain four DWORDs");
|
||||
Require((words[1] & 0x40000000u) == 0, "buffer descriptor has reserved bits set");
|
||||
const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}};
|
||||
Require(descriptor.Type() == 0u, "buffer descriptor uses an unsupported type");
|
||||
const auto address = descriptor.Base48();
|
||||
const auto byteSize = guestBufferBytes(words);
|
||||
if (byteSize == 0 || address == 0) {
|
||||
allocations.push_back({0, EmptyBufferBytes, false, nullptr, ShaderRecompiler::DescriptorRole::GuestBuffers, false});
|
||||
return allocations.size() - 1;
|
||||
}
|
||||
Require(byteSize <= context.limits.maxStorageBufferRange, "shader buffer exceeds descriptor range limit");
|
||||
Require(byteSize <= std::numeric_limits<std::size_t>::max(), "shader buffer size exceeds host address space");
|
||||
auto size = static_cast<std::size_t>(byteSize);
|
||||
if (const std::uint64_t stride = descriptor.Stride(); stride != 0) {
|
||||
constexpr std::uint64_t GuestPageBytes = 0x4000;
|
||||
const auto end = address + size;
|
||||
const auto overhang = end % GuestPageBytes;
|
||||
if (overhang != 0 && overhang < stride) {
|
||||
const auto allocationEnd = RegisteredReadableEnd(address);
|
||||
if (allocationEnd > address && allocationEnd < end && end - allocationEnd < stride) size = static_cast<std::size_t>(allocationEnd - address);
|
||||
}
|
||||
}
|
||||
const auto size = static_cast<std::size_t>(byteSize);
|
||||
Require(target == nullptr || !overlap(address, size, target->address, target->bytes), "shader buffer aliases the render target");
|
||||
// APS5_ALL_BUFFERS_WRITTEN=1: every element is noted as written, as before bufferWritten existed.
|
||||
static const bool allWritten = std::getenv("APS5_ALL_BUFFERS_WRITTEN") != nullptr;
|
||||
@@ -4098,6 +4272,10 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t StorageTableCapacity(const Context& context) {
|
||||
return std::min<std::uint32_t>({context.imageTableCapacity, 4096u, context.limits.maxPerStageDescriptorStorageImages / 4u});
|
||||
}
|
||||
|
||||
std::uint32_t ImageTableCapacity(const Context& context) {
|
||||
if (!context.runtimeDescriptorArray || !context.partiallyBound || !context.variableDescriptorCount || !context.descriptorIndexing) return 0u;
|
||||
constexpr std::uint64_t sampledHeaps = static_cast<std::uint64_t>(ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding - ShaderRecompiler::RuntimeAbi::FirstImageBinding) * ShaderRecompiler::RuntimeAbi::SampledHeapCapacity;
|
||||
@@ -4193,14 +4371,16 @@ void ShaderResources::addSamplerTable(const ShaderRecompiler::DescriptorBinding&
|
||||
}
|
||||
|
||||
void ShaderResources::resolveImageTables() {
|
||||
if (tableBindings[0] == std::numeric_limits<std::size_t>::max() && tableBindings[1] == std::numeric_limits<std::size_t>::max()) return;
|
||||
const auto none = [](const std::array<std::size_t, 2>& set) { return set[0] == std::numeric_limits<std::size_t>::max() && set[1] == std::numeric_limits<std::size_t>::max(); };
|
||||
if (none(tableBindings) && none(storageTableBindings)) return;
|
||||
auto& counters = TextureCounts();
|
||||
for (std::size_t set = 0; set < tableBindings.size(); ++set) {
|
||||
if (storageTableBindings[set] != std::numeric_limits<std::size_t>::max()) bindings[storageTableBindings[set]].imageAllocations.assign(std::min(tableElements[set], StorageTableCapacity(context)), std::numeric_limits<std::size_t>::max());
|
||||
if (tableBindings[set] != std::numeric_limits<std::size_t>::max()) bindings[tableBindings[set]].imageAllocations.assign(std::min(tableElements[set], context.imageTableCapacity), std::numeric_limits<std::size_t>::max());
|
||||
}
|
||||
for (auto& table : tableStages) {
|
||||
if (table.images == nullptr) continue;
|
||||
auto& item = bindings[tableBindings[table.set]];
|
||||
auto* item = tableBindings[table.set] != std::numeric_limits<std::size_t>::max() ? &bindings[tableBindings[table.set]] : nullptr;
|
||||
const auto& binding = *table.images;
|
||||
const auto faults = table.map[ShaderRecompiler::ImageTableAbi::HeaderElementFaults];
|
||||
const auto stageSamplers = std::span<const std::shared_ptr<Sampler>>(samplers).subspan(table.firstSampler, table.samplerCount);
|
||||
@@ -4229,11 +4409,12 @@ void ShaderResources::resolveImageTables() {
|
||||
textureFirstLayer.push_back(firstLayer);
|
||||
describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
|
||||
table.elementTextures[element] = textures.size() - 1u;
|
||||
item.imageAllocations[static_cast<std::size_t>(table.elementBase) + element] = textures.size() - 1u;
|
||||
item->imageAllocations[static_cast<std::size_t>(table.elementBase) + element] = textures.size() - 1u;
|
||||
};
|
||||
const auto state = CurrentTableState();
|
||||
std::vector<std::shared_ptr<Texture>> cached;
|
||||
if (auto* reused = FindReusedTable(context.device, binding, state); reused != nullptr && TableCurrent(*reused) && CachedTableTextures(*reused, cached)) {
|
||||
const bool storageTable = std::find(binding.imageWritten.begin(), binding.imageWritten.end(), true) != binding.imageWritten.end();
|
||||
if (auto* reused = storageTable ? nullptr : FindReusedTable(context.device, binding, state); reused != nullptr && TableCurrent(*reused) && CachedTableTextures(*reused, cached)) {
|
||||
auto next = cached.begin();
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8u, 8u);
|
||||
@@ -4277,6 +4458,45 @@ void ShaderResources::resolveImageTables() {
|
||||
if (reusable) fresh.elements.push_back({{}, {}, {}, 0, false, true, error.what()});
|
||||
continue;
|
||||
}
|
||||
if (element < binding.imageWritten.size() && binding.imageWritten[element]) {
|
||||
reusable = false;
|
||||
const auto storageBinding = storageTableBindings[table.set];
|
||||
if (storageBinding == std::numeric_limits<std::size_t>::max()) {
|
||||
table.map[faults + element] = rejectTableElement(table, words, "set " + std::to_string(table.set) + " has no image storage table, but the shader stores to its table element " + std::to_string(absolute));
|
||||
continue;
|
||||
}
|
||||
if (absolute >= StorageTableCapacity(context)) {
|
||||
table.map[faults + element] = rejectTableElement(table, words, "the stored table element " + std::to_string(absolute) + " of set " + std::to_string(table.set) + " is past the image storage table's capacity of " + std::to_string(StorageTableCapacity(context)) + " images");
|
||||
continue;
|
||||
}
|
||||
CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes));
|
||||
if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes));
|
||||
const auto mip = std::min(resource.baseLevel, resource.mipCount - 1u);
|
||||
if (resource.minLod > mip * 256u) {
|
||||
table.map[faults + element] = rejectTableElement(table, words, "the table entry's storage T# clamps its minimum LOD above the level it addresses, which is not implemented");
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
storageTextures.push_back(cachedStorageTexture(context, words, resource, mip, guestBytes, true));
|
||||
} catch (const std::runtime_error& error) {
|
||||
table.map[faults + element] = rejectTableElement(table, words, error.what());
|
||||
continue;
|
||||
}
|
||||
storageMips.push_back(mip);
|
||||
storageKeys.push_back(resource.dccAddress);
|
||||
storageFirstLayer.push_back(firstLayer);
|
||||
storageWritten.push_back(true);
|
||||
storageAtomic.push_back(false);
|
||||
storageAtomic64.push_back(false);
|
||||
describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
|
||||
bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u;
|
||||
continue;
|
||||
}
|
||||
if (item == nullptr) {
|
||||
table.map[faults + element] = rejectTableElement(table, words, "a sampled image table entry has no sampled image table binding");
|
||||
reusable = false;
|
||||
continue;
|
||||
}
|
||||
CompletePendingCpuWrites(context, resource.baseAddress, static_cast<std::size_t>(guestBytes));
|
||||
if (resource.dccAddress != 0) CompletePendingCpuWrites(context, resource.dccAddress, DccKeyBytes(guestBytes));
|
||||
std::shared_ptr<Texture> texture;
|
||||
@@ -4363,15 +4583,20 @@ void ShaderResources::finishImageTables() {
|
||||
bda->SetImageTables(std::move(faults), std::move(ranges));
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> ShaderResources::PresyncSurfaces() const {
|
||||
std::vector<std::pair<std::uint64_t, std::uint64_t>> surfaces;
|
||||
std::vector<PresyncSurface> ShaderResources::PresyncSurfaces() const {
|
||||
std::vector<PresyncSurface> surfaces;
|
||||
// A surface's lookup reads guest memory on the CPU unless it is served GPU-direct from a host
|
||||
// import: a storage image of imported memory uploads and refreshes from the import, and a
|
||||
// sampled texture of one views that image (SampledFromStorageEligible), so neither waits for
|
||||
// the producer.
|
||||
const auto consider = [&](std::uint32_t format, std::uint64_t address, std::uint64_t guestBytes, bool sampled) {
|
||||
const bool gpuDirect = sampled ? SampledFromStorageEligible(context, format, address, guestBytes) : HostImportCovers(context, address, static_cast<std::size_t>(guestBytes));
|
||||
if (!gpuDirect) surfaces.emplace_back(address, guestBytes);
|
||||
if (!gpuDirect) surfaces.push_back({address, guestBytes, false});
|
||||
};
|
||||
static const bool keyRanges = std::getenv("APS5_NO_DCC_PRESYNC") == nullptr;
|
||||
const auto considerKeys = [&](std::uint64_t dccAddress, std::uint64_t guestBytes) {
|
||||
const auto keyBytes = DccKeyBytes(guestBytes);
|
||||
if (keyRanges && dccAddress != 0 && keyBytes != 0) surfaces.push_back({dccAddress, keyBytes, true});
|
||||
};
|
||||
if (completed) {
|
||||
// A cached object (the resource cache): Revalidate repeats the lookups of the objects the
|
||||
@@ -4387,18 +4612,22 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> ShaderResources::PresyncSur
|
||||
const auto& texture = textures[textureIndex++];
|
||||
if (texture->ViewsStorageImage()) consider(range.format, range.address, range.bytes, false);
|
||||
else consider(range.format, range.address, range.bytes, true);
|
||||
considerKeys(range.dccAddress, range.bytes);
|
||||
}
|
||||
}
|
||||
for (std::size_t index = 0; index < storageTextures.size(); ++index) {
|
||||
if (storageTextures[index] == nullptr || (index != 0 && storageTextures[index] == storageTextures[index - 1])) continue;
|
||||
const auto& image = *storageTextures[index];
|
||||
consider(image.Descriptor().format, image.Descriptor().baseAddress, image.GuestBytes(), false);
|
||||
considerKeys(image.Descriptor().dccAddress, image.GuestBytes());
|
||||
}
|
||||
return surfaces;
|
||||
}
|
||||
if (!imageRecords.empty()) {
|
||||
for (const auto& record : imageRecords) {
|
||||
if (record.decoded) consider(record.resource.format, record.resource.baseAddress, record.guestBytes, record.sampled);
|
||||
if (!record.decoded) continue;
|
||||
consider(record.resource.format, record.resource.baseAddress, record.guestBytes, record.sampled);
|
||||
considerKeys(record.resource.dccAddress, record.guestBytes);
|
||||
}
|
||||
return surfaces;
|
||||
}
|
||||
@@ -4407,14 +4636,18 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> ShaderResources::PresyncSur
|
||||
forEachImageElement(*deferredCompute.program, [&](const ShaderRecompiler::DescriptorBinding& binding, std::uint32_t, std::span<const std::uint32_t> words) {
|
||||
if (IsNullTextureResource(words)) return;
|
||||
const auto resource = DecodeTextureResource(words);
|
||||
consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage);
|
||||
const auto guestBytes = DescribeSurface(resource).guestBytes;
|
||||
consider(resource.format, resource.baseAddress, guestBytes, binding.kind == ShaderRecompiler::DescriptorKind::SampledImage);
|
||||
considerKeys(resource.dccAddress, guestBytes);
|
||||
});
|
||||
for (const auto& binding : deferredCompute.program->bindings) {
|
||||
if (binding.role != ShaderRecompiler::DescriptorRole::ImageTable) continue;
|
||||
for (std::size_t element = 0; element < binding.guestDescriptor.size() / 8u; ++element) {
|
||||
try {
|
||||
const auto resource = DecodeTextureResource(std::span<const std::uint32_t>(binding.guestDescriptor).subspan(element * 8u, 8u));
|
||||
consider(resource.format, resource.baseAddress, DescribeSurface(resource).guestBytes, true);
|
||||
const auto guestBytes = DescribeSurface(resource).guestBytes;
|
||||
consider(resource.format, resource.baseAddress, guestBytes, true);
|
||||
considerKeys(resource.dccAddress, guestBytes);
|
||||
} catch (const std::runtime_error&) {
|
||||
}
|
||||
}
|
||||
@@ -4520,13 +4753,71 @@ std::optional<std::vector<ShaderResources::MovedBuffer>> ShaderResources::MovedR
|
||||
return moved;
|
||||
}
|
||||
|
||||
const char* ShaderResources::MoveRefusalName(MoveRefusal refusal) {
|
||||
switch (refusal) {
|
||||
case MoveRefusal::None: return "none";
|
||||
case MoveRefusal::Shape: return "bindings differ";
|
||||
case MoveRefusal::NotGuest: return "not read in place";
|
||||
case MoveRefusal::Written: return "written or emptied";
|
||||
case MoveRefusal::Limit: return "over the limit";
|
||||
case MoveRefusal::Inaccessible: return "unmapped";
|
||||
case MoveRefusal::Overlap: return "pending write";
|
||||
case MoveRefusal::Bindings: return "no bindings made";
|
||||
case MoveRefusal::Count: break;
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
std::optional<std::vector<ShaderResources::MovedBuffer>> ShaderResources::MovedDispatchBuffers(const CompiledShader& shader, Recorder* recorder, std::size_t maxBytes, MoveProof& proof) const {
|
||||
proof = {};
|
||||
const auto refuse = [&](MoveRefusal why) {
|
||||
proof.refusal = why;
|
||||
return std::optional<std::vector<MovedBuffer>>{};
|
||||
};
|
||||
std::vector<MovedBuffer> moved;
|
||||
if (shader.program == nullptr || usesBda) return refuse(MoveRefusal::Shape);
|
||||
if (_set == VK_NULL_HANDLE) return moved;
|
||||
for (const auto& binding : shader.program->bindings) {
|
||||
if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue;
|
||||
const auto kept = std::find_if(bindings.begin(), bindings.end(), [&](const Binding& item) { return item.layout.binding == binding.binding; });
|
||||
if (kept == bindings.end() || kept->allocations.size() != binding.count || binding.guestDescriptor.size() != static_cast<std::size_t>(binding.count) * 4u) return refuse(MoveRefusal::Shape);
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
const auto* words = binding.guestDescriptor.data() + static_cast<std::size_t>(element) * 4u;
|
||||
const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}};
|
||||
const auto address = descriptor.Base48();
|
||||
const auto size = guestBufferBytes(std::span<const std::uint32_t>(words, 4));
|
||||
const auto& item = allocations[kept->allocations[element]];
|
||||
const bool empty = size == 0 || address == 0;
|
||||
if (!item.guest) {
|
||||
if (empty) continue;
|
||||
return refuse(MoveRefusal::NotGuest);
|
||||
}
|
||||
if (item.address == address && item.size == size) continue;
|
||||
const bool written = element >= binding.bufferWritten.size() || binding.bufferWritten[element];
|
||||
if (written || empty || item.written) return refuse(MoveRefusal::Written);
|
||||
if (size > context.limits.maxStorageBufferRange) return refuse(MoveRefusal::Limit);
|
||||
const auto begin = address - item.adjustment;
|
||||
const auto bytes = static_cast<std::size_t>(GuestBufferMemory::ViewBytes(size, item.adjustment));
|
||||
if (maxBytes != 0 && proof.bytes + bytes > maxBytes) return refuse(MoveRefusal::Limit);
|
||||
if (!GuestMemory::Accessible(reinterpret_cast<const void*>(begin), bytes)) return refuse(MoveRefusal::Inaccessible);
|
||||
const bool pending = recorder != nullptr ? recorder->PendingWriteOverlaps(begin, bytes) : Recorder::SnapshotWriteOverlaps(begin, bytes);
|
||||
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> range{{{begin, begin + bytes}}};
|
||||
if (guestMemory.WritesOverlap(begin, bytes) || pending || StorageTexture::AnyPendingOverlaps(range) || AnyShadowedOverlaps(begin, bytes)) return refuse(MoveRefusal::Overlap);
|
||||
moved.push_back({kept->allocations[element], address, static_cast<std::size_t>(size)});
|
||||
++proof.buffers;
|
||||
proof.bytes += bytes;
|
||||
}
|
||||
}
|
||||
return moved;
|
||||
}
|
||||
|
||||
std::size_t ShaderResources::MaxDrawSnapshotBytes() {
|
||||
const char* text = std::getenv("APS5_DRAW_SNAPSHOT_MAX_MIB");
|
||||
const auto mib = text != nullptr ? std::strtoull(text, nullptr, 10) : 64ull;
|
||||
return static_cast<std::size_t>(mib * 1024ull * 1024ull);
|
||||
}
|
||||
|
||||
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved) const {
|
||||
std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindings(Recorder& recorder, std::span<const MovedBuffer> moved, bool movedOnly) const {
|
||||
if ((_set == VK_NULL_HANDLE && fragment.set == VK_NULL_HANDLE) || usesBda) return {};
|
||||
const auto reads = guestMemory.InPlaceReads();
|
||||
const auto maxSnapshotBytes = maxDrawSnapshotBytes;
|
||||
@@ -4566,7 +4857,7 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
|
||||
address = override->address;
|
||||
size = override->size;
|
||||
} else {
|
||||
if (!item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue;
|
||||
if (movedOnly || !item.guest || item.written || guestMemory.WritesOverlap(item.address, item.size)) continue;
|
||||
const bool direct = std::any_of(reads.begin(), reads.end(), [&](const auto& range) { return item.address >= range.first && item.address < range.second && item.size <= range.second - item.address; });
|
||||
if (!direct || recorder.PendingWriteOverlaps(item.address, item.size)) continue;
|
||||
}
|
||||
@@ -4637,7 +4928,8 @@ std::shared_ptr<ShaderResources::DrawBindings> ShaderResources::PrepareDrawBindi
|
||||
for (std::size_t index = 0; index < selected.size(); ++index) snapshotIndices[selected[index]] = index;
|
||||
auto nextWrite = writes.begin();
|
||||
for (const auto& binding : bindings) {
|
||||
while (nextWrite != writes.end() && nextWrite->dstBinding == binding.layout.binding) {
|
||||
const auto original = setOf(binding) == 0 ? _set : fragment.set;
|
||||
while (nextWrite != writes.end() && nextWrite->dstBinding == binding.layout.binding && nextWrite->dstSet == original) {
|
||||
auto& write = *nextWrite++;
|
||||
write.dstSet = setOf(binding) == 0 ? result->allocation.set : result->fragmentAllocation.set;
|
||||
if (binding.allocations.empty()) continue;
|
||||
|
||||
@@ -115,7 +115,7 @@ struct Module {
|
||||
return;
|
||||
}
|
||||
// One gl_ClipDistance array holds the guest clip distances and the recompiler's all-zero position distance.
|
||||
if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationEvaluation || stage == Stage::Mesh)) || (value == spv::BuiltInCullDistance && (vertex || stage == Stage::Mesh)))) {
|
||||
if (!input && ((value == spv::BuiltInClipDistance && (vertex || stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation || stage == Stage::Mesh)) || (value == spv::BuiltInCullDistance && (vertex || stage == Stage::Mesh)))) {
|
||||
const auto signature = Signature(type);
|
||||
auto& count = value == spv::BuiltInClipDistance ? clipDistances : cullDistances;
|
||||
Require(count == 0 && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']', "invalid or duplicate clip or cull distance output");
|
||||
@@ -131,7 +131,8 @@ struct Module {
|
||||
return;
|
||||
}
|
||||
if (stage == Stage::TessellationControl || stage == Stage::TessellationEvaluation) {
|
||||
Require(input && ((value == spv::BuiltInPosition && signature == "f32x4") || (value == spv::BuiltInTessCoord && stage == Stage::TessellationEvaluation && signature == "f32x3") || ((value == spv::BuiltInInvocationId || value == spv::BuiltInPrimitiveId || value == spv::BuiltInPatchVertices) && (signature == "i32" || signature == "u32"))), "unsupported tessellation built-in");
|
||||
const bool clipInput = value == spv::BuiltInClipDistance && signature.size() == 6 && signature.starts_with("f32[") && signature[4] >= '1' && signature[4] <= '8' && signature[5] == ']';
|
||||
Require(input && (clipInput || (value == spv::BuiltInPosition && signature == "f32x4") || (value == spv::BuiltInTessCoord && stage == Stage::TessellationEvaluation && signature == "f32x3") || ((value == spv::BuiltInInvocationId || value == spv::BuiltInPrimitiveId || value == spv::BuiltInPatchVertices) && (signature == "i32" || signature == "u32"))), "unsupported tessellation built-in");
|
||||
return;
|
||||
}
|
||||
if (!input && value == spv::BuiltInPrimitiveLineIndicesEXT) primitiveIndices = 2u;
|
||||
@@ -252,7 +253,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
||||
fragment &&
|
||||
capability == spv::CapabilityStencilExportEXT;
|
||||
|
||||
if ((vertex || evaluation || mesh) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) {
|
||||
if ((vertex || control || evaluation || mesh) && (capability == spv::CapabilityClipDistance || capability == spv::CapabilityCullDistance)) {
|
||||
char word[16];
|
||||
std::snprintf(word, sizeof(word), "0x%08x", state.paClVsOutCntl);
|
||||
Require(capability == spv::CapabilityClipDistance ? shaderClipDistance : (vertex || mesh) && shaderCullDistance, std::string("vertex clip or cull distance needs shaderClipDistance or shaderCullDistance, which the device lacks (PA_CL_VS_OUT_CNTL=") + word + ")");
|
||||
@@ -264,7 +265,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
||||
capability == spv::CapabilityImage1D ||
|
||||
capability == spv::CapabilityImageGatherExtended ||
|
||||
capability == spv::CapabilityImageQuery ||
|
||||
((vertex || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) ||
|
||||
((vertex || control || evaluation || mesh) && shaderClipDistance && capability == spv::CapabilityClipDistance) ||
|
||||
(vertex && capability == spv::CapabilityDrawParameters) ||
|
||||
(fragment && geometryShader && capability == spv::CapabilityGeometry) ||
|
||||
(fragment && sampleRateShading && capability == spv::CapabilitySampleRateShading) ||
|
||||
@@ -441,6 +442,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
||||
const auto it = module.modes.find(name);
|
||||
Require(it != module.modes.end() && it->second == operands, "missing or incompatible shader execution mode");
|
||||
};
|
||||
const bool barycentricPatches = (control || evaluation) && !state.rectList && !state.stages.tessellation && state.stages.path == ShaderPath::Vertex;
|
||||
if (mesh) {
|
||||
Require(state.stages.mesh.has_value(), "mesh configuration is missing");
|
||||
const auto& config = *state.stages.mesh;
|
||||
@@ -451,6 +453,12 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
||||
mode(spv::ExecutionModeOutputPrimitivesEXT, {config.maxPrimitives});
|
||||
mode(config.outputPrimitiveSize == 2u ? spv::ExecutionModeOutputLinesEXT : spv::ExecutionModeOutputTrianglesEXT, {});
|
||||
Require(module.modes.size() == 4, "unsupported mesh execution mode");
|
||||
} else if (barycentricPatches) {
|
||||
if (control) mode(spv::ExecutionModeOutputVertices, {3u});
|
||||
mode(spv::ExecutionModeTriangles, {});
|
||||
mode(spv::ExecutionModeSpacingEqual, {});
|
||||
mode(spv::ExecutionModeVertexOrderCw, {});
|
||||
Require(module.modes.size() == (control ? 4u : 3u), "unsupported generated tessellation execution mode");
|
||||
} else if (control) {
|
||||
Require(state.rectList || state.stages.tessellation.has_value(), "tessellation configuration is missing");
|
||||
mode(spv::ExecutionModeOutputVertices, {state.rectList ? 4u : state.stages.tessellation->outputControlPoints});
|
||||
@@ -503,7 +511,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
||||
} else if (geometry) {
|
||||
Require(length->second == 3u, "geometry interface array must hold the three vertices of a triangle");
|
||||
} else {
|
||||
const auto expected = state.rectList ? (control && input ? 3u : 4u) : control && input ? state.stages.tessellation->inputControlPoints : state.stages.tessellation->outputControlPoints;
|
||||
const auto expected = barycentricPatches ? 3u : state.rectList ? (control && input ? 3u : 4u) : control && input ? state.stages.tessellation->inputControlPoints : state.stages.tessellation->outputControlPoints;
|
||||
Require(length->second == expected, "tessellation interface array disagrees with control-point count");
|
||||
}
|
||||
typeId = outer[2];
|
||||
@@ -549,10 +557,10 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
||||
Require(decoration.set && decoration.binding, "unbound shader resource");
|
||||
const auto key = std::make_pair(*decoration.set, *decoration.binding);
|
||||
const auto binding = std::find_if(shader.bindings.begin(), shader.bindings.end(), [&](const auto& item) { return item.descriptorSet == key.first && item.binding == key.second; });
|
||||
Require(descriptors.insert(key).second || (binding != shader.bindings.end() && binding->role == ShaderRecompiler::DescriptorRole::ImageTable), "duplicate SPIR-V resource binding");
|
||||
Require(descriptors.insert(key).second || (binding != shader.bindings.end() && (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::ImageStorageTable)), "duplicate SPIR-V resource binding");
|
||||
Require(binding != shader.bindings.end(), "SPIR-V resource is absent from recompiler binding metadata");
|
||||
Require(binding->kind == ShaderRecompiler::DescriptorKind::Sampler || binding->kind == ShaderRecompiler::DescriptorKind::SampledImage || binding->kind == ShaderRecompiler::DescriptorKind::StorageImage, "SPIR-V descriptor type disagrees with recompiler binding metadata");
|
||||
if (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::SamplerTable) {
|
||||
if (binding->role == ShaderRecompiler::DescriptorRole::ImageTable || binding->role == ShaderRecompiler::DescriptorRole::ImageStorageTable || binding->role == ShaderRecompiler::DescriptorRole::SamplerTable) {
|
||||
Require(type.size() == 3 && (type[0] & 0xffffu) == spv::OpTypeRuntimeArray, "image table must be declared as a runtime descriptor array");
|
||||
continue;
|
||||
}
|
||||
@@ -604,7 +612,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
||||
Require(shader.vertexAttributes.empty(), "vertex attribute metadata is invalid for this stage");
|
||||
}
|
||||
Require(descriptors.size() == shader.bindings.size(), "recompiler binding metadata contains undeclared resources");
|
||||
Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl)), "recompiler push constant metadata disagrees with SPIR-V");
|
||||
Require(push == (!shader.pushConstants.empty() || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V");
|
||||
for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable");
|
||||
if (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices");
|
||||
if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position");
|
||||
@@ -635,7 +643,9 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
|
||||
Require(state.stages.mesh.has_value() == mesh && state.stages.tessellation.has_value() == tessellation, "graphics stage configuration disagrees with its path");
|
||||
Require(!state.rectList || (state.stages.path == ShaderPath::Vertex && state.topology == VK_PRIMITIVE_TOPOLOGY_PATCH_LIST && state.cullMode == VK_CULL_MODE_NONE), "invalid rect-list pipeline state");
|
||||
const bool geometry = state.stages.path == ShaderPath::Vertex && !state.rectList && shaders.size() == 3u;
|
||||
Require(geometry || shaders.size() == (tessellation || state.rectList ? 4u : 2u), "incorrect graphics stage count");
|
||||
const bool patches = BarycentricPatches(state, shaders) && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active;
|
||||
Require(geometry || shaders.size() == (tessellation || state.rectList || patches ? 4u : 2u), "incorrect graphics stage count");
|
||||
if (patches) Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "emulated barycentrics through tessellation require a triangle list");
|
||||
if (geometry) {
|
||||
Require(shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active, "a geometry stage is generated only for emulated barycentrics");
|
||||
Require(state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP || state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, "emulated barycentrics require triangle primitives");
|
||||
@@ -644,7 +654,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
|
||||
static_cast<void>(AssemblePushConstants(shaders));
|
||||
Module previous;
|
||||
for (std::size_t i = 0; i < shaders.size(); ++i) {
|
||||
const auto expected = state.rectList ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex;
|
||||
const auto expected = state.rectList || patches ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex;
|
||||
Require(shaders[i].program != nullptr, "missing compiled shader");
|
||||
Require(shaders[i].stage == expected, "graphics stage order disagrees");
|
||||
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's");
|
||||
@@ -675,7 +685,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
|
||||
const auto color = std::find_if(state.colors.begin(), state.colors.end(), [&](const ColorTarget& target) { return target.exportIndex == location; });
|
||||
const bool uintExport = color != state.colors.end() && color->uintExport;
|
||||
const bool packed = color != state.colors.end() && color->packing != ShaderRecompiler::ColorExportPacking::None;
|
||||
Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed 10_11_11 unorm attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments");
|
||||
Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments");
|
||||
locations.insert(location);
|
||||
}
|
||||
Require((!state.dualSourceBlend && state.reversedBlend == 0) || previous.secondSource == "f32x4", "dual-source blending needs a float4 second color at location 0 index 1");
|
||||
|
||||
@@ -743,6 +743,10 @@ struct DecodedColorFormat {
|
||||
ShaderRecompiler::ColorExportPacking packing = ShaderRecompiler::ColorExportPacking::None;
|
||||
};
|
||||
|
||||
const char* PackedColorTargetName(ShaderRecompiler::ColorExportPacking packing) {
|
||||
return packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "11_11_10 float" : "10_11_11 unorm";
|
||||
}
|
||||
|
||||
// CB_COLOR_INFO FORMAT / NUMBER_TYPE / COMP_SWAP to a Vulkan attachment format. AMD formats list
|
||||
// components from the least significant bits, as Vulkan's non-packed formats do.
|
||||
DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number, std::uint32_t swap) {
|
||||
@@ -753,7 +757,7 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
|
||||
};
|
||||
const bool alternate = swap == 1;
|
||||
const auto single = [&](VkFormat vkFormat, std::uint32_t bytes) { return DecodedColorFormat{vkFormat, bytes, static_cast<std::uint8_t>((0xe4u & ~3u) | swap)}; };
|
||||
if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 10 && format != 12) return fail();
|
||||
if (swap > 1 && format != 1 && format != 2 && format != 4 && format != 7 && format != 10 && format != 12 && format != 16) return fail();
|
||||
switch (format) {
|
||||
case 1:
|
||||
if (number == unorm) return single(VK_FORMAT_R8_UNORM, 1);
|
||||
@@ -791,6 +795,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
|
||||
if (swap != 0 || (number != floating && number != unorm)) return fail();
|
||||
if (number == unorm) return {VK_FORMAT_R32_UINT, 4, 0xe4u, ShaderRecompiler::ColorExportPacking::Unorm10_11_11};
|
||||
return {VK_FORMAT_B10G11R11_UFLOAT_PACK32, 4};
|
||||
case 7:
|
||||
if (swap != 2 || number != floating) return fail();
|
||||
return {VK_FORMAT_R32_UINT, 4, 0xc6u, ShaderRecompiler::ColorExportPacking::Float11_11_10};
|
||||
case 9:
|
||||
// COLOR_2_10_10_10 keeps red in the low bits, the Vulkan A2B10G10R10 packing.
|
||||
if (number != unorm) return fail();
|
||||
@@ -821,6 +828,9 @@ DecodedColorFormat DecodeColorFormat(std::uint32_t format, std::uint32_t number,
|
||||
if (number == floating) return {VK_FORMAT_R32G32B32A32_SFLOAT, 16};
|
||||
if (number == uint) return {VK_FORMAT_R32G32B32A32_UINT, 16};
|
||||
return fail();
|
||||
case 16:
|
||||
if ((swap != 0 && swap != 2) || number != unorm) return fail();
|
||||
return {VK_FORMAT_R5G6B5_UNORM_PACK16, 2, static_cast<std::uint8_t>(swap == 2 ? 0xe4u : 0xc6u)};
|
||||
case 17:
|
||||
if (swap != 0 || number != unorm) return fail();
|
||||
return {VK_FORMAT_A1R5G5B5_UNORM_PACK16, 2, 0xc6u};
|
||||
@@ -1350,16 +1360,22 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
|
||||
}
|
||||
}
|
||||
if (color.packing != ShaderRecompiler::ColorExportPacking::None) {
|
||||
const std::string packed = std::string(color.packing == ShaderRecompiler::ColorExportPacking::Float11_11_10 ? "an " : "a ") + PackedColorTargetName(color.packing);
|
||||
const auto slotExport = (exportFormat >> (4u * color.exportIndex)) & 0xfu;
|
||||
if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into a 10_11_11 unorm target is unsupported");
|
||||
Require(!state.blendEnable, "blending into a 10_11_11 unorm color target is unsupported");
|
||||
Require((exportedMask & 7u) == 7u, "partial writes of a 10_11_11 unorm color target are unsupported");
|
||||
if (slotExport == 5 || slotExport == 6) throw std::runtime_error("AGC graphics: color export format " + std::to_string(slotExport) + " into " + packed + " target is unsupported");
|
||||
Require(!state.blendEnable, "blending into " + packed + " color target is unsupported");
|
||||
Require((exportedMask & 7u) == 7u, "partial writes of " + packed + " color target are unsupported");
|
||||
}
|
||||
if (state.blendEnable && !reversed && (secondSource(state.srcColorBlendFactor) || secondSource(state.dstColorBlendFactor) || secondSource(state.srcAlphaBlendFactor) || secondSource(state.dstAlphaBlendFactor))) {
|
||||
const auto addition = [](VkBlendOp op) { return op == VK_BLEND_OP_ADD || op == VK_BLEND_OP_SUBTRACT || op == VK_BLEND_OP_REVERSE_SUBTRACT; };
|
||||
Require(addition(state.colorBlendOp) && addition(state.alphaBlendOp), "dual-source blending with a MIN or MAX operation is unsupported");
|
||||
result.dualSourceBlend = true;
|
||||
}
|
||||
if (const auto info = read(cx, 0x31c + slot * 0xfu); state.blendEnable && (info & 0x8000u) == 0 && ((info >> 8u) & 7u) <= 1u) {
|
||||
char detail[80];
|
||||
std::snprintf(detail, sizeof(detail), " (CB_COLOR_INFO 0x%08x, CB_BLEND%u_CONTROL 0x%08x)", info, slot, blend);
|
||||
throw std::runtime_error(std::string("AGC graphics: blending unclamped normalized color is not implemented") + detail);
|
||||
}
|
||||
result.blends[color.exportIndex] = state;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < 3; ++i) {
|
||||
@@ -1517,9 +1533,9 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
|
||||
Require(color.samples <= 8u, "color targets with more than eight samples are unsupported");
|
||||
Require((1u << ((attrib >> 15u) & 3u)) == color.samples, "color targets with fewer fragments than samples (EQAA) are unsupported");
|
||||
const auto multisampleInfo = color.samples > 1 ? 0x0c006000u : 0u;
|
||||
// ROUND_MODE (bit 18) only affects unorm rounding. With DCC_ENABLE (bit 28) the target is written
|
||||
// ROUND_MODE (bit 18) only affects unorm rounding and packed float truncation. With DCC_ENABLE (bit 28) the target is written
|
||||
if ((info & ~(0x00039f7cu | 0x00040000u | 0x10000000u | 0x00002000u | multisampleInfo)) != 0) throw std::runtime_error("AGC graphics: color compression, DCC, endian conversion, nonstandard rounding or color optimization is unsupported (CB_COLOR_INFO 0x" + [&] { char text[16]; std::snprintf(text, sizeof(text), "%08x", info); return std::string(text); }() + ")");
|
||||
Require((info & 0x8000u) != 0 || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
|
||||
Require((info & 0x8000u) != 0 || number <= 1u || number == 7 || number == 4 || number == 5, "unclamped normalized color is unsupported");
|
||||
const auto view = read(cx, 0x31b + stride);
|
||||
Require((view & ~0x3fffffffu) == 0, "reserved CB_COLOR_VIEW bits are set");
|
||||
const auto slice = view & 0x1fffu;
|
||||
@@ -1536,7 +1552,11 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
|
||||
Require(((attrib3 >> 24u) & 3u) != 0u || (attrib2 & 0x3fffu) == 0u, "1D color targets taller than one row are unsupported");
|
||||
if (volume) {
|
||||
color.depth = (attrib3 & 0x1fffu) + 1u;
|
||||
Require(maxMip == 0, "mipmapped 3D color targets are unsupported");
|
||||
if (maxMip != 0) {
|
||||
char message[384];
|
||||
std::snprintf(message, sizeof(message), "mipmapped 3D color targets are unsupported (slot %u, %ux%ux%u, viewMip %u, maxMip %u, slices %u..%u, CB_COLOR_INFO 0x%08x, ATTRIB2 0x%08x, ATTRIB3 0x%08x, VIEW 0x%08x)", slot, ((attrib2 >> 14u) & 0x3fffu) + 1u, (attrib2 & 0x3fffu) + 1u, color.depth, viewMip, maxMip, slice, lastSlice, info, attrib2, attrib3, view);
|
||||
Require(false, message);
|
||||
}
|
||||
Require(slice < color.depth && lastSlice >= slice, "the color view slices are outside the 3D surface");
|
||||
color.depthSlice = slice;
|
||||
color.layers = std::min(lastSlice + 1u, color.depth) - slice;
|
||||
@@ -1590,8 +1610,9 @@ ColorTarget DecodeColorBuffer(const Registers& cx, std::uint32_t slot, bool allo
|
||||
color.format = decoded.format;
|
||||
color.componentMapping = decoded.componentMapping;
|
||||
color.packing = decoded.packing;
|
||||
Require(color.packing == ShaderRecompiler::ColorExportPacking::None || (info & 0x10000000u) == 0, "DCC-compressed 10_11_11 unorm color targets are unsupported");
|
||||
Require(color.packing == ShaderRecompiler::ColorExportPacking::None || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported");
|
||||
if (color.packing != ShaderRecompiler::ColorExportPacking::None && (info & 0x10000000u) != 0) throw std::runtime_error("AGC graphics: DCC-compressed " + std::string(PackedColorTargetName(color.packing)) + " color targets are unsupported");
|
||||
Require(color.packing != ShaderRecompiler::ColorExportPacking::Unorm10_11_11 || (info & 0x00040000u) == 0, "truncating (ROUND_MODE) 10_11_11 unorm color targets are unsupported");
|
||||
Require(color.packing != ShaderRecompiler::ColorExportPacking::Float11_11_10 || (info & 0x00040000u) != 0, "rounding (ROUND_MODE 0) 11_11_10 float color targets are unsupported");
|
||||
for (std::uint32_t word = 0; word < 2; ++word) {
|
||||
const auto clear = find(cx, 0x323 + word + stride);
|
||||
color.clearWords[word] = clear == cx.end() ? 0u : clear->second;
|
||||
@@ -1690,6 +1711,7 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
|
||||
Require(destination.samples == 1, "CB resolve into a multisampled target is unsupported");
|
||||
Require(destination.cmaskAddress == 0, "CB resolve into a fast-clear color target is unsupported (the resolve does not update its CMASK)");
|
||||
Require(source.format == destination.format && source.extent.width == destination.extent.width && source.extent.height == destination.extent.height, "CB resolve between different formats or extents is unsupported");
|
||||
if (source.packing != ShaderRecompiler::ColorExportPacking::None) throw std::runtime_error("AGC graphics: CB resolves of " + std::string(PackedColorTargetName(source.packing)) + " color targets are unsupported");
|
||||
requireCovered(destination);
|
||||
pass.source = source;
|
||||
pass.targets.push_back(destination);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/SlimMutex.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
@@ -292,7 +293,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
allocation.allocationSize = requirements.size;
|
||||
allocationBytes = requirements.size;
|
||||
allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "texture");
|
||||
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory texture");
|
||||
owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory texture");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory");
|
||||
|
||||
{
|
||||
@@ -583,7 +584,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
||||
allocation.allocationSize = requirements.size;
|
||||
allocationBytes = requirements.size;
|
||||
allocation.memoryTypeIndex = context.MemoryType(requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &owned->memory), "vkAllocateMemory depth copy");
|
||||
owned->memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory depth copy");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, owned->memory, 0), "vkBindImageMemory depth copy");
|
||||
|
||||
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
|
||||
@@ -867,7 +868,13 @@ VkImageView Texture::SrgbView(bool firstLayer, bool baseLevel) const {
|
||||
VkImageView Texture::BaseLevelView() const {
|
||||
Require(baseLevelInfo.has_value(), "a base-level view needs a mipmapped 1D, 2D or 2D array texture");
|
||||
std::lock_guard lock(baseLevelMutex);
|
||||
if (!baseLevelView) Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &*baseLevelInfo, nullptr, &baseLevelView), "vkCreateImageView base level");
|
||||
if (!baseLevelView) {
|
||||
auto info = *baseLevelInfo;
|
||||
VkImageViewUsageCreateInfo usage{VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO};
|
||||
usage.usage = VK_IMAGE_USAGE_SAMPLED_BIT;
|
||||
info.pNext = &usage;
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &info, nullptr, &baseLevelView), "vkCreateImageView base level");
|
||||
}
|
||||
return baseLevelView;
|
||||
}
|
||||
|
||||
@@ -1144,7 +1151,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
|
||||
allocation.allocationSize = requirements.size;
|
||||
memoryBytes = requirements.size;
|
||||
allocation.memoryTypeIndex = context.DeviceMemoryType(requirements.memoryTypeBits, requirements.size, "storage texture");
|
||||
Check(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")(context.device, &allocation, nullptr, &memory), "vkAllocateMemory storage texture");
|
||||
memory = GetBufferPool(context)->AllocateMemory(allocation, "vkAllocateMemory storage texture");
|
||||
Check(context.Function<PFN_vkBindImageMemory>("vkBindImageMemory")(context.device, image, memory, 0), "vkBindImageMemory storage");
|
||||
} catch (...) {
|
||||
release();
|
||||
@@ -3179,6 +3186,12 @@ void StorageTexture::reconcilePending() {
|
||||
BumpPendingSerial();
|
||||
}
|
||||
|
||||
bool StorageTexture::Dirty() const {
|
||||
auto& pending = Pending();
|
||||
std::lock_guard lock(pending.mutex);
|
||||
return dirty;
|
||||
}
|
||||
|
||||
void StorageTexture::Flush() {
|
||||
{
|
||||
auto& live = Live();
|
||||
|
||||
@@ -144,8 +144,8 @@ VkPipeline TextureDetiler::pipeline(TextureTileMode tileMode, std::uint32_t elem
|
||||
// per-bit XOR equation for the equation family (2); 20-21 override the block extent in elements.
|
||||
std::array<std::uint32_t, 22> values{elementBytes, BlockBytesFor(tileMode), tileMode == TextureTileMode::kLinear ? 0u : 1u, retile ? 1u : 0u};
|
||||
if (thick) {
|
||||
const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : tileMode == TextureTileMode::kD64KBX ? 0x11au : 0u;
|
||||
Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_64KB_S, SW_64KB_S_X or SW_64KB_D_X");
|
||||
const auto thickMode = tileMode == TextureTileMode::kStandard4KB ? 0x105u : tileMode == TextureTileMode::kS4KBX ? 0x115u : tileMode == TextureTileMode::kStandard64KB ? 0x109u : tileMode == TextureTileMode::kS64KBX ? 0x119u : tileMode == TextureTileMode::kD64KBX ? 0x11au : 0u;
|
||||
Require(thickMode != 0, "3D textures are only detiled thick from SW_4KB_S, SW_4KB_S_X, SW_64KB_S, SW_64KB_S_X or SW_64KB_D_X");
|
||||
const auto* equation = FindTextureSwizzleEquation(thickMode, elementBytes);
|
||||
Require(equation != nullptr, "no thick swizzle equation for the element size");
|
||||
values[2] = 2u;
|
||||
|
||||
@@ -262,11 +262,12 @@ std::array<std::uint32_t, 3> ThickBlockExtent(TextureTileMode tileMode, std::uin
|
||||
Require(std::has_single_bit(bytesPerElement) && bytesPerElement <= 16u, "unsupported bytes per element for thick texture geometry");
|
||||
const auto index = static_cast<std::size_t>(std::countr_zero(bytesPerElement));
|
||||
switch (tileMode) {
|
||||
case TextureTileMode::kStandard4KB: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]};
|
||||
case TextureTileMode::kStandard4KB:
|
||||
case TextureTileMode::kS4KBX: return {1u << thick4KB[index][0], 1u << thick4KB[index][1], 1u << thick4KB[index][2]};
|
||||
case TextureTileMode::kStandard64KB:
|
||||
case TextureTileMode::kS64KBX:
|
||||
case TextureTileMode::kD64KBX: return {1u << thick64KB[index][0], 1u << thick64KB[index][1], 1u << thick64KB[index][2]};
|
||||
default: throw std::runtime_error("AGC graphics: thick 3D textures are only supported in SW_4KB_S, SW_64KB_S, SW_64KB_S_X and SW_64KB_D_X, not in swizzle mode " + std::to_string(EquationSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element");
|
||||
default: throw std::runtime_error("AGC graphics: thick 3D textures are only supported in SW_4KB_S, SW_4KB_S_X, SW_64KB_S, SW_64KB_S_X and SW_64KB_D_X, not in swizzle mode " + std::to_string(EquationSwizzleMode(tileMode)) + " at " + std::to_string(bytesPerElement) + " bytes per element");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +317,7 @@ ThickLayout ComputeThickLayout(TextureTileMode tileMode, std::uint32_t format, s
|
||||
auto firstTailLevel = mipCount;
|
||||
if (tileMode != TextureTileMode::kLinear) {
|
||||
block = ThickBlockExtent(tileMode, bytesPerElement);
|
||||
blockBytes = tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u;
|
||||
blockBytes = tileMode == TextureTileMode::kStandard4KB || tileMode == TextureTileMode::kS4KBX ? 4096u : 65536u;
|
||||
const auto log2BlockBytes = static_cast<std::uint32_t>(std::countr_zero(blockBytes));
|
||||
const auto tailWidth = log2BlockBytes % 3u == 1u ? block[0] >> 1u : block[0];
|
||||
const auto tailHeight = log2BlockBytes % 3u == 0u ? block[1] >> 1u : block[1];
|
||||
|
||||
@@ -1,10 +1,120 @@
|
||||
#include "BdaShader.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "ControlFlow/RequestSerializer.hpp"
|
||||
#include "SpirvBackend/SpirvBda.hpp"
|
||||
#include <spirv/unified1/GLSL.std.450.h>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
|
||||
namespace {
|
||||
|
||||
struct ScanWords {
|
||||
std::vector<std::uint32_t> words;
|
||||
void Op(spv::Op opcode, std::initializer_list<std::uint32_t> operands) {
|
||||
words.push_back((static_cast<std::uint32_t>(operands.size() + 1u) << 16u) | static_cast<std::uint32_t>(opcode));
|
||||
words.insert(words.end(), operands.begin(), operands.end());
|
||||
}
|
||||
void OpString(spv::Op opcode, std::initializer_list<std::uint32_t> operands, const char* text) {
|
||||
std::vector<std::uint32_t> packed((std::strlen(text) + 4u) / 4u, 0u);
|
||||
std::memcpy(packed.data(), text, std::strlen(text));
|
||||
words.push_back((static_cast<std::uint32_t>(operands.size() + packed.size() + 1u) << 16u) | static_cast<std::uint32_t>(opcode));
|
||||
words.insert(words.end(), operands.begin(), operands.end());
|
||||
words.insert(words.end(), packed.begin(), packed.end());
|
||||
}
|
||||
};
|
||||
|
||||
enum ScanId : std::uint32_t {
|
||||
IdVoid = 1, IdVoidFunction, IdUint, IdUlong, IdUintPointer, IdAddress, IdOne, IdDeviceScope, IdNoSemantics, IdPointerPointer, IdPointerFunction, IdBool, IdTrue, IdFloat, IdFloatPointer, IdGlsl, IdFunctionUintPointer, IdUntypedPointer, IdFloatOne, IdMain, IdMainLabel, IdPointer, IdHelper, IdHelperParameter, IdHelperLabel, IdBody,
|
||||
};
|
||||
|
||||
bool ScanStores(const std::function<void(ScanWords&)>& main, const std::function<void(ScanWords&)>& after = {}) {
|
||||
ScanWords module{{spv::MagicNumber, 0x00010500u, 0u, 200u, 0u}};
|
||||
module.Op(spv::OpCapability, {spv::CapabilityShader});
|
||||
module.Op(spv::OpCapability, {spv::CapabilityInt64});
|
||||
module.Op(spv::OpCapability, {spv::CapabilityPhysicalStorageBufferAddresses});
|
||||
module.OpString(spv::OpExtInstImport, {IdGlsl}, "GLSL.std.450");
|
||||
module.Op(spv::OpMemoryModel, {spv::AddressingModelPhysicalStorageBuffer64, spv::MemoryModelGLSL450});
|
||||
module.OpString(spv::OpEntryPoint, {spv::ExecutionModelGLCompute, IdMain}, "main");
|
||||
module.Op(spv::OpTypeVoid, {IdVoid});
|
||||
module.Op(spv::OpTypeFunction, {IdVoidFunction, IdVoid});
|
||||
module.Op(spv::OpTypeInt, {IdUint, 32u, 0u});
|
||||
module.Op(spv::OpTypeInt, {IdUlong, 64u, 0u});
|
||||
module.Op(spv::OpTypeFloat, {IdFloat, 32u});
|
||||
module.Op(spv::OpTypeBool, {IdBool});
|
||||
module.Op(spv::OpTypePointer, {IdUintPointer, spv::StorageClassPhysicalStorageBuffer, IdUint});
|
||||
module.Op(spv::OpTypePointer, {IdPointerPointer, spv::StorageClassPhysicalStorageBuffer, IdUintPointer});
|
||||
module.Op(spv::OpTypePointer, {IdFloatPointer, spv::StorageClassPhysicalStorageBuffer, IdFloat});
|
||||
module.Op(spv::OpTypePointer, {IdPointerFunction, spv::StorageClassFunction, IdUint});
|
||||
module.Op(spv::OpTypeUntypedPointerKHR, {IdUntypedPointer, spv::StorageClassPhysicalStorageBuffer});
|
||||
module.Op(spv::OpTypeFunction, {IdFunctionUintPointer, IdVoid, IdUintPointer});
|
||||
module.Op(spv::OpConstant, {IdUlong, IdAddress, 0x1000u, 0u});
|
||||
module.Op(spv::OpConstant, {IdUint, IdOne, 1u});
|
||||
module.Op(spv::OpConstant, {IdUint, IdDeviceScope, spv::ScopeDevice});
|
||||
module.Op(spv::OpConstant, {IdUint, IdNoSemantics, 0u});
|
||||
module.Op(spv::OpConstant, {IdFloat, IdFloatOne, 0x3f800000u});
|
||||
module.Op(spv::OpConstantTrue, {IdBool, IdTrue});
|
||||
module.Op(spv::OpFunction, {IdVoid, IdMain, spv::FunctionControlMaskNone, IdVoidFunction});
|
||||
module.Op(spv::OpLabel, {IdMainLabel});
|
||||
module.Op(spv::OpConvertUToPtr, {IdUintPointer, IdPointer, IdAddress});
|
||||
main(module);
|
||||
module.Op(spv::OpReturn, {});
|
||||
module.Op(spv::OpFunctionEnd, {});
|
||||
if (after) after(module);
|
||||
return AgcDriver::Graphics::SpirvStoresThroughAddresses(module.words);
|
||||
}
|
||||
|
||||
void RunAddressStoreScanTests() {
|
||||
using AgcDriver::Graphics::Require;
|
||||
Require(!ScanStores([](ScanWords& m) { m.Op(spv::OpLoad, {IdUint, IdBody, IdPointer}); }), "store scan: a load through a physical pointer was taken for a store");
|
||||
Require(!ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicLoad, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics}); }), "store scan: an atomic load was taken for a store");
|
||||
Require(ScanStores([](ScanWords& m) { m.Op(spv::OpStore, {IdPointer, IdOne}); }), "store scan: a store was missed");
|
||||
Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicStore, {IdPointer, IdDeviceScope, IdNoSemantics, IdOne}); }), "store scan: an atomic store was missed");
|
||||
for (const auto opcode : {spv::OpAtomicExchange, spv::OpAtomicIAdd, spv::OpAtomicISub, spv::OpAtomicSMin, spv::OpAtomicUMin, spv::OpAtomicSMax, spv::OpAtomicUMax, spv::OpAtomicAnd, spv::OpAtomicOr, spv::OpAtomicXor}) {
|
||||
Require(ScanStores([opcode](ScanWords& m) { m.Op(opcode, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics, IdOne}); }), "store scan: atomic opcode " + std::to_string(static_cast<std::uint32_t>(opcode)) + " was missed");
|
||||
}
|
||||
Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicIIncrement, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics}); }), "store scan: an atomic increment was missed");
|
||||
Require(ScanStores([](ScanWords& m) { m.Op(spv::OpAtomicCompareExchange, {IdUint, IdBody, IdPointer, IdDeviceScope, IdNoSemantics, IdNoSemantics, IdOne, IdOne}); }), "store scan: an atomic compare-exchange was missed");
|
||||
Require(ScanStores([](ScanWords& m) {
|
||||
m.Op(spv::OpConvertUToPtr, {IdPointerPointer, IdBody, IdAddress});
|
||||
m.Op(spv::OpLoad, {IdUintPointer, IdBody + 1u, IdBody});
|
||||
m.Op(spv::OpStore, {IdBody + 1u, IdOne});
|
||||
}), "store scan: a store through a pointer loaded from guest memory was missed");
|
||||
Require(ScanStores([](ScanWords& m) {
|
||||
m.Op(spv::OpSelect, {IdUintPointer, IdBody, IdTrue, IdPointer, IdPointer});
|
||||
m.Op(spv::OpStore, {IdBody, IdOne});
|
||||
}), "store scan: a store through a selected pointer was missed");
|
||||
Require(ScanStores([](ScanWords& m) { m.Op(spv::OpFunctionCall, {IdVoid, IdBody, IdHelper, IdPointer}); }, [](ScanWords& m) {
|
||||
m.Op(spv::OpFunction, {IdVoid, IdHelper, spv::FunctionControlMaskNone, IdFunctionUintPointer});
|
||||
m.Op(spv::OpFunctionParameter, {IdUintPointer, IdHelperParameter});
|
||||
m.Op(spv::OpLabel, {IdHelperLabel});
|
||||
m.Op(spv::OpStore, {IdHelperParameter, IdOne});
|
||||
m.Op(spv::OpReturn, {});
|
||||
m.Op(spv::OpFunctionEnd, {});
|
||||
}), "store scan: a store through a pointer parameter of a called function was missed");
|
||||
Require(!ScanStores([](ScanWords& m) {
|
||||
m.Op(spv::OpVariable, {IdPointerFunction, IdBody, spv::StorageClassFunction});
|
||||
m.Op(spv::OpCopyMemory, {IdBody, IdPointer});
|
||||
}), "store scan: a copy out of guest memory was taken for a store");
|
||||
Require(ScanStores([](ScanWords& m) {
|
||||
m.Op(spv::OpVariable, {IdPointerFunction, IdBody, spv::StorageClassFunction});
|
||||
m.Op(spv::OpCopyMemory, {IdPointer, IdBody});
|
||||
}), "store scan: a copy into guest memory was missed");
|
||||
Require(ScanStores([](ScanWords& m) {
|
||||
m.Op(spv::OpConvertUToPtr, {IdUntypedPointer, IdBody, IdAddress});
|
||||
m.Op(spv::OpStore, {IdBody, IdOne});
|
||||
}), "store scan: a store through an untyped physical pointer was missed");
|
||||
Require(ScanStores([](ScanWords& m) {
|
||||
m.Op(spv::OpConvertUToPtr, {IdFloatPointer, IdBody, IdAddress});
|
||||
m.Op(spv::OpExtInst, {IdFloat, IdBody + 1u, IdGlsl, GLSLstd450Modf, IdFloatOne, IdBody});
|
||||
}), "store scan: an extended instruction writing through a physical pointer was missed");
|
||||
Require(ScanStores([](ScanWords& m) { m.Op(spv::OpCooperativeMatrixStoreKHR, {IdPointer, IdOne, IdNoSemantics}); }), "store scan: a cooperative matrix store was missed");
|
||||
}
|
||||
|
||||
|
||||
template<typename TAction>
|
||||
void reject(TAction action, const char* expected) {
|
||||
try { action(); }
|
||||
@@ -19,6 +129,21 @@ void reject(TAction action, const char* expected) {
|
||||
|
||||
void RunBdaContractTests() {
|
||||
using namespace ShaderRecompiler;
|
||||
using AgcDriver::Graphics::SpirvStoresThroughAddresses;
|
||||
RunAddressStoreScanTests();
|
||||
AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaTestShader(0x1000, 32)), "a BDA read was taken for a store through an address");
|
||||
AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(MakeBdaDwordReadTestShader(0x1000, 4, false, false)), "BDA dword reads were taken for a store through an address");
|
||||
AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaStoreTestShader(0x1000, 7, 32, false)), "a BDA store was not seen");
|
||||
AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaWriteTestShader(0x1000, 7, 32, false)), "a BDA write was not seen");
|
||||
const std::array<std::uint32_t, 2> stored{7u, 9u};
|
||||
AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(MakeBdaDwordWriteTestShader(0x1000, static_cast<std::uint32_t>(stored.size()), stored.data())), "BDA dword writes were not seen");
|
||||
const std::array<std::uint32_t, 5> headerOnly{spv::MagicNumber, 0x00010300u, 0u, 1u, 0u};
|
||||
AgcDriver::Graphics::Require(!SpirvStoresThroughAddresses(headerOnly), "a module without physical pointers was taken for one storing through addresses");
|
||||
const std::array<std::uint32_t, 2> truncated{spv::MagicNumber, 0u};
|
||||
AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(truncated), "an unreadable module was taken for one without stores");
|
||||
auto unparsed = MakeBdaStoreTestShader(0x1000, 7, 32, false);
|
||||
unparsed[5] |= 0xffff0000u;
|
||||
AgcDriver::Graphics::Require(SpirvStoresThroughAddresses(unparsed), "a module with an instruction past its end was taken for one without stores");
|
||||
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
SpirvTargetOptions target{0x00401000u, 0x00010300u, 32, BdaAbi::Version, capabilities, extensions};
|
||||
|
||||
@@ -102,6 +102,7 @@ public:
|
||||
context.bufferDeviceAddress = true;
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -550,6 +550,137 @@ void importJoinTests(const Context& context) {
|
||||
::operator delete(block, std::align_val_t{window});
|
||||
}
|
||||
|
||||
class ImportBudgetRanges {
|
||||
public:
|
||||
static constexpr std::size_t piece = 65536;
|
||||
static constexpr std::size_t bytes = 3 * piece;
|
||||
|
||||
explicit ImportBudgetRanges(const Context& context) : importing(context) {
|
||||
block = static_cast<std::byte*>(::operator new(4 * bytes, std::align_val_t{piece}));
|
||||
importing.hostImportAlignment = piece;
|
||||
ClearHostImports(importing.device);
|
||||
SetHostImportWindow(2 * piece);
|
||||
SetHostImportRangeWindow(0);
|
||||
SetHostImportBudget(2 * bytes);
|
||||
GuestAllocations::Mutation mutation;
|
||||
for (std::size_t index = 0; index < 4; ++index) {
|
||||
std::memset(block + index * bytes, static_cast<int>(0x31 + index), bytes);
|
||||
mutation.Add(block + index * bytes, bytes, true, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
~ImportBudgetRanges() {
|
||||
ClearHostImports(importing.device);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
for (std::size_t index = 0; index < 4; ++index) mutation.Remove(block + index * bytes);
|
||||
}
|
||||
SetHostImportBudget(0);
|
||||
SetHostImportRangeWindow(std::uint64_t{64} << 20u);
|
||||
SetHostImportWindow(0);
|
||||
::operator delete(block, std::align_val_t{piece});
|
||||
}
|
||||
|
||||
ImportBudgetRanges(const ImportBudgetRanges&) = delete;
|
||||
ImportBudgetRanges& operator=(const ImportBudgetRanges&) = delete;
|
||||
|
||||
std::uintptr_t Address(std::size_t index) const { return reinterpret_cast<std::uintptr_t>(block + index * bytes); }
|
||||
|
||||
void CheckUnchanged() const {
|
||||
for (std::size_t index = 0; index < 4; ++index) {
|
||||
const auto expected = static_cast<std::byte>(0x31 + index);
|
||||
Require(std::all_of(block + index * bytes, block + (index + 1) * bytes, [expected](std::byte value) { return value == expected; }), "import budget probes changed guest bytes");
|
||||
}
|
||||
}
|
||||
|
||||
Context importing;
|
||||
|
||||
private:
|
||||
std::byte* block = nullptr;
|
||||
};
|
||||
|
||||
void importCoverageHintBudgetTests(const Context& context, const BdaTestAccess& access) {
|
||||
for (const bool acquired : {true, false}) {
|
||||
ImportBudgetRanges ranges(context);
|
||||
const auto a = ranges.Address(0);
|
||||
const auto c = ranges.Address(2);
|
||||
const auto d = ranges.Address(3);
|
||||
const auto* first = HostImportFor(ranges.importing, a, 16);
|
||||
Require(first != nullptr && first->base == a && first->bytes == ranges.bytes, "coverage budget fixture did not import its first range");
|
||||
const auto firstBuffer = first->buffer;
|
||||
const auto firstSerial = HostImportSerial(ranges.importing, a, 16, false);
|
||||
for (int use = 0; use < 300; ++use) Require(HostImportFor(ranges.importing, d, 16) != nullptr, "coverage budget fixture did not import its hot range");
|
||||
const auto hotSerial = HostImportSerial(ranges.importing, d, 16, false);
|
||||
Require(firstSerial != 0 && hotSerial != 0 && HostImportLiveBytes() == 2 * ranges.bytes, "coverage budget fixture has unexpected import ownership or usage");
|
||||
{
|
||||
GuestBufferMemory memory(ranges.importing);
|
||||
memory.AddReadable(c, 32);
|
||||
memory.UploadPrepare(false);
|
||||
if (acquired) {
|
||||
const auto* held = HostImportFor(ranges.importing, a, 16);
|
||||
Require(held != nullptr && held->buffer == firstBuffer, "a genuine import acquisition replaced the retained range");
|
||||
} else {
|
||||
Require(HostImportCovers(ranges.importing, a, 16), "a coverage hint missed the existing range");
|
||||
}
|
||||
memory.UploadFinish(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto view = memory.Descriptor(c, 32, adjustment);
|
||||
Require(view.range == 32 + adjustment, "a disjoint descriptor has an incorrect extent");
|
||||
Require(HostImportSerial(ranges.importing, d, 16, false) == hotSerial && HostImportLiveBytes() == 2 * ranges.bytes, "a coverage probe displaced the hot import or changed the budget");
|
||||
if (acquired) {
|
||||
Require(view.buffer != firstBuffer, "a disjoint descriptor uses the retained range's import");
|
||||
Require(HostImportSerial(ranges.importing, a, 16, false) == firstSerial && HostImportSerial(ranges.importing, c, 16, false) == 0, "a genuine import acquisition did not protect its buffer from eviction");
|
||||
Require(memory.CopiedBytes() == 32, "a disjoint descriptor at the budget was not copied while an import was held");
|
||||
const auto copied = access.bytes(view.buffer);
|
||||
const auto offset = static_cast<std::size_t>(view.offset + adjustment);
|
||||
Require(offset + 32 <= copied.size() && std::memcmp(copied.data() + offset, reinterpret_cast<const void*>(c), 32) == 0, "the descriptor copied behind a held import lost its guest bytes");
|
||||
} else {
|
||||
const auto importedSerial = HostImportSerial(ranges.importing, c, 16, false);
|
||||
Require(HostImportSerial(ranges.importing, a, 16, false) == 0 && importedSerial != 0 && importedSerial != firstSerial, "a coverage hint prevented eviction of an unused import");
|
||||
const auto* imported = ExistingHostImportFor(ranges.importing, c, 32);
|
||||
Require(imported != nullptr && imported->base == c && imported->bytes == ranges.bytes && view.buffer == imported->buffer && view.offset + adjustment == c - imported->base, "a descriptor does not bind the import obtained after a coverage hint");
|
||||
Require(memory.CopiedBytes() == 0, "a coverage hint forced the disjoint imported descriptor to copy");
|
||||
}
|
||||
memory.WriteBack();
|
||||
}
|
||||
ranges.CheckUnchanged();
|
||||
}
|
||||
}
|
||||
|
||||
void failedImportJoinProbeBudgetTests(const Context& context, const BdaTestAccess& access) {
|
||||
ImportBudgetRanges ranges(context);
|
||||
const auto a = ranges.Address(0);
|
||||
const auto crossing = ranges.Address(1) - 16;
|
||||
const auto c = ranges.Address(2);
|
||||
const auto d = ranges.Address(3);
|
||||
const auto* first = HostImportFor(ranges.importing, a, 16);
|
||||
Require(first != nullptr && first->base == a && first->bytes == ranges.bytes, "failed join budget fixture did not import its first range");
|
||||
const auto firstSerial = HostImportSerial(ranges.importing, a, 16, false);
|
||||
for (int use = 0; use < 300; ++use) Require(HostImportFor(ranges.importing, d, 16) != nullptr, "failed join budget fixture did not import its hot range");
|
||||
const auto hotSerial = HostImportSerial(ranges.importing, d, 16, false);
|
||||
Require(firstSerial != 0 && hotSerial != 0 && HostImportLiveBytes() == 2 * ranges.bytes, "failed join budget fixture has unexpected import ownership or usage");
|
||||
{
|
||||
GuestBufferMemory memory(ranges.importing);
|
||||
memory.AddReadable(crossing, 32);
|
||||
memory.AddReadable(c, 32);
|
||||
memory.Upload(false);
|
||||
std::uint32_t adjustment = 0;
|
||||
const auto copiedView = memory.Descriptor(crossing, 32, adjustment);
|
||||
const auto copied = access.bytes(copiedView.buffer);
|
||||
const auto offset = static_cast<std::size_t>(copiedView.offset + adjustment);
|
||||
Require(copiedView.range == 32 + adjustment && offset + 32 <= copied.size() && std::memcmp(copied.data() + offset, reinterpret_cast<const void*>(crossing), 32) == 0, "the refused joined descriptor lost either side's guest bytes");
|
||||
Require(HostImportSerial(ranges.importing, crossing, 32, false) == 0, "a join over the available budget unexpectedly produced an import");
|
||||
const auto importedSerial = HostImportSerial(ranges.importing, c, 16, false);
|
||||
Require(HostImportSerial(ranges.importing, a, 16, false) == 0 && importedSerial != 0 && importedSerial != firstSerial, "a failed join endpoint probe prevented eviction of an unused import");
|
||||
const auto* imported = ExistingHostImportFor(ranges.importing, c, 32);
|
||||
const auto view = memory.Descriptor(c, 32, adjustment);
|
||||
Require(imported != nullptr && imported->base == c && imported->bytes == ranges.bytes && view.buffer == imported->buffer && view.offset + adjustment == c - imported->base && view.range == 32 + adjustment, "the descriptor after a failed join does not bind its complete import");
|
||||
Require(memory.CopiedBytes() == 32 && HostImportLiveBytes() == 2 * ranges.bytes && HostImportSerial(ranges.importing, d, 16, false) == hotSerial, "a failed join forced a second copy or displaced the hot import");
|
||||
memory.WriteBack();
|
||||
}
|
||||
ranges.CheckUnchanged();
|
||||
}
|
||||
|
||||
// One registered range of `cells` import windows of `window` bytes, with the window and the import budget set for the
|
||||
// test and reset after it.
|
||||
class WindowedRange {
|
||||
@@ -1934,6 +2065,8 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
||||
spaceWriteMergedPastImportTests(context, access);
|
||||
spaceReadOverWritableImportTableTests(context);
|
||||
importJoinTests(context);
|
||||
importCoverageHintBudgetTests(context, access);
|
||||
failedImportJoinProbeBudgetTests(context, access);
|
||||
importWholeRangeOverWindowTests(context);
|
||||
importRefusedWholeRangeTests(context);
|
||||
importNeighbouringEdgesTests(context);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "AllocationFailure.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <deque>
|
||||
@@ -50,6 +51,8 @@ struct MockDevice {
|
||||
VkResult pressureResult = VK_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
std::uint64_t attempts = 0;
|
||||
VkResult mapResult = VK_SUCCESS;
|
||||
VkResult bindResult = VK_SUCCESS;
|
||||
unsigned binds = 0;
|
||||
unsigned maps = 0;
|
||||
unsigned unmaps = 0;
|
||||
};
|
||||
@@ -90,6 +93,8 @@ VKAPI_ATTR VkResult VKAPI_CALL mockAllocateMemory(VkDevice, const VkMemoryAlloca
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockBindBufferMemory(VkDevice, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize offset) {
|
||||
++mock.binds;
|
||||
if (mock.bindResult != VK_SUCCESS) return mock.bindResult;
|
||||
if (offset + mock.sizes.at(buffer) > mock.memoryBytes.at(memory)) return VK_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
mock.bound[buffer] = {memory, offset};
|
||||
return VK_SUCCESS;
|
||||
@@ -222,6 +227,31 @@ void SizeClassesAndDirections() {
|
||||
Expect(mock.sizes.at(writeBack.Handle()) == 10 * MiB + 512 * 1024, "a reused device buffer kept the VkBuffer of the earlier size");
|
||||
}
|
||||
|
||||
void SmallDeviceRetention() {
|
||||
mock = MockDevice{};
|
||||
{
|
||||
auto context = mockContext();
|
||||
constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
const auto deviceBudget = BufferPool::DeviceBudget(context.memory);
|
||||
const auto bytes = static_cast<std::size_t>((deviceBudget != 0 ? deviceBudget : 512 * MiB) / 2) + MiB;
|
||||
VkDeviceMemory small = VK_NULL_HANDLE;
|
||||
{
|
||||
DeviceBuffer buffer(context, 256, usage);
|
||||
small = MemoryOf(buffer.Handle());
|
||||
}
|
||||
std::vector<std::unique_ptr<DeviceBuffer>> large;
|
||||
for (int i = 0; i < 2; ++i) large.push_back(std::make_unique<DeviceBuffer>(context, bytes, usage));
|
||||
large.clear();
|
||||
Expect(mock.frees != 0, "large device buffers did not exercise pool budget eviction");
|
||||
const auto made = mock.allocations;
|
||||
const bool isolated = std::getenv("APS5_BUFFER_POOL_SHARED") == nullptr;
|
||||
DeviceBuffer again(context, 256, usage);
|
||||
Expect((MemoryOf(again.Handle()) == small) == isolated, "small device buffer retention did not follow the separate or shared pool budget");
|
||||
Expect(mock.allocations == made + (isolated ? 0 : 1), "large device buffer churn changed small buffer allocation reuse");
|
||||
}
|
||||
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "device pool tier retention leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void LargerClass() {
|
||||
mock = MockDevice{};
|
||||
auto context = mockContext();
|
||||
@@ -568,6 +598,101 @@ void AllocationFailure(VkResult initial, VkResult final, bool cached) {
|
||||
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "failure cleanup leaked a resource");
|
||||
}
|
||||
|
||||
struct ReclaimMock {
|
||||
VkDeviceMemory external = VK_NULL_HANDLE;
|
||||
VkDeviceSize externalBytes = 0;
|
||||
std::uint64_t requests = 0;
|
||||
VkDeviceSize asked = 0;
|
||||
};
|
||||
|
||||
ReclaimMock reclaimMock;
|
||||
|
||||
VkDeviceMemory ExternalAllocation(VkDeviceSize bytes) {
|
||||
VkMemoryAllocateInfo info{VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO};
|
||||
info.allocationSize = bytes;
|
||||
VkDeviceMemory memory = VK_NULL_HANDLE;
|
||||
Expect(mockAllocateMemory(VK_NULL_HANDLE, &info, nullptr, &memory) == VK_SUCCESS, "the external allocation of the reclaim test failed");
|
||||
reclaimMock.external = memory;
|
||||
reclaimMock.externalBytes = bytes;
|
||||
return memory;
|
||||
}
|
||||
|
||||
VkDeviceSize mockCacheReclaim(VkDeviceSize bytes) {
|
||||
++reclaimMock.requests;
|
||||
reclaimMock.asked = bytes;
|
||||
if (reclaimMock.external == VK_NULL_HANDLE) return 0;
|
||||
mockFreeMemory(VK_NULL_HANDLE, reclaimMock.external, nullptr);
|
||||
reclaimMock.external = VK_NULL_HANDLE;
|
||||
return reclaimMock.externalBytes;
|
||||
}
|
||||
|
||||
void CacheReclaimRecovery() {
|
||||
Expect(BufferPool::ReclaimTarget(4 * MiB) == 260 * MiB, "a 4 MiB request does not ask the caches for 256 MiB on top of itself");
|
||||
Expect(BufferPool::ReclaimTarget(512 * MiB) == 1024 * MiB, "a 512 MiB request does not ask the caches for twice its size");
|
||||
constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT;
|
||||
mock = MockDevice{};
|
||||
reclaimMock = ReclaimMock{};
|
||||
{
|
||||
auto context = mockContext();
|
||||
context.cacheReclaim = mockCacheReclaim;
|
||||
{ DeviceBuffer idle(context, 2 * MiB, usage); }
|
||||
ExternalAllocation(10 * MiB);
|
||||
mock.limit = 12 * MiB;
|
||||
const auto before = mock.attempts;
|
||||
try {
|
||||
DeviceBuffer recovered(context, 4 * MiB, usage);
|
||||
Expect(mock.attempts == before + 3, "an allocation the pool's own slots could not satisfy did not retry once after the trim and once after the cache release");
|
||||
Expect(reclaimMock.requests == 1 && reclaimMock.asked == 260 * MiB, "the caches were not asked once for the request plus its reserve");
|
||||
Expect(mock.frees == 2 && mock.liveBytes == 4 * MiB, "the trim or the cache release did not free its memory");
|
||||
} catch (const std::exception& error) {
|
||||
Expect(false, std::string("the cache release did not permit recovery: ") + error.what());
|
||||
}
|
||||
}
|
||||
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "cache release recovery leaked a resource");
|
||||
mock = MockDevice{};
|
||||
reclaimMock = ReclaimMock{};
|
||||
{
|
||||
auto context = mockContext();
|
||||
context.cacheReclaim = mockCacheReclaim;
|
||||
ExternalAllocation(10 * MiB);
|
||||
mock.limit = 12 * MiB;
|
||||
const auto before = mock.attempts;
|
||||
try {
|
||||
DeviceBuffer recovered(context, 4 * MiB, usage);
|
||||
Expect(mock.attempts == before + 2 && reclaimMock.requests == 1, "an empty pool did not go straight to the cache release and retry once");
|
||||
} catch (const std::exception& error) {
|
||||
Expect(false, std::string("the cache release alone did not permit recovery: ") + error.what());
|
||||
}
|
||||
}
|
||||
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "cache release recovery with an empty pool leaked a resource");
|
||||
mock = MockDevice{};
|
||||
reclaimMock = ReclaimMock{};
|
||||
{
|
||||
auto context = mockContext();
|
||||
context.cacheReclaim = mockCacheReclaim;
|
||||
mock.limit = 2 * MiB;
|
||||
const auto before = mock.attempts;
|
||||
std::string failure;
|
||||
try {
|
||||
DeviceBuffer buffer(context, 4 * MiB, usage);
|
||||
} catch (const std::exception& error) {
|
||||
failure = error.what();
|
||||
}
|
||||
Expect(reclaimMock.requests == 1 && mock.attempts == before + 1, "a cache release that freed nothing was retried after");
|
||||
Expect(failure.find("Vulkan result -2") != std::string::npos && failure.find("released=0") != std::string::npos, "the failure after an empty cache release lost its result or the released bytes");
|
||||
mock.results = {VK_ERROR_DEVICE_LOST};
|
||||
mock.limit = 0;
|
||||
failure.clear();
|
||||
try {
|
||||
DeviceBuffer buffer(context, 4 * MiB, usage);
|
||||
} catch (const std::exception& error) {
|
||||
failure = error.what();
|
||||
}
|
||||
Expect(reclaimMock.requests == 1 && failure.find("Vulkan result -4") != std::string::npos, "a device loss asked the caches for memory");
|
||||
}
|
||||
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "the failed cache release leaked a resource");
|
||||
}
|
||||
|
||||
void SuccessfulAllocationKeepsPool() {
|
||||
mock = MockDevice{};
|
||||
{
|
||||
@@ -600,18 +725,55 @@ void PartialSlabDoesNotRetry() {
|
||||
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "partial slab failure leaked a resource");
|
||||
}
|
||||
|
||||
void DeviceBindingFailures() {
|
||||
constexpr auto bytes = 3 * MiB;
|
||||
constexpr auto usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT;
|
||||
for (const auto result : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY}) {
|
||||
mock = MockDevice{};
|
||||
{
|
||||
auto context = mockContext();
|
||||
mock.bindResult = result;
|
||||
bool threw = false;
|
||||
try {
|
||||
DeviceBuffer failed(context, bytes, usage);
|
||||
} catch (const std::runtime_error& error) {
|
||||
threw = std::string(error.what()).find("Vulkan result " + std::to_string(result)) != std::string::npos;
|
||||
}
|
||||
const auto retained = mock.liveBytes;
|
||||
Expect(threw && mock.binds == 1, "a failed device buffer bind lost its error");
|
||||
Expect(retained == 0 && mock.liveBuffers.empty(), "a failed device buffer was retained for reuse");
|
||||
mock.bindResult = VK_SUCCESS;
|
||||
unsigned invalid = 0;
|
||||
for (unsigned retry = 0; retry < 3; ++retry) {
|
||||
DeviceBuffer recovered(context, bytes, usage);
|
||||
if (!mock.bound.contains(recovered.Handle())) ++invalid;
|
||||
}
|
||||
Expect(invalid == 0, "a retry reused an unbound device buffer");
|
||||
Expect(mock.binds == 2 && mock.allocations == 2, "successfully bound device buffers stopped being reused");
|
||||
std::printf("device bind result=%d retained=%llu invalid retries=%u binds=%u\n", static_cast<int>(result), static_cast<unsigned long long>(retained), invalid, mock.binds);
|
||||
}
|
||||
Expect(mock.liveBytes == 0 && mock.liveBuffers.empty(), "device bind recovery leaked a resource");
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--small-device-retention") {
|
||||
SmallDeviceRetention();
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
SizeClassesAndDirections();
|
||||
LargerClass();
|
||||
KeptUntilFence();
|
||||
SmallDeviceRetention();
|
||||
Budget();
|
||||
AddressAndHostUnchanged();
|
||||
SmallDeviceClasses();
|
||||
SlabMetadataFailure();
|
||||
SlabMapFailure();
|
||||
DeviceBindingFailures();
|
||||
Slabs();
|
||||
for (const auto result : {VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_OUT_OF_HOST_MEMORY}) {
|
||||
AllocationRecovery(result, false);
|
||||
@@ -625,6 +787,7 @@ int main() {
|
||||
AllocationFailure(VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_DEVICE_LOST, true);
|
||||
SuccessfulAllocationKeepsPool();
|
||||
PartialSlabDoesNotRetry();
|
||||
CacheReclaimRecovery();
|
||||
HostPlacement();
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", error.what());
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
#include "CommandBatchFailure.hpp"
|
||||
#include "AllocationFailure.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
|
||||
int main() {
|
||||
try {
|
||||
AgcDriver::Graphics::Context context{};
|
||||
context.device = reinterpret_cast<VkDevice>(std::uintptr_t{1});
|
||||
context.queue = reinterpret_cast<VkQueue>(std::uintptr_t{2});
|
||||
context.pool = reinterpret_cast<VkCommandPool>(std::uintptr_t{3});
|
||||
AgcDriver::Tests::RunCommandBatchFailureTests(context, false);
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
AgcDriver::Tests::SetAllocationFailureCountdown(-1);
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "AllocationFailure.hpp"
|
||||
#include "CommandBatchFailure.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace AgcDriver::Graphics;
|
||||
using AgcDriver::Tests::SetAllocationFailureCountdown;
|
||||
|
||||
enum class Fault { None, Timing, End, Submit };
|
||||
|
||||
struct Probe {
|
||||
PFN_vkGetDeviceProcAddr real = nullptr;
|
||||
Fault fault = Fault::None;
|
||||
VkFence fence = VK_NULL_HANDLE;
|
||||
bool pending = false;
|
||||
unsigned submitted = 0;
|
||||
unsigned queries = 0;
|
||||
unsigned idleWaits = 0;
|
||||
unsigned fenceWaits = 0;
|
||||
unsigned premature = 0;
|
||||
unsigned safetyWaits = 0;
|
||||
unsigned nativePending = 0;
|
||||
unsigned freed = 0;
|
||||
unsigned destroyed = 0;
|
||||
};
|
||||
|
||||
Probe probe;
|
||||
VkDevice activeDevice = VK_NULL_HANDLE;
|
||||
VkQueue activeQueue = VK_NULL_HANDLE;
|
||||
|
||||
void GuardRelease(VkDevice device, VkQueue queue) {
|
||||
if (!probe.pending) return;
|
||||
++probe.premature;
|
||||
++probe.safetyWaits;
|
||||
if (probe.real != nullptr) {
|
||||
const auto status = reinterpret_cast<PFN_vkGetFenceStatus>(probe.real(device, "vkGetFenceStatus"))(device, probe.fence);
|
||||
if (status == VK_NOT_READY) ++probe.nativePending;
|
||||
else if (status != VK_SUCCESS) std::terminate();
|
||||
const auto wait = reinterpret_cast<PFN_vkQueueWaitIdle>(probe.real(device, "vkQueueWaitIdle"));
|
||||
if (wait(queue) != VK_SUCCESS) std::terminate();
|
||||
}
|
||||
probe.pending = false;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL AllocateCommands(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) {
|
||||
*commands = reinterpret_cast<VkCommandBuffer>(std::uintptr_t{4});
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL CreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) {
|
||||
*fence = reinterpret_cast<VkFence>(std::uintptr_t{5});
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL BeginCommands(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return VK_SUCCESS; }
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL EndCommands(VkCommandBuffer commands) {
|
||||
if (probe.fault == Fault::End) return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
if (probe.real == nullptr) return VK_SUCCESS;
|
||||
return reinterpret_cast<PFN_vkEndCommandBuffer>(probe.real(activeDevice, "vkEndCommandBuffer"))(commands);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL Submit(VkQueue queue, std::uint32_t count, const VkSubmitInfo* submissions, VkFence fence) {
|
||||
if (probe.fault == Fault::Submit) return VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
const auto result = probe.real != nullptr ? reinterpret_cast<PFN_vkQueueSubmit>(probe.real(activeDevice, "vkQueueSubmit"))(queue, count, submissions, fence) : VK_SUCCESS;
|
||||
if (result == VK_SUCCESS) {
|
||||
++probe.submitted;
|
||||
probe.pending = true;
|
||||
probe.fence = fence;
|
||||
if (probe.fault == Fault::Timing) SetAllocationFailureCountdown(0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL FenceStatus(VkDevice device, VkFence fence) {
|
||||
++probe.queries;
|
||||
const auto result = probe.real != nullptr ? reinterpret_cast<PFN_vkGetFenceStatus>(probe.real(device, "vkGetFenceStatus"))(device, fence) : VK_NOT_READY;
|
||||
if (result == VK_SUCCESS) probe.pending = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL QueueIdle(VkQueue queue) {
|
||||
++probe.idleWaits;
|
||||
const auto result = probe.real != nullptr ? reinterpret_cast<PFN_vkQueueWaitIdle>(probe.real(activeDevice, "vkQueueWaitIdle"))(queue) : VK_SUCCESS;
|
||||
if (result == VK_SUCCESS) probe.pending = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL WaitFences(VkDevice device, std::uint32_t count, const VkFence* fences, VkBool32 all, std::uint64_t timeout) {
|
||||
++probe.fenceWaits;
|
||||
const auto result = probe.real != nullptr ? reinterpret_cast<PFN_vkWaitForFences>(probe.real(device, "vkWaitForFences"))(device, count, fences, all, timeout) : VK_SUCCESS;
|
||||
if (result == VK_SUCCESS) probe.pending = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL FreeCommands(VkDevice device, VkCommandPool pool, std::uint32_t count, const VkCommandBuffer* commands) {
|
||||
GuardRelease(device, activeQueue);
|
||||
probe.freed += count;
|
||||
if (probe.real != nullptr) reinterpret_cast<PFN_vkFreeCommandBuffers>(probe.real(device, "vkFreeCommandBuffers"))(device, pool, count, commands);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL DestroyFence(VkDevice device, VkFence fence, const VkAllocationCallbacks* allocator) {
|
||||
GuardRelease(device, activeQueue);
|
||||
++probe.destroyed;
|
||||
if (probe.real != nullptr) reinterpret_cast<PFN_vkDestroyFence>(probe.real(device, "vkDestroyFence"))(device, fence, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL DeviceProc(VkDevice device, const char* name) {
|
||||
if (std::strcmp(name, "vkEndCommandBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(EndCommands);
|
||||
if (std::strcmp(name, "vkQueueSubmit") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Submit);
|
||||
if (std::strcmp(name, "vkGetFenceStatus") == 0) return reinterpret_cast<PFN_vkVoidFunction>(FenceStatus);
|
||||
if (std::strcmp(name, "vkQueueWaitIdle") == 0) return reinterpret_cast<PFN_vkVoidFunction>(QueueIdle);
|
||||
if (std::strcmp(name, "vkWaitForFences") == 0) return reinterpret_cast<PFN_vkVoidFunction>(WaitFences);
|
||||
if (std::strcmp(name, "vkFreeCommandBuffers") == 0) return reinterpret_cast<PFN_vkVoidFunction>(FreeCommands);
|
||||
if (std::strcmp(name, "vkDestroyFence") == 0) return reinterpret_cast<PFN_vkVoidFunction>(DestroyFence);
|
||||
if (probe.real != nullptr) return probe.real(device, name);
|
||||
if (std::strcmp(name, "vkAllocateCommandBuffers") == 0) return reinterpret_cast<PFN_vkVoidFunction>(AllocateCommands);
|
||||
if (std::strcmp(name, "vkCreateFence") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CreateFence);
|
||||
if (std::strcmp(name, "vkBeginCommandBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(BeginCommands);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool RunCase(const Context& original, Fault fault, bool wait, bool native) {
|
||||
probe = {};
|
||||
probe.real = native ? original.deviceProc : nullptr;
|
||||
probe.fault = fault;
|
||||
activeDevice = original.device;
|
||||
activeQueue = original.queue;
|
||||
auto context = original;
|
||||
context.deviceProc = DeviceProc;
|
||||
std::unique_ptr<Buffer> output;
|
||||
constexpr std::size_t bytes = 256;
|
||||
constexpr std::uint32_t pattern = 0x5a3c71e9u;
|
||||
if (native) {
|
||||
output = std::make_unique<Buffer>(context, bytes, VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
std::memset(output->Bytes().data(), 0, bytes);
|
||||
}
|
||||
bool allocationFailed = false;
|
||||
bool apiFailed = false;
|
||||
bool waitFailed = false;
|
||||
{
|
||||
AgcDriver::FrameTiming frame(1, true);
|
||||
AgcDriver::PerformanceContext timing(&frame);
|
||||
try {
|
||||
CommandBatch batch(context);
|
||||
if (output) {
|
||||
context.Function<PFN_vkCmdFillBuffer>("vkCmdFillBuffer")(batch.Handle(), output->Handle(), 0, bytes, pattern);
|
||||
RecordMemoryBarrier(context, batch.Handle(), VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
}
|
||||
try {
|
||||
batch.Submit();
|
||||
} catch (const std::bad_alloc&) {
|
||||
if (!wait) throw;
|
||||
allocationFailed = true;
|
||||
}
|
||||
SetAllocationFailureCountdown(-1);
|
||||
if (wait && probe.submitted != 0) {
|
||||
try {
|
||||
batch.Wait();
|
||||
} catch (const std::exception&) {
|
||||
waitFailed = true;
|
||||
}
|
||||
}
|
||||
} catch (const std::bad_alloc&) {
|
||||
allocationFailed = true;
|
||||
} catch (const std::runtime_error& error) {
|
||||
apiFailed = std::string(error.what()).find(fault == Fault::End ? "vkEndCommandBuffer" : "vkQueueSubmit graphics") != std::string::npos;
|
||||
}
|
||||
SetAllocationFailureCountdown(-1);
|
||||
}
|
||||
bool pixels = true;
|
||||
if (output && probe.submitted != 0) {
|
||||
output->Invalidate();
|
||||
const auto* words = reinterpret_cast<const std::uint32_t*>(output->Bytes().data());
|
||||
pixels = std::all_of(words, words + bytes / sizeof(*words), [](auto word) { return word == pattern; });
|
||||
}
|
||||
const bool rejected = fault == Fault::End || fault == Fault::Submit;
|
||||
const bool complete = rejected ? probe.queries == 0 && probe.idleWaits == 0 && probe.fenceWaits == 0 : wait ? probe.fenceWaits == 1 : probe.queries == 1;
|
||||
const bool passed = allocationFailed == (fault == Fault::Timing) && apiFailed == rejected && !waitFailed && !probe.pending && probe.submitted == (rejected ? 0u : 1u) && probe.premature == 0 && probe.freed == 1 && probe.destroyed == 1 && complete && pixels;
|
||||
std::printf("command batch fault=%u wait=%u allocation failed=%u submitted=%u premature=%u safety waits=%u queries=%u idle waits=%u fence waits=%u readbacks=%u correct=%u wait failed=%u native pending=%u\n", static_cast<unsigned>(fault), wait, allocationFailed, probe.submitted, probe.premature, probe.safetyWaits, probe.queries, probe.idleWaits, probe.fenceWaits, output && probe.submitted != 0 ? 1u : 0u, pixels, waitFailed, probe.nativePending);
|
||||
return passed;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void AgcDriver::Tests::RunCommandBatchFailureTests(const AgcDriver::Graphics::Context& context, bool native) {
|
||||
unsigned failures = 0;
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
for (const auto fault : {Fault::Timing, Fault::None}) {
|
||||
for (const auto wait : {false, true}) if (!RunCase(context, fault, wait, native)) ++failures;
|
||||
}
|
||||
for (const auto fault : {Fault::End, Fault::Submit}) if (!RunCase(context, fault, false, native)) ++failures;
|
||||
Require(failures == 0, "command batch failure handling released pending work or lost its submission state");
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_COMMANDBATCHFAILURE_HPP
|
||||
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_COMMANDBATCHFAILURE_HPP
|
||||
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
|
||||
|
||||
namespace AgcDriver::Tests {
|
||||
|
||||
void RunCommandBatchFailureTests(const Graphics::Context& context, bool native);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "Optimization/ConstantFolder.hpp"
|
||||
#include "Optimization/DeadCodeEliminator.hpp"
|
||||
#include <cstdio>
|
||||
#include <pthread.h>
|
||||
#include <string>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
@@ -77,11 +79,54 @@ void CheckDiamond(unsigned mode) {
|
||||
}
|
||||
}
|
||||
|
||||
void CheckLongAncillaryForwarding() {
|
||||
IrProgram program;
|
||||
auto& block = program.CreateBlock();
|
||||
program.BlockOrder().push_back(&block);
|
||||
BlockInfo info;
|
||||
info.id = 0u;
|
||||
info.terminator.kind = TerminatorKind::Return;
|
||||
program.Metadata().blockInfo.push_back(info);
|
||||
program.SetEntryBlock(block);
|
||||
IrBuilder builder(program);
|
||||
builder.SetInsertionPoint(block);
|
||||
auto& ancillary = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast<std::uint32_t>(StageInputKind::PackedAncillary)), &builder.Constant(0u)});
|
||||
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
|
||||
auto& condition = builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {&lane, &builder.Constant(16u)});
|
||||
auto& zero = builder.Constant(0u);
|
||||
IrValue* forwarded = &ancillary;
|
||||
for (std::uint32_t link = 0; link < 20000u; ++link) forwarded = &builder.Emit(IrOpcode::SelectU32, IrType::U32, {&condition, &zero, forwarded});
|
||||
auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {forwarded, &builder.Constant(8u), &builder.Constant(4u)});
|
||||
builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&field});
|
||||
struct Run {
|
||||
IrProgram& program;
|
||||
std::string error;
|
||||
} run{program, {}};
|
||||
pthread_attr_t attributes;
|
||||
pthread_attr_init(&attributes);
|
||||
pthread_attr_setstacksize(&attributes, 256u * 1024u);
|
||||
pthread_t thread;
|
||||
const auto body = +[](void* argument) -> void* {
|
||||
auto& state = *static_cast<Run*>(argument);
|
||||
try {
|
||||
ConstantFolder{}.Fold(state.program);
|
||||
} catch (const std::exception& error) {
|
||||
state.error = error.what();
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
Require(pthread_create(&thread, &attributes, body, &run) == 0, "long ancillary forwarding: cannot start the folding thread");
|
||||
pthread_join(thread, nullptr);
|
||||
pthread_attr_destroy(&attributes);
|
||||
Require(run.error.empty(), "long ancillary forwarding: folding failed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
for (unsigned mode = 0; mode < 5u; ++mode) CheckDiamond(mode);
|
||||
CheckLongAncillaryForwarding();
|
||||
std::puts("constant phi dominance tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#include "IntermediateRepresentation/IrBuilder.hpp"
|
||||
#include "Optimization/ConstantFolder.hpp"
|
||||
#include "Optimization/DeadCodeEliminator.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
namespace {
|
||||
|
||||
void Require(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct Program {
|
||||
IrProgram program;
|
||||
IrBuilder builder{program};
|
||||
IrValue* lane = nullptr;
|
||||
IrValue* condition = nullptr;
|
||||
IrValue* other = nullptr;
|
||||
std::uint32_t values = 0;
|
||||
|
||||
Program() {
|
||||
auto& block = program.CreateBlock();
|
||||
program.BlockOrder().push_back(&block);
|
||||
BlockInfo info;
|
||||
info.id = 0;
|
||||
info.terminator.kind = TerminatorKind::Return;
|
||||
program.Metadata().blockInfo.push_back(info);
|
||||
program.SetEntryBlock(block);
|
||||
builder.SetInsertionPoint(block);
|
||||
lane = &builder.Emit(IrOpcode::LaneId, IrType::U32, {});
|
||||
condition = &builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {lane, &builder.Constant(16u)});
|
||||
other = &builder.Emit(IrOpcode::ULessThan32, IrType::Bool, {lane, &builder.Constant(8u)});
|
||||
}
|
||||
|
||||
IrValue& value() {
|
||||
return builder.Emit(IrOpcode::IAdd32, IrType::U32, {lane, &builder.Constant(++values)});
|
||||
}
|
||||
|
||||
IrValue& select(IrValue& selector, IrValue& onTrue, IrValue& onFalse) {
|
||||
return builder.Emit(IrOpcode::SelectU32, IrType::U32, {&selector, &onTrue, &onFalse});
|
||||
}
|
||||
|
||||
const IrValue* folded(IrValue& result) {
|
||||
auto& reference = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&result});
|
||||
ValidateProgram(program, true);
|
||||
ConstantFolder{}.Fold(program);
|
||||
DeadCodeEliminator{}.RemoveIdentities(program);
|
||||
DeadCodeEliminator{}.Eliminate(program);
|
||||
ValidateProgram(program, true);
|
||||
return reference.Argument(0)->Resolve();
|
||||
}
|
||||
};
|
||||
|
||||
bool Selects(const IrValue* value, const IrValue* selector, const IrValue* onTrue, const IrValue* onFalse) {
|
||||
return value->Opcode() == IrOpcode::SelectU32 && value->Argument(0)->Resolve() == selector && value->Argument(1)->Resolve() == onTrue && value->Argument(2)->Resolve() == onFalse;
|
||||
}
|
||||
|
||||
void CheckFalseArm() {
|
||||
Program p;
|
||||
auto& kept = p.value();
|
||||
auto& dropped = p.value();
|
||||
auto& fallback = p.value();
|
||||
auto& inner = p.select(*p.condition, dropped, fallback);
|
||||
const auto* result = p.folded(p.select(*p.condition, kept, inner));
|
||||
Require(Selects(result, p.condition, &kept, &fallback), "a select in the false arm of a select on the same condition was not folded to its false arm");
|
||||
Require(!inner.HasUses(), "the folded inner select is still used");
|
||||
}
|
||||
|
||||
void CheckTrueArm() {
|
||||
Program p;
|
||||
auto& kept = p.value();
|
||||
auto& dropped = p.value();
|
||||
auto& fallback = p.value();
|
||||
const auto* result = p.folded(p.select(*p.condition, p.select(*p.condition, kept, dropped), fallback));
|
||||
Require(Selects(result, p.condition, &kept, &fallback), "a select in the true arm of a select on the same condition was not folded to its true arm");
|
||||
}
|
||||
|
||||
void CheckChain() {
|
||||
Program p;
|
||||
auto& last = p.value();
|
||||
auto& fallback = p.value();
|
||||
auto* chain = &p.select(*p.condition, p.value(), fallback);
|
||||
chain = &p.select(*p.condition, p.value(), *chain);
|
||||
const auto* result = p.folded(p.select(*p.condition, last, *chain));
|
||||
Require(Selects(result, p.condition, &last, &fallback), "a chain of selects on one condition did not fold to the last write and the fallback");
|
||||
}
|
||||
|
||||
void CheckEqualArms() {
|
||||
Program p;
|
||||
auto& same = p.value();
|
||||
const auto* result = p.folded(p.select(*p.condition, same, p.select(*p.condition, p.value(), same)));
|
||||
Require(result == &same, "a select whose arms become equal after folding was not replaced by that value");
|
||||
}
|
||||
|
||||
void CheckOtherCondition() {
|
||||
Program p;
|
||||
auto& kept = p.value();
|
||||
auto& inner = p.select(*p.other, p.value(), p.value());
|
||||
const auto* result = p.folded(p.select(*p.condition, kept, inner));
|
||||
Require(Selects(result, p.condition, &kept, &inner), "a select nested under a different condition was folded");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
CheckFalseArm();
|
||||
CheckTrueArm();
|
||||
CheckChain();
|
||||
CheckEqualArms();
|
||||
CheckOtherCondition();
|
||||
std::puts("constant select tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
#include "IntermediateRepresentation/IrProgram.hpp"
|
||||
#include "Optimization/DenormalFlushEliminator.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <set>
|
||||
#include <span>
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
@@ -80,6 +89,46 @@ bool run(const char* name, const std::function<bool()>& test) {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Recompiled(std::span<const std::uint32_t> code) {
|
||||
const std::array<std::uint32_t, 4> userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu};
|
||||
const std::array<std::uint32_t, 2> capabilities{1u, 61u};
|
||||
const std::array<std::string_view, 1> extensions{"SPV_KHR_storage_buffer_storage_class"};
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}};
|
||||
request.context.waveSize = 32;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00403000u;
|
||||
request.target.spirvVersion = 0x00010600u;
|
||||
request.target.subgroupSize = 32u;
|
||||
request.target.bdaAbiVersion = 1;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
|
||||
request.target.maxWorkgroupInvocations = 1024;
|
||||
request.target.maxWorkgroupSharedMemoryBytes = 49152;
|
||||
request.layout = {0, 0, 0, 128};
|
||||
request.useCache = false;
|
||||
const auto& words = Recompile(request).spirv.Words();
|
||||
return {words.begin(), words.end()};
|
||||
}
|
||||
|
||||
std::size_t ExponentMasks(std::span<const std::uint32_t> words) {
|
||||
if (words.size() < 5 || words[0] != spv::MagicNumber) throw std::runtime_error("the recompiler returned no SPIR-V module");
|
||||
std::set<std::uint32_t> exponents;
|
||||
std::size_t masks = 0;
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> spv::WordCountShift;
|
||||
if (count == 0 || count > words.size() - offset) throw std::runtime_error("the recompiler returned a malformed SPIR-V instruction");
|
||||
const auto opcode = words[offset] & spv::OpCodeMask;
|
||||
if (opcode == spv::OpConstant && count == 4 && words[offset + 3] == 0x7f800000u) exponents.insert(words[offset + 2]);
|
||||
if (opcode == spv::OpBitwiseAnd && count == 5 && (exponents.contains(words[offset + 3]) || exponents.contains(words[offset + 4]))) ++masks;
|
||||
offset += count;
|
||||
}
|
||||
return masks;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -252,5 +301,79 @@ int main() {
|
||||
return loop(true) && !loop(false);
|
||||
});
|
||||
|
||||
passed &= run("a product behind its round-to-nearest tiny-product guard needs no flush, one behind another guard does", [] {
|
||||
Builder b;
|
||||
const auto value = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&bits}); };
|
||||
const auto bitsOf = [&](IrValue& number) -> IrValue& { return b.emit(IrOpcode::BitCastU32F32, IrType::U32, {&number}); };
|
||||
const auto guarded = [&](IrValue& lhs, IrValue& rhs, IrValue& guardLhs, IrValue& guardRhs, std::uint32_t threshold) -> IrValue& {
|
||||
auto& product = b.emit(IrOpcode::FPMul32, IrType::F32, {&lhs, &rhs});
|
||||
auto& scaled = b.emit(IrOpcode::FPMul32, IrType::F32, {&b.emit(IrOpcode::FPMul32, IrType::F32, {&guardLhs, &b.constant(0x5f800000u)}), &guardRhs});
|
||||
auto& magnitude = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bitsOf(scaled), &b.constant(0x7fffffffu)});
|
||||
auto& tiny = b.emit(IrOpcode::ULessThan32, IrType::Bool, {&b.emit(IrOpcode::ISub32, IrType::U32, {&magnitude, &b.constant(1u)}), &b.constant(threshold)});
|
||||
auto& sign = b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.emit(IrOpcode::BitwiseXor32, IrType::U32, {&bitsOf(lhs), &bitsOf(rhs)}), &b.constant(0x80000000u)});
|
||||
return b.flush(bitsOf(b.emit(IrOpcode::SelectF32, IrType::F32, {&tiny, &value(sign), &product})));
|
||||
};
|
||||
auto& x = value(b.flush(b.unknown()));
|
||||
auto& y = value(b.flush(b.unknown()));
|
||||
auto& raw = value(b.unknown());
|
||||
IrValue* removed[] = {&guarded(x, y, x, y, 0x207fffffu), &guarded(x, y, y, x, 0x207fffffu)};
|
||||
IrValue* kept[] = {&guarded(x, y, x, raw, 0x207fffffu), &guarded(x, y, x, y, 0x20800000u), &guarded(x, raw, x, raw, 0x207fffffu)};
|
||||
for (auto* flushed : removed) b.keep(*flushed);
|
||||
for (auto* flushed : kept) b.keep(*flushed);
|
||||
const bool counted = b.eliminate() == 2u;
|
||||
bool flushes = true;
|
||||
for (auto* flushed : removed) flushes &= !flushed->HasUses();
|
||||
for (auto* flushed : kept) flushes &= flushed->HasUses();
|
||||
return counted && flushes;
|
||||
});
|
||||
|
||||
passed &= run("an arm a select takes only when its bits are NaN needs no flush, one under another value's NaN test does", [] {
|
||||
Builder b;
|
||||
auto& x = b.unknown();
|
||||
auto& other = b.unknown();
|
||||
const auto magnitude = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&bits, &b.constant(0x7fffffffu)}); };
|
||||
const auto isNan = [&](IrValue& bits) -> IrValue& { return b.emit(IrOpcode::UGreaterThan32, IrType::Bool, {&magnitude(bits), &b.constant(0x7f800000u)}); };
|
||||
const auto chosen = [&](IrValue& condition, IrValue& arm) -> IrValue& { return b.flush(b.emit(IrOpcode::SelectU32, IrType::U32, {&condition, &arm, &b.flush(b.unknown())})); };
|
||||
IrValue* removed[] = {
|
||||
&chosen(isNan(x), x),
|
||||
&chosen(isNan(x), b.emit(IrOpcode::BitwiseOr32, IrType::U32, {&x, &b.constant(0x00400000u)})),
|
||||
&chosen(b.emit(IrOpcode::FPIsNan32, IrType::Bool, {&b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&x})}), x),
|
||||
};
|
||||
IrValue* kept[] = {
|
||||
&chosen(isNan(other), x),
|
||||
&chosen(b.emit(IrOpcode::UGreaterThan32, IrType::Bool, {&b.constant(0x7f800000u), &magnitude(x)}), x),
|
||||
};
|
||||
for (auto* flushed : removed) b.keep(*flushed);
|
||||
for (auto* flushed : kept) b.keep(*flushed);
|
||||
const bool counted = b.eliminate() == 3u;
|
||||
bool flushes = true;
|
||||
for (auto* flushed : removed) flushes &= !flushed->HasUses();
|
||||
for (auto* flushed : kept) flushes &= flushed->HasUses();
|
||||
return counted && flushes;
|
||||
});
|
||||
|
||||
passed &= run("the sign or exponent bits of any value need no flush, a mask that keeps significand bits does", [] {
|
||||
Builder b;
|
||||
auto& sign = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.unknown(), &b.constant(0x80000000u)}));
|
||||
auto& exponent = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.constant(0xff800000u), &b.unknown()}));
|
||||
auto& significand = b.flush(b.emit(IrOpcode::BitwiseAnd32, IrType::U32, {&b.unknown(), &b.constant(0x807fffffu)}));
|
||||
b.keep(sign);
|
||||
b.keep(exponent);
|
||||
b.keep(significand);
|
||||
return b.eliminate() == 2u && !sign.HasUses() && !exponent.HasUses() && significand.HasUses();
|
||||
});
|
||||
|
||||
passed &= run("two chained v_mul_f32 keep only the input flush of the first source", [] {
|
||||
const std::array<std::uint32_t, 5> code{
|
||||
0x10020100u,
|
||||
0x10040301u,
|
||||
0xe0700000u, 0x80000200u,
|
||||
0xbf810000u,
|
||||
};
|
||||
const auto masks = ExponentMasks(Recompiled(code));
|
||||
if (masks != 1u) std::fprintf(stderr, " %zu f32 exponent masks, expected 1\n", masks);
|
||||
return masks == 1u;
|
||||
});
|
||||
|
||||
return passed ? 0 : 1;
|
||||
}
|
||||
@@ -458,8 +458,8 @@ void RunDepthSurfaceReuseTests() {
|
||||
const auto otherWithA = pairing(otherDepth, stencilA);
|
||||
Require(imageCopies.size() == 1 && imageCopies[0] == std::make_tuple(withA, otherWithA, static_cast<VkImageAspectFlags>(VK_IMAGE_ASPECT_STENCIL_BIT)), "a stencil plane paired with another depth plane must carry its stencil over");
|
||||
{
|
||||
constexpr std::uint64_t arrayBase = 0x80000000;
|
||||
constexpr std::uint64_t htile = 0x81000000;
|
||||
constexpr std::uint64_t arrayBase = 0x30000000;
|
||||
constexpr std::uint64_t htile = 0x31000000;
|
||||
constexpr VkExtent2D extent{1024, 512};
|
||||
const auto sliceHtile = HtileMetadataBytes(extent);
|
||||
DepthTarget first{arrayBase, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0};
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
namespace {
|
||||
thread_local bool failNextAllocation = false;
|
||||
}
|
||||
|
||||
void* operator new(std::size_t size) {
|
||||
if (failNextAllocation) {
|
||||
failNextAllocation = false;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
if (void* memory = std::malloc(size == 0 ? 1 : size)) return memory;
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
|
||||
void* operator new[](std::size_t size) { return ::operator new(size); }
|
||||
void operator delete(void* memory) noexcept { std::free(memory); }
|
||||
void operator delete[](void* memory) noexcept { std::free(memory); }
|
||||
void operator delete(void* memory, std::size_t) noexcept { std::free(memory); }
|
||||
void operator delete[](void* memory, std::size_t) noexcept { std::free(memory); }
|
||||
|
||||
namespace {
|
||||
using namespace AgcDriver::Graphics;
|
||||
|
||||
struct VulkanObjects {
|
||||
std::uintptr_t next = 1;
|
||||
std::uint32_t layouts = 0;
|
||||
std::uint32_t pools = 0;
|
||||
std::uint32_t createdLayouts = 0;
|
||||
std::uint32_t createdPools = 0;
|
||||
bool failAfterLayout = false;
|
||||
bool failAfterPool = false;
|
||||
std::array<bool, 32> poolUsed{};
|
||||
} objects;
|
||||
|
||||
void RequireTest(bool condition, const char* message) {
|
||||
if (condition) return;
|
||||
std::fprintf(stderr, "%s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL CreateLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo*, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) {
|
||||
*layout = reinterpret_cast<VkDescriptorSetLayout>(objects.next++);
|
||||
++objects.layouts;
|
||||
++objects.createdLayouts;
|
||||
failNextAllocation = objects.failAfterLayout;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL DestroyLayout(VkDevice, VkDescriptorSetLayout, const VkAllocationCallbacks*) {
|
||||
RequireTest(objects.layouts != 0, "layout destroyed twice");
|
||||
--objects.layouts;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL CreatePool(VkDevice, const VkDescriptorPoolCreateInfo*, const VkAllocationCallbacks*, VkDescriptorPool* pool) {
|
||||
RequireTest(objects.next < objects.poolUsed.size(), "mock pool capacity exceeded");
|
||||
*pool = reinterpret_cast<VkDescriptorPool>(objects.next++);
|
||||
++objects.pools;
|
||||
++objects.createdPools;
|
||||
failNextAllocation = objects.failAfterPool;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL DestroyPool(VkDevice, VkDescriptorPool, const VkAllocationCallbacks*) {
|
||||
RequireTest(objects.pools != 0, "pool destroyed twice");
|
||||
--objects.pools;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL AllocateSets(VkDevice, const VkDescriptorSetAllocateInfo* info, VkDescriptorSet* set) {
|
||||
auto& used = objects.poolUsed.at(reinterpret_cast<std::uintptr_t>(info->descriptorPool));
|
||||
if (used) return VK_ERROR_OUT_OF_POOL_MEMORY;
|
||||
used = true;
|
||||
*set = reinterpret_cast<VkDescriptorSet>(objects.next++);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL ResetPool(VkDevice, VkDescriptorPool pool, VkDescriptorPoolResetFlags) {
|
||||
objects.poolUsed.at(reinterpret_cast<std::uintptr_t>(pool)) = false;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice, const char* name) {
|
||||
if (std::strcmp(name, "vkCreateDescriptorSetLayout") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CreateLayout);
|
||||
if (std::strcmp(name, "vkDestroyDescriptorSetLayout") == 0) return reinterpret_cast<PFN_vkVoidFunction>(DestroyLayout);
|
||||
if (std::strcmp(name, "vkCreateDescriptorPool") == 0) return reinterpret_cast<PFN_vkVoidFunction>(CreatePool);
|
||||
if (std::strcmp(name, "vkDestroyDescriptorPool") == 0) return reinterpret_cast<PFN_vkVoidFunction>(DestroyPool);
|
||||
if (std::strcmp(name, "vkAllocateDescriptorSets") == 0) return reinterpret_cast<PFN_vkVoidFunction>(AllocateSets);
|
||||
if (std::strcmp(name, "vkResetDescriptorPool") == 0) return reinterpret_cast<PFN_vkVoidFunction>(ResetPool);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Context TestContext() {
|
||||
Context context{};
|
||||
context.deviceProc = Resolve;
|
||||
context.limits.maxPerStageResources = 128;
|
||||
context.limits.maxDescriptorSetStorageBuffers = 4096;
|
||||
context.descriptorIndexingLimits.maxPerStageUpdateAfterBindResources = 128;
|
||||
context.descriptorIndexingLimits.maxDescriptorSetUpdateAfterBindStorageBuffers = 4096;
|
||||
return context;
|
||||
}
|
||||
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, nullptr};
|
||||
const std::array<std::uint32_t, 4> key{binding.binding, binding.descriptorType, binding.descriptorCount, binding.stageFlags};
|
||||
const VkDescriptorPoolSize size{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1};
|
||||
|
||||
bool LayoutFailure(bool updateAfterBind) {
|
||||
objects = {};
|
||||
bool failed = false;
|
||||
{
|
||||
DescriptorCache cache(TestContext());
|
||||
objects.failAfterLayout = true;
|
||||
try {
|
||||
cache.Layout(key, std::span(&binding, 1), updateAfterBind);
|
||||
} catch (const std::bad_alloc&) {
|
||||
failed = true;
|
||||
}
|
||||
failNextAllocation = false;
|
||||
objects.failAfterLayout = false;
|
||||
RequireTest(failed && objects.createdLayouts == 1, "layout publication failure was not exercised");
|
||||
const auto layout = cache.Layout(key, std::span(&binding, 1), updateAfterBind);
|
||||
RequireTest(layout != VK_NULL_HANDLE && objects.createdLayouts == 2, "layout creation did not recover");
|
||||
RequireTest(cache.Layout(key, std::span(&binding, 1), updateAfterBind) == layout && objects.createdLayouts == 2, "layout cache hit created another object");
|
||||
}
|
||||
std::printf("layout updateAfterBind=%d: unowned=%u\n", updateAfterBind, objects.layouts);
|
||||
return objects.layouts == 0;
|
||||
}
|
||||
|
||||
bool PoolFailure(bool updateAfterBind, bool grow) {
|
||||
objects = {};
|
||||
{
|
||||
DescriptorCache cache(TestContext());
|
||||
const auto layout = cache.Layout(key, std::span(&binding, 1), updateAfterBind);
|
||||
DescriptorCache::SetAllocation first{};
|
||||
if (grow) first = cache.Allocate(layout, std::span(&size, 1), updateAfterBind);
|
||||
objects.failAfterPool = true;
|
||||
DescriptorCache::SetAllocation allocation{};
|
||||
try {
|
||||
allocation = cache.Allocate(layout, std::span(&size, 1), updateAfterBind);
|
||||
} catch (const std::bad_alloc&) {
|
||||
}
|
||||
failNextAllocation = false;
|
||||
objects.failAfterPool = false;
|
||||
RequireTest(objects.createdPools == (grow ? 2u : 1u), "pool publication path was not exercised");
|
||||
if (allocation.set != VK_NULL_HANDLE) cache.Free(allocation);
|
||||
if (grow) cache.Free(first);
|
||||
const auto recovered = cache.Allocate(layout, std::span(&size, 1), updateAfterBind);
|
||||
RequireTest(recovered.set != VK_NULL_HANDLE, "pool allocation did not recover");
|
||||
cache.Free(recovered);
|
||||
}
|
||||
std::printf("pool updateAfterBind=%d grow=%d: unowned=%u\n", updateAfterBind, grow, objects.pools);
|
||||
return objects.layouts == 0 && objects.pools == 0;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
int failures = 0;
|
||||
for (const bool updateAfterBind : {false, true}) {
|
||||
if (!LayoutFailure(updateAfterBind)) ++failures;
|
||||
for (const bool grow : {false, true}) if (!PoolFailure(updateAfterBind, grow)) ++failures;
|
||||
}
|
||||
return failures == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -30,6 +30,7 @@
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
@@ -1338,6 +1339,48 @@ void BlendBypassTests() {
|
||||
Require(!bypassed.blends.at(0).blendEnable && bypassed.blends.at(0).colorWriteMask != 0, "a target with BLEND_BYPASS still blends");
|
||||
}
|
||||
|
||||
void UnclampedNormalizedColorTests() {
|
||||
using AgcDriver::Graphics::DecodeState;
|
||||
constexpr std::uint32_t blending = (1u << 30u) | 4u | (5u << 8u);
|
||||
const auto target = [](std::uint32_t info) {
|
||||
auto queue = makeState();
|
||||
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
|
||||
queue.context[0x31c] = info;
|
||||
queue.context[0x1b3] = 2;
|
||||
queue.context[0x1b4] = 2;
|
||||
return queue;
|
||||
};
|
||||
auto queue = target(0x0000000cu);
|
||||
auto state = DecodeState(queue);
|
||||
Require(state.colors.size() == 1 && state.colors[0].format == VK_FORMAT_R8G8_UNORM && !state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an 8_8 UNORM target with BLEND_CLAMP clear (CB_COLOR_INFO 0x0000000c, PPSA26344) was refused without blending");
|
||||
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
|
||||
Require(rejection.empty(), "the precheck refused an unclamped UNORM target that does not blend: " + rejection);
|
||||
Require(AgcDriver::Graphics::DecodeColorBuffer(queue.context, 0).format == VK_FORMAT_R8G8_UNORM, "an unclamped UNORM target was refused outside a draw");
|
||||
auto eliminate = target(0x0000000cu);
|
||||
eliminate.context[0x0] = 0;
|
||||
eliminate.context[0x202] = 0xcc0020;
|
||||
const auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(eliminate);
|
||||
Require(pass.has_value() && pass->mode == AgcDriver::Graphics::ColorMetadataPass::Mode::EliminateFastClear && pass->targets.size() == 1 && pass->targets[0].format == VK_FORMAT_R8G8_UNORM, "a fast-clear eliminate over an unclamped UNORM target was refused");
|
||||
queue.context[0x1e0] = blending;
|
||||
expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented (CB_COLOR_INFO 0x0000000c, CB_BLEND0_CONTROL 0x40000504)");
|
||||
queue.context[0x31c] |= 0x10000u;
|
||||
state = DecodeState(queue);
|
||||
Require(!state.blends.at(0).blendEnable && state.blends.at(0).colorWriteMask != 0, "an unclamped UNORM target with BLEND_BYPASS was refused or blended");
|
||||
queue = target(0x0000010cu);
|
||||
Require(DecodeState(queue).colors.at(0).format == VK_FORMAT_R8G8_SNORM, "an unclamped SNORM target that does not blend was refused");
|
||||
queue.context[0x1e0] = blending;
|
||||
expectFailure([&] { DecodeState(queue); }, "blending unclamped normalized color is not implemented");
|
||||
queue = target(0x00000604u);
|
||||
expectFailure([&] { DecodeState(queue); }, "unclamped normalized color is unsupported");
|
||||
queue = target(0x0000800cu);
|
||||
queue.context[0x1e0] = blending;
|
||||
Require(DecodeState(queue).blends.at(0).blendEnable, "a clamped UNORM target stopped blending");
|
||||
queue = target(0x00000708u);
|
||||
queue.context[0x1e0] = blending;
|
||||
state = DecodeState(queue);
|
||||
Require(state.colors.at(0).format == VK_FORMAT_R16_SFLOAT && state.blends.at(0).blendEnable, "a 16 float target, which keeps BLEND_CLAMP clear, stopped blending");
|
||||
}
|
||||
|
||||
void OpaqueDestinationAlphaTests() {
|
||||
auto queue = makeState();
|
||||
for (std::uint32_t constant = 0x105; constant < 0x109; ++constant) queue.context[constant] = 0;
|
||||
@@ -1693,6 +1736,35 @@ void ReversedBlendRoutingTests() {
|
||||
Require(native == 121u && routed == 932u, "the reversed-order blend routing accepts " + std::to_string(native) + " native and " + std::to_string(routed) + " routed factor combinations, not 121 and 932");
|
||||
}
|
||||
|
||||
void Color565TargetTests() {
|
||||
auto queue = makeState();
|
||||
const auto info = [&](std::uint32_t number, std::uint32_t swap) {
|
||||
queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (16u << 2u) | (number << 8u) | (swap << 11u);
|
||||
};
|
||||
info(0, 2);
|
||||
const auto reversed = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(reversed.colors.size() == 1 && reversed.colors[0].format == VK_FORMAT_R5G6B5_UNORM_PACK16 && reversed.colors[0].elementBytes == 2 && reversed.colors[0].componentMapping == 0xe4u, "a 5_6_5 target with the reversed component order did not render through R5G6B5 with X in its high bits");
|
||||
Require(reversed.blends[0].colorWriteMask == 0xfu, "a 5_6_5 target with the reversed component order did not write all its channels");
|
||||
queue.context[0x8e] = 0x1u;
|
||||
Require(AgcDriver::Graphics::DecodeState(queue).blends[0].colorWriteMask == VK_COLOR_COMPONENT_R_BIT, "a 5_6_5 target with the reversed component order writing only X did not write the R5G6B5 channel in its high bits");
|
||||
info(0, 0);
|
||||
const auto standard = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(standard.colors[0].format == VK_FORMAT_R5G6B5_UNORM_PACK16 && standard.colors[0].componentMapping == 0xc6u, "a 5_6_5 target with the standard component order did not render through R5G6B5 with X and Z exchanged");
|
||||
Require(AgcDriver::Graphics::ExportMappings(standard)[0] == 0xc6u, "a 5_6_5 target with the standard component order did not export through the X and Z exchange");
|
||||
Require(standard.blends[0].colorWriteMask == VK_COLOR_COMPONENT_B_BIT, "a 5_6_5 target with the standard component order writing only X did not write the R5G6B5 channel in its low bits");
|
||||
queue.context[0x8e] = 0xfu;
|
||||
info(0, 2);
|
||||
for (std::uint32_t i = 0; i < 4; ++i) queue.context[0x105 + i] = 0;
|
||||
queue.context[0x1e0] = 0x40000504u;
|
||||
const auto blended = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(blended.blends[0].blendEnable && blended.blends[0].srcColorBlendFactor == VK_BLEND_FACTOR_SRC_ALPHA && blended.blends[0].dstColorBlendFactor == VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, "alpha blending into a 5_6_5 target was not decoded");
|
||||
queue.context[0x1e0] = 0;
|
||||
for (const auto& [number, swap] : {std::pair{1u, 2u}, std::pair{4u, 0u}, std::pair{0u, 1u}, std::pair{0u, 3u}}) {
|
||||
info(number, swap);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "unsupported color format 16 number type " + std::to_string(number) + " component swap " + std::to_string(swap));
|
||||
}
|
||||
}
|
||||
|
||||
void Color1555TargetTests() {
|
||||
auto queue = makeState();
|
||||
queue.context[0x31c] = (queue.context[0x31c] & ~((0x1fu << 2u) | (7u << 8u) | (3u << 11u))) | (17u << 2u);
|
||||
@@ -2013,6 +2085,12 @@ void DepthStencilTests() {
|
||||
const auto volume = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(volume.color.address == sliced && volume.color.depth == 4u && volume.color.depthSlice == 2u, "a color view of one 3D depth slice did not keep the surface address and select the slice");
|
||||
Require(volume.color.layers == 1 && volume.renderLayers == 1 && volume.layerExports == 0, "a color view of one 3D depth slice became layered");
|
||||
{
|
||||
auto mipmapped = queue;
|
||||
mipmapped.context[0x3b0] |= 2u << 28u;
|
||||
mipmapped.context[0x31b] = (1u << 26u) | (1u << 13u) | 1u;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeColorBuffer(mipmapped.context, 0); }, "slot 0, 64x4x4, viewMip 1, maxMip 2, slices 1..1, CB_COLOR_INFO 0x00028028, ATTRIB2 0x200fc003, ATTRIB3 0x0a000003, VIEW 0x04002001");
|
||||
}
|
||||
queue.context[0x31b] = 4u | (4u << 13u);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "outside the 3D surface");
|
||||
queue.context[0x31b] = 2u | (1u << 13u);
|
||||
@@ -2993,6 +3071,11 @@ void multisampleTests() {
|
||||
}
|
||||
queue.context[0x32b] = queue.context[0x31c] ^ (1u << 8u);
|
||||
expectFailure([&] { DecodeColorMetadataPass(queue); }, "different formats or extents");
|
||||
for (const auto [packedInfo, name] : {std::pair{6u << 2u, "10_11_11 unorm"}, std::pair{(7u << 2u) | (7u << 8u) | (2u << 11u) | 0x40000u, "11_11_10 float"}}) {
|
||||
auto packed = rasterizedResolve;
|
||||
for (const auto offset : {0x31cu, 0x32bu}) packed.context[offset] = (packed.context[offset] & ~0x41f7cu) | packedInfo;
|
||||
expectFailure([&] { DecodeColorMetadataPass(packed); }, std::string("CB resolves of ") + name + " color targets");
|
||||
}
|
||||
queue = rasterizedResolve;
|
||||
queue.context[0x31c] |= 0x10000000u;
|
||||
queue.context[0x325] = 0;
|
||||
@@ -3655,9 +3738,36 @@ struct MockDescriptorWrite {
|
||||
std::vector<VkDescriptorImageInfo> images;
|
||||
};
|
||||
|
||||
struct LiveCounter {
|
||||
std::atomic<std::int64_t> value{0};
|
||||
LiveCounter() = default;
|
||||
LiveCounter(const LiveCounter& other) : value(other.value.load()) {}
|
||||
LiveCounter& operator=(const LiveCounter& other) {
|
||||
value.store(other.value.load());
|
||||
return *this;
|
||||
}
|
||||
operator std::int64_t() const { return value.load(); }
|
||||
LiveCounter& operator++() {
|
||||
++value;
|
||||
return *this;
|
||||
}
|
||||
LiveCounter& operator--() {
|
||||
--value;
|
||||
return *this;
|
||||
}
|
||||
LiveCounter& operator+=(std::int64_t count) {
|
||||
value += count;
|
||||
return *this;
|
||||
}
|
||||
LiveCounter& operator-=(std::int64_t count) {
|
||||
value -= count;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
struct MockVulkan {
|
||||
std::uint64_t next = 1;
|
||||
std::int64_t live = 0;
|
||||
LiveCounter live;
|
||||
std::map<VkBuffer, VkDeviceSize> bufferSizes;
|
||||
std::map<VkBuffer, VkBufferUsageFlags> bufferUsage;
|
||||
std::map<VkDeviceMemory, VkMemoryAllocateFlags> allocationFlags;
|
||||
@@ -3677,6 +3787,7 @@ struct MockVulkan {
|
||||
std::vector<VkDescriptorPoolSize> poolSizes;
|
||||
std::uint32_t poolMaxSets = 0;
|
||||
VkDescriptorPoolCreateFlags poolFlags = 0;
|
||||
std::uint32_t freedDescriptorSets = 0;
|
||||
struct DescriptorPool {
|
||||
VkDescriptorPoolCreateFlags flags;
|
||||
std::uint32_t maxSets;
|
||||
@@ -3844,6 +3955,15 @@ VKAPI_ATTR VkResult VKAPI_CALL mockResetDescriptorPool(VkDevice, VkDescriptorPoo
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockFreeDescriptorSets(VkDevice, VkDescriptorPool handle, std::uint32_t count, const VkDescriptorSet*) {
|
||||
auto& pool = mock.descriptorPools.at(handle);
|
||||
Require((pool.flags & VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT) != 0, "a descriptor set was freed from a pool without free support");
|
||||
Require(count <= pool.allocated, "more descriptor sets were freed than allocated");
|
||||
pool.allocated -= count;
|
||||
mock.freedDescriptorSets += count;
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) {
|
||||
*sampler = makeHandle<VkSampler>();
|
||||
mock.samplerFlags[*sampler] = info->flags;
|
||||
@@ -4029,6 +4149,7 @@ PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) {
|
||||
{"vkCreateDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockCreateDescriptorPool)},
|
||||
{"vkDestroyDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyDescriptorPool)},
|
||||
{"vkAllocateDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateDescriptorSets)},
|
||||
{"vkFreeDescriptorSets", reinterpret_cast<PFN_vkVoidFunction>(mockFreeDescriptorSets)},
|
||||
{"vkResetDescriptorPool", reinterpret_cast<PFN_vkVoidFunction>(mockResetDescriptorPool)},
|
||||
{"vkCreateSampler", reinterpret_cast<PFN_vkVoidFunction>(mockCreateSampler)},
|
||||
{"vkDestroySampler", reinterpret_cast<PFN_vkVoidFunction>(mockDestroySampler)},
|
||||
@@ -4439,6 +4560,152 @@ void descriptorSnapshotTests() {
|
||||
}
|
||||
}
|
||||
|
||||
alignas(16) std::array<std::uint32_t, 8> guestMovedFirst{0x51515151, 0x52525252, 0x53535353, 0x54545454, 0x55555555, 0x56565656, 0x57575757, 0x58585858};
|
||||
alignas(16) std::array<std::uint32_t, 8> guestMovedSecond{0x61616161, 0x62626262, 0x63636363, 0x64646464, 0x65656565, 0x66666666, 0x67676767, 0x68686868};
|
||||
|
||||
void dispatchMoveTests() {
|
||||
using AgcDriver::Graphics::CompiledShader;
|
||||
using AgcDriver::Graphics::DescriptorCache;
|
||||
using AgcDriver::Graphics::Recorder;
|
||||
using AgcDriver::Graphics::ShaderResources;
|
||||
using Refusal = ShaderResources::MoveRefusal;
|
||||
mock = MockVulkan{};
|
||||
mock.allowCopies = true;
|
||||
auto context = mockContext();
|
||||
{
|
||||
DescriptorCache cache(context);
|
||||
context.descriptorCache = &cache;
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
Recorder recorder(context);
|
||||
ShaderRecompiler::RecompileResult built;
|
||||
auto inputs = makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)));
|
||||
inputs.bufferWritten = {false, false};
|
||||
built.bindings.push_back(inputs);
|
||||
auto output = makeBinding(Role::GuestBuffers, 4, 1, vsharp(guestThird.data(), 8));
|
||||
output.bufferWritten = {true};
|
||||
built.bindings.push_back(output);
|
||||
const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &built, 0};
|
||||
ShaderResources resources(context, shader);
|
||||
const auto moveTo = [&](std::size_t element, const void* where, std::uint32_t bytes) {
|
||||
auto program = built;
|
||||
const auto words = vsharp(where, bytes);
|
||||
std::copy(words.begin(), words.end(), program.bindings[0].guestDescriptor.begin() + static_cast<std::ptrdiff_t>(element) * 4);
|
||||
return program;
|
||||
};
|
||||
const auto prove = [&](const ShaderRecompiler::RecompileResult& program, std::size_t maxBytes, ShaderResources::MoveProof& proof) {
|
||||
return resources.MovedDispatchBuffers(CompiledShader{ShaderRecompiler::ShaderStage::Compute, &program, 0}, &recorder, maxBytes, proof);
|
||||
};
|
||||
const auto refuses = [&](const ShaderRecompiler::RecompileResult& program, std::size_t maxBytes, Refusal why, const char* what) {
|
||||
ShaderResources::MoveProof refusal;
|
||||
const auto listed = prove(program, maxBytes, refusal);
|
||||
Require(!listed.has_value() && refusal.refusal == why, what);
|
||||
};
|
||||
|
||||
ShaderResources::MoveProof proof;
|
||||
auto moved = prove(built, 0, proof);
|
||||
Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None && proof.buffers == 0, "a dispatch with every buffer where it was built moved something");
|
||||
|
||||
const auto relocated = moveTo(1, guestMovedSecond.data(), 24);
|
||||
const CompiledShader relocatedShader{ShaderRecompiler::ShaderStage::Compute, &relocated, 0};
|
||||
Require(ShaderResources::ContentKey(shader, false, true) == ShaderResources::ContentKey(relocatedShader, false, true), "moving a read-only buffer changed the movable content key");
|
||||
Require(ShaderResources::ContentKey(shader, false, false) != ShaderResources::ContentKey(relocatedShader, false, false), "the fixed content key ignored a moved buffer");
|
||||
|
||||
moved = prove(relocated, 0, proof);
|
||||
Require(moved.has_value() && moved->size() == 1 && proof.refusal == Refusal::None && proof.buffers == 1 && proof.bytes >= 24, "a moved read-only buffer was not listed");
|
||||
Require(moved->front().address == reinterpret_cast<std::uintptr_t>(guestMovedSecond.data()) && moved->front().size == 24 && moved->front().words.empty(), "the listed buffer is not the moved range");
|
||||
|
||||
moved = prove(moveTo(0, guestFirst.data(), 8), 0, proof);
|
||||
Require(moved.has_value() && moved->size() == 1 && moved->front().address == reinterpret_cast<std::uintptr_t>(guestFirst.data()) && moved->front().size == 8, "a buffer that only changed its size was not listed");
|
||||
|
||||
auto both = relocated;
|
||||
const auto firstWords = vsharp(guestMovedFirst.data(), 12);
|
||||
std::copy(firstWords.begin(), firstWords.end(), both.bindings[0].guestDescriptor.begin());
|
||||
moved = prove(both, 1024, proof);
|
||||
Require(moved.has_value() && moved->size() == 2 && proof.buffers == 2 && proof.bytes >= 36, "two moved buffers were not both listed");
|
||||
refuses(both, 30, Refusal::Limit, "moved buffers past the byte budget together were served");
|
||||
|
||||
auto written = relocated;
|
||||
written.bindings[0].bufferWritten[1] = true;
|
||||
refuses(written, 0, Refusal::Written, "a moved buffer the dispatch writes was served");
|
||||
refuses(moveTo(1, guestMovedSecond.data(), 20000), 0, Refusal::Written, "a moved buffer over the storage buffer range that no registered allocation holds was served");
|
||||
refuses(relocated, 16, Refusal::Limit, "a moved buffer over the byte budget was served");
|
||||
refuses(moveTo(1, reinterpret_cast<const void*>(std::uintptr_t{0x10}), 24), 0, Refusal::Inaccessible, "a moved buffer over unmapped memory was served");
|
||||
refuses(moveTo(1, guestThird.data(), 8), 0, Refusal::Overlap, "a moved buffer over the dispatch's own written range was served");
|
||||
auto shaped = relocated;
|
||||
shaped.bindings[0].guestDescriptor.resize(4);
|
||||
refuses(shaped, 0, Refusal::Shape, "a dispatch with another binding shape was served");
|
||||
recorder.Sync();
|
||||
}
|
||||
{
|
||||
constexpr std::size_t blockBytes = 65536;
|
||||
void* block = ::operator new(blockBytes, std::align_val_t{blockBytes});
|
||||
std::memset(block, 0x5a, blockBytes);
|
||||
{
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, blockBytes, true, true, true);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
}
|
||||
const auto* end = static_cast<const std::byte*>(block) + blockBytes;
|
||||
const auto strided = [](const void* pointer, std::uint32_t stride, std::uint32_t records) {
|
||||
auto words = vsharp(pointer, records);
|
||||
words[1] |= stride << 16u;
|
||||
return words;
|
||||
};
|
||||
DescriptorCache cache(context);
|
||||
context.descriptorCache = &cache;
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
Recorder recorder(context);
|
||||
const auto proveAgainst = [&](const ShaderRecompiler::RecompileResult& built, const std::vector<std::uint32_t>& incoming, ShaderResources::MoveProof& proof, std::optional<std::vector<ShaderResources::MovedBuffer>>& moved) {
|
||||
const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &built, 0};
|
||||
ShaderResources resources(context, shader);
|
||||
auto program = built;
|
||||
program.bindings[0].guestDescriptor = incoming;
|
||||
moved = resources.MovedDispatchBuffers(CompiledShader{ShaderRecompiler::ShaderStage::Compute, &program, 0}, &recorder, 0, proof);
|
||||
};
|
||||
const auto readOnly = [&](const std::vector<std::uint32_t>& words) {
|
||||
ShaderRecompiler::RecompileResult built;
|
||||
auto binding = makeBinding(Role::GuestBuffers, 0, 1, words);
|
||||
binding.bufferWritten = {false};
|
||||
built.bindings.push_back(binding);
|
||||
return built;
|
||||
};
|
||||
const auto address = [](const void* pointer) { return reinterpret_cast<std::uintptr_t>(pointer); };
|
||||
ShaderResources::MoveProof proof;
|
||||
std::optional<std::vector<ShaderResources::MovedBuffer>> moved;
|
||||
|
||||
const auto* tail = end - 8192;
|
||||
const auto* nearEnd = end - 4096;
|
||||
const auto oversized = readOnly(vsharp(tail, 0xffffffffu));
|
||||
proveAgainst(oversized, vsharp(tail, 0xffffffffu), proof, moved);
|
||||
Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None && proof.buffers == 0, "an unchanged V# the build cut to its allocation end was not an exact hit");
|
||||
proveAgainst(oversized, vsharp(nearEnd, 0xffffffffu), proof, moved);
|
||||
Require(moved.has_value() && moved->size() == 1 && proof.buffers == 1 && proof.bytes == 4096 && moved->front().address == address(nearEnd) && moved->front().size == 4096, "a moved V# past the range limit was not snapshotted at its clamped range");
|
||||
proveAgainst(oversized, vsharp(nearEnd, 0xfffffff0u), proof, moved);
|
||||
Require(moved.has_value() && moved->size() == 1 && moved->front().size == 4096, "another oversized size over the same allocation end listed another range");
|
||||
proveAgainst(oversized, vsharp(tail, 4096), proof, moved);
|
||||
Require(moved.has_value() && moved->size() == 1 && moved->front().address == address(tail) && moved->front().size == 4096, "a V# that now fits the range limit was not listed at its own size");
|
||||
|
||||
proveAgainst(oversized, vsharp(block, 0xffffffffu), proof, moved);
|
||||
Require(!moved.has_value() && proof.refusal == Refusal::Limit, "a moved V# the allocation holds past the range limit was served");
|
||||
|
||||
const auto overhang = readOnly(strided(tail, 24, 342));
|
||||
proveAgainst(overhang, strided(tail, 24, 342), proof, moved);
|
||||
Require(moved.has_value() && moved->empty() && proof.refusal == Refusal::None, "an unchanged V# with a stride overhang past the allocation was not an exact hit");
|
||||
proveAgainst(overhang, strided(nearEnd, 24, 171), proof, moved);
|
||||
Require(moved.has_value() && moved->size() == 1 && moved->front().address == address(nearEnd) && moved->front().size == 4096 && proof.bytes == 4096, "a moved V# with a stride overhang was snapshotted past the allocation end");
|
||||
proveAgainst(overhang, strided(nearEnd, 24, 100), proof, moved);
|
||||
Require(moved.has_value() && moved->size() == 1 && moved->front().size == 2400, "a moved V# inside the allocation was not listed at its own size");
|
||||
recorder.Sync();
|
||||
{
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
}
|
||||
::operator delete(block, std::align_val_t{blockBytes});
|
||||
}
|
||||
Require(mock.live == 0, "dispatch move resources leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void resourceTests() {
|
||||
mock = MockVulkan{};
|
||||
const auto context = mockContext();
|
||||
@@ -4741,6 +5008,28 @@ void descriptorCacheTests() {
|
||||
expectFailure([&] { cache.Allocate(fittingLayout, fitting, false, 3u); }, "descriptor set exceeds the device's per-set descriptor limit");
|
||||
}
|
||||
Require(mock.live == 0, "the combined descriptor cache leaked a pool or layout");
|
||||
mock = MockVulkan{};
|
||||
auto lavapipeContext = mockContext();
|
||||
lavapipeContext.individuallyFreeDescriptorSets = true;
|
||||
lavapipeContext.limits.maxDescriptorSetStorageBuffers = 16;
|
||||
{
|
||||
AgcDriver::Graphics::DescriptorCache cache(lavapipeContext);
|
||||
const VkDescriptorSetLayoutBinding binding{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT, nullptr};
|
||||
const std::array<std::uint32_t, 4> key{0, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1, VK_SHADER_STAGE_FRAGMENT_BIT};
|
||||
const auto layout = cache.Layout(key, std::span(&binding, 1));
|
||||
const std::array<VkDescriptorPoolSize, 1> sizes{{{VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 1}}};
|
||||
const auto first = cache.Allocate(layout, sizes);
|
||||
const auto second = cache.Allocate(layout, sizes);
|
||||
Require(first.pool == second.pool && mock.poolFlags == VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT, "lavapipe chain pools do not enable individual descriptor-set frees");
|
||||
cache.Free(first);
|
||||
Require(mock.freedDescriptorSets == 1 && mock.descriptorPools.at(first.pool).resets == 0, "freeing one of two live sets reset its chain pool");
|
||||
cache.Free(second);
|
||||
Require(mock.freedDescriptorSets == 2 && mock.descriptorPools.at(second.pool).resets == 1, "freeing the last live set did not free it and reset its chain pool");
|
||||
const auto reused = cache.Allocate(layout, sizes);
|
||||
Require(reused.pool == first.pool && mock.descriptorPools.at(reused.pool).attempts == 3, "an emptied lavapipe chain pool was not reused");
|
||||
cache.Free(reused);
|
||||
}
|
||||
Require(mock.live == 0, "the lavapipe descriptor cache leaked a pool or layout");
|
||||
}
|
||||
|
||||
void imageTableAllocationTests() {
|
||||
@@ -5008,21 +5297,33 @@ void storageTextureBudgetTests() {
|
||||
memory.memoryHeaps[1] = {32 * GiB, 0};
|
||||
memory.memoryHeaps[2] = {16 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT};
|
||||
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
|
||||
Require(StorageTextureBudget(memory, nullptr) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap");
|
||||
Require(StorageTextureBudget(memory, nullptr, 0, 0) == 4 * GiB, "without VK_EXT_memory_budget the storage image cache does not keep a quarter of the 16 GiB heap");
|
||||
reported.heapBudget[0] = 100 * GiB;
|
||||
reported.heapBudget[1] = 100 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap");
|
||||
Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a zero budget for the largest device-local heap did not fall back to a quarter of the heap");
|
||||
reported.heapBudget[2] = 10 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
|
||||
Require(StorageTextureBudget(memory, &reported, 0, 0) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
|
||||
reported.heapUsage[2] = 5 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "2 GiB used outside the texture caches on a 10 GiB budget lowered the storage image cache below its quarter");
|
||||
reported.heapUsage[2] = 8 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 1792 * MiB, "a 10 GiB budget with 5 GiB used outside the texture caches does not leave 1792 MiB after the sampled textures' 2 GiB floor and 1280 MiB of headroom");
|
||||
reported.heapUsage[2] = 9 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved past their 2 GiB floor");
|
||||
reported.heapUsage[2] = 1 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "usage below the cached bytes is not read as no other device memory in use");
|
||||
Require(StorageTextureBudget(memory, &reported, 7 * GiB, 1 * GiB) == 1792 * MiB, "7 GiB of sampled textures with the usage below the cached bytes do not leave 1792 MiB after their reservation and 1280 MiB of headroom");
|
||||
reported.heapUsage[2] = 9 * GiB + 512 * MiB;
|
||||
Require(StorageTextureBudget(memory, &reported, 2 * GiB, 512 * MiB) == 512 * MiB, "a heap whose other users leave no room does not keep the 512 MiB floor");
|
||||
reported.heapUsage[2] = 0;
|
||||
reported.heapBudget[2] = 6 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
|
||||
Require(StorageTextureBudget(memory, &reported, 0, 0) == 1536 * MiB, "a 6 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
|
||||
reported.heapBudget[2] = 1 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor");
|
||||
Require(StorageTextureBudget(memory, &reported, 0, 0) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor");
|
||||
reported.heapBudget[2] = 40 * GiB;
|
||||
Require(StorageTextureBudget(memory, &reported) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap");
|
||||
Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap");
|
||||
memory.memoryHeaps[0].flags = 0;
|
||||
memory.memoryHeaps[2].flags = 0;
|
||||
Require(StorageTextureBudget(memory, &reported) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images");
|
||||
Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images");
|
||||
}
|
||||
|
||||
void sampledBudgetReportTests() {
|
||||
@@ -6109,7 +6410,67 @@ void barycentricEmulationTests() {
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricGeometryShader(vertex, plain, target, limits)); }, "does not emulate barycentrics");
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricGeometryShader(vertex, fragment, target, std::nullopt)); }, "geometry shaders are unavailable");
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricGeometryShader(vertex, fragment, target, GeometryStageLimits{64u, 12u, 256u, 1024u, 128u})); }, "exceeds device limits");
|
||||
|
||||
const std::array<std::uint32_t, 2> tessellationCapabilities{spv::CapabilityShader, spv::CapabilityTessellation};
|
||||
SpirvTarget tessellationTarget = target;
|
||||
tessellationTarget.supportedCapabilities = tessellationCapabilities;
|
||||
tessellationTarget.tessellation = TessellationTargetLimits{32u, 128u, 128u, 120u, 4096u, 128u, 128u};
|
||||
const auto modes = [](const RecompileResult& result) {
|
||||
std::vector<std::uint32_t> values;
|
||||
const auto& words = result.spirv.Words();
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpExecutionMode) values.push_back(words[at + 2]);
|
||||
}
|
||||
return values;
|
||||
};
|
||||
auto patches = BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u);
|
||||
Require(modes(patches.control) == std::vector<std::uint32_t>{spv::ExecutionModeOutputVertices, spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated control stage does not declare three output vertices and the triangle domain");
|
||||
Require(modes(patches.evaluation) == std::vector<std::uint32_t>{spv::ExecutionModeTriangles, spv::ExecutionModeSpacingEqual, spv::ExecutionModeVertexOrderCw}, "the generated evaluation stage does not declare the triangle domain");
|
||||
const std::array<CompiledShader, 4> patchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}};
|
||||
Require(BarycentricPatches(state, patchShaders) && !BarycentricPatches(state, shaders), "generated tessellation stages were not recognised as barycentric patches");
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
ValidateShaders(patchShaders, state, subgroup, false);
|
||||
for (const auto topology : {VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_FAN, VK_PRIMITIVE_TOPOLOGY_POINT_LIST}) {
|
||||
state.topology = topology;
|
||||
expectFailure([&] { ValidateShaders(patchShaders, state, subgroup, false); }, "require a triangle list");
|
||||
}
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
const std::array<CompiledShader, 4> plainPatchShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &patches.control, 0}, {ShaderStage::TessellationEvaluation, &patches.evaluation, 0}, {ShaderStage::Fragment, &plain, 0}}};
|
||||
expectFailure([&] { ValidateShaders(plainPatchShaders, state, subgroup, false); }, "stage count");
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, plain, tessellationTarget, 128u)); }, "does not emulate barycentrics");
|
||||
auto noTessellation = tessellationTarget;
|
||||
noTessellation.tessellation.reset();
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, noTessellation, 128u)); }, "tessellation shaders are unavailable");
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, target, 128u)); }, "tessellation shaders are unavailable");
|
||||
auto noCapability = tessellationTarget;
|
||||
noCapability.supportedCapabilities = std::span<const std::uint32_t>(capabilities).first(1);
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, noCapability, 128u)); }, "tessellation capability is unavailable");
|
||||
auto smallPatches = tessellationTarget;
|
||||
smallPatches.tessellation->maxEvaluationOutputComponents = 4u;
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, smallPatches, 128u)); }, "exceeds device limits");
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 0u)); }, "exceeds device limits");
|
||||
|
||||
RecompileResult clipped;
|
||||
clipped.spirv = makeModule({.parameterOutput = true, .clipDistanceLength = 2});
|
||||
clipped.parameterExports = {0u};
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(clipped, fragment, tessellationTarget, 128u)); }, "need shaderClipDistance");
|
||||
const std::array<std::uint32_t, 3> clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance};
|
||||
auto clipTarget = tessellationTarget;
|
||||
clipTarget.supportedCapabilities = clipCapabilities;
|
||||
auto clippedPatches = BuildBarycentricTessellationShaders(clipped, fragment, clipTarget, 128u);
|
||||
const auto memberBuiltIn = [](const RecompileResult& result, spv::BuiltIn builtin) {
|
||||
const auto& words = result.spirv.Words();
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpMemberDecorate && words[at + 3] == spv::DecorationBuiltIn && words[at + 4] == builtin) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Require(!memberBuiltIn(patches.control, spv::BuiltInClipDistance) && !memberBuiltIn(patches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage declares clip distances the vertex shader does not write");
|
||||
Require(memberBuiltIn(clippedPatches.control, spv::BuiltInClipDistance) && memberBuiltIn(clippedPatches.evaluation, spv::BuiltInClipDistance), "a generated tessellation stage does not forward the vertex clip distances");
|
||||
const std::array<CompiledShader, 4> clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedPatches.control, 0}, {ShaderStage::TessellationEvaluation, &clippedPatches.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}};
|
||||
ValidateShaders(clippedShaders, state, subgroup, false, false, false, false, false, false, true);
|
||||
vertex.parameterExports.clear();
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricTessellationShaders(vertex, fragment, tessellationTarget, 128u)); }, "not exported by the vertex shader");
|
||||
expectFailure([&] { static_cast<void>(BuildBarycentricGeometryShader(vertex, fragment, target, limits)); }, "not exported by the vertex shader");
|
||||
}
|
||||
|
||||
@@ -6240,6 +6601,23 @@ void registeredReadableCoverTests() {
|
||||
::operator delete(memory, std::align_val_t{block});
|
||||
}
|
||||
|
||||
std::size_t VariableBuiltIns(const ShaderRecompiler::RecompileResult& result, spv::BuiltIn builtin) {
|
||||
std::size_t count = 0;
|
||||
const auto& words = result.spirv.Words();
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpDecorate && (words[at] >> 16u) == 4u && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == builtin) ++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool MemberBuiltIn(const ShaderRecompiler::RecompileResult& result, spv::BuiltIn builtin) {
|
||||
const auto& words = result.spirv.Words();
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
if (static_cast<spv::Op>(words[at] & 0xffffu) == spv::OpMemberDecorate && words[at + 3] == spv::DecorationBuiltIn && words[at + 4] == builtin) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void rectListTests() {
|
||||
using namespace ShaderRecompiler;
|
||||
using namespace AgcDriver::Graphics;
|
||||
@@ -6273,17 +6651,35 @@ void rectListTests() {
|
||||
expectFailure([&] { ValidateShaders(shaders, state, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false); }, "stage count");
|
||||
}
|
||||
}
|
||||
{
|
||||
RecompileResult clipped;
|
||||
clipped.spirv = makeModule({.clipDistanceLength = 2});
|
||||
const std::array<std::uint32_t, 3> clipCapabilities{spv::CapabilityShader, spv::CapabilityTessellation, spv::CapabilityClipDistance};
|
||||
auto clipTarget = target;
|
||||
clipTarget.supportedCapabilities = clipCapabilities;
|
||||
auto clippedRect = BuildRectListShaders(clipped, fragment, clipTarget);
|
||||
for (const auto* stage : {&clippedRect.control, &clippedRect.lastVertexControl, &clippedRect.evaluation}) {
|
||||
Require(!MemberBuiltIn(*stage, spv::BuiltInClipDistance) && VariableBuiltIns(*stage, spv::BuiltInClipDistance) == 2u, "a rect-list stage does not take and forward the vertex clip distances through variables of their own, as the vertex shader writes them");
|
||||
}
|
||||
const std::array<CompiledShader, 4> clippedShaders{{{ShaderStage::Vertex, &clipped, 0}, {ShaderStage::TessellationControl, &clippedRect.control, 0}, {ShaderStage::TessellationEvaluation, &clippedRect.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}};
|
||||
auto queue = makeState();
|
||||
queue.userConfig[0x242] = 17;
|
||||
ValidateShaders(clippedShaders, DecodeState(queue), VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false, false, false, false, false, false, true);
|
||||
}
|
||||
vertex.spirv = makeModule({.parameterOutput = true});
|
||||
fragment.spirv = makeModule({.fragment = true, .rectParameters = true});
|
||||
vertex.parameterExports = {0};
|
||||
fragment.fragmentParameters = {{0, 0, false, false}, {1, 0, true, false}};
|
||||
auto auxiliary = BuildRectListShaders(vertex, fragment, target);
|
||||
Require(!auxiliary.control.spirv.empty() && !auxiliary.evaluation.spirv.empty(), "rect-list parameter shaders are empty");
|
||||
Require(faultAuxiliary.lastVertexControl.Identity() == faultAuxiliary.control.Identity() && auxiliary.lastVertexControl.Identity() != auxiliary.control.Identity() && auxiliary.lastVertexControl.bindings.size() == 1 && auxiliary.lastVertexControl.bdaAbiVersion == auxiliary.control.bdaAbiVersion, "the last-vertex rect-list control shader does not differ from the first-vertex one exactly when a flat parameter is present");
|
||||
auto parameterQueue = makeState();
|
||||
parameterQueue.userConfig[0x242] = 17;
|
||||
const auto parameterState = DecodeState(parameterQueue);
|
||||
const std::array<CompiledShader, 4> parameterShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, &auxiliary.control, 0}, {ShaderStage::TessellationEvaluation, &auxiliary.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}};
|
||||
ValidateShaders(parameterShaders, parameterState, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false);
|
||||
for (const auto* control : {&auxiliary.control, &auxiliary.lastVertexControl}) {
|
||||
const std::array<CompiledShader, 4> parameterShaders{{{ShaderStage::Vertex, &vertex, 0}, {ShaderStage::TessellationControl, control, 0}, {ShaderStage::TessellationEvaluation, &auxiliary.evaluation, 0}, {ShaderStage::Fragment, &fragment, 0}}};
|
||||
ValidateShaders(parameterShaders, parameterState, VkPhysicalDeviceSubgroupProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}, false);
|
||||
}
|
||||
fragment.fragmentParameters[0].perVertex = true;
|
||||
auto explicitInterpolation = BuildRectListShaders(vertex, fragment, target);
|
||||
Require(!explicitInterpolation.control.spirv.empty() && !explicitInterpolation.evaluation.spirv.empty(), "rect-list shaders for an explicitly interpolated parameter are empty");
|
||||
@@ -7148,13 +7544,16 @@ void ProvokingVertexTests() {
|
||||
generated.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
|
||||
ValidateProvokingVertex(unsupported, generated);
|
||||
}
|
||||
for (std::uint32_t generated = 0; generated < 3; ++generated) {
|
||||
for (std::uint32_t generated = 0; generated < 2; ++generated) {
|
||||
auto state = last;
|
||||
if (generated == 0) state.rectList = true;
|
||||
if (generated == 1) state.stages.mesh.emplace();
|
||||
if (generated == 2) state.stages.tessellation.emplace();
|
||||
if (generated == 0) state.stages.mesh.emplace();
|
||||
if (generated == 1) state.stages.tessellation.emplace();
|
||||
expectFailure([&] { ValidateProvokingVertex(supported, state); }, "last provoking vertex is unsupported for generated primitive pipelines");
|
||||
}
|
||||
auto rectangle = last;
|
||||
rectangle.rectList = true;
|
||||
rectangle.topology = VK_PRIMITIVE_TOPOLOGY_PATCH_LIST;
|
||||
ValidateProvokingVertex(supported, rectangle, flatShaders);
|
||||
last.provokingVertexMode = VK_PROVOKING_VERTEX_MODE_MAX_ENUM_EXT;
|
||||
expectFailure([&] { ValidateProvokingVertex(supported, last); }, "invalid provoking vertex mode");
|
||||
}
|
||||
@@ -7398,13 +7797,78 @@ void unorm10_11_11TargetTests() {
|
||||
return AgcDriver::Graphics::ValidateShaders(shaders, target, subgroup, false);
|
||||
};
|
||||
Require(validate(packed, state) == std::set<std::uint32_t>{0u}, "the packed export did not reach attachment 0");
|
||||
expectFailure([&] { validate(unpacked, state); }, "uint4 words to its packed 10_11_11 unorm attachments");
|
||||
expectFailure([&] { validate(unpacked, state); }, "uint4 words to its packed color attachments");
|
||||
auto plainState = state;
|
||||
plainState.colors[0].packing = ColorExportPacking::None;
|
||||
expectFailure([&] { validate(packed, plainState); }, "float4 colors");
|
||||
auto uintExport = request;
|
||||
uintExport.context.pixel->targetOutputMode[0] = 7;
|
||||
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no 10_11_11 unorm packing path");
|
||||
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path");
|
||||
}
|
||||
|
||||
void float11_11_10TargetTests() {
|
||||
using ShaderRecompiler::ColorExportPacking;
|
||||
auto queue = makeState();
|
||||
queue.context[0x31c] = (queue.context.at(0x31c) & ~0x1f7cu) | (7u << 2u) | (7u << 8u) | (2u << 11u) | 0x40000u;
|
||||
queue.context[0x1b3] = 2;
|
||||
queue.context[0x1b4] = 2;
|
||||
const auto state = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(state.color.format == VK_FORMAT_R32_UINT && state.color.elementBytes == 4 && state.color.bytes == colorMemory.size() && state.color.componentMapping == 0xc6u && state.color.packing == ColorExportPacking::Float11_11_10, "an 11_11_10 float target did not decode as a packed R32_UINT attachment with blue in the low bits");
|
||||
const auto packings = AgcDriver::Graphics::ExportPackings(state);
|
||||
Require(packings[0] == ColorExportPacking::Float11_11_10 && std::all_of(packings.begin() + 1, packings.end(), [](ColorExportPacking packing) { return packing == ColorExportPacking::None; }), "export 0 did not carry the 11_11_10 float packing alone");
|
||||
Require(AgcDriver::Graphics::ExportMappings(state)[0] == 0xc6u, "export 0 did not reverse its components into the 11_11_10 float target");
|
||||
auto changed = queue;
|
||||
changed.context[0x31c] &= ~0x40000u;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "rounding (ROUND_MODE 0) 11_11_10 float color targets");
|
||||
for (const auto swap : {0u, 1u, 3u}) {
|
||||
changed = queue;
|
||||
changed.context[0x31c] = (changed.context[0x31c] & ~0x1800u) | (swap << 11u);
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type 7 component swap " + std::to_string(swap));
|
||||
}
|
||||
for (const auto number : {0u, 4u}) {
|
||||
changed = queue;
|
||||
changed.context[0x31c] = (changed.context[0x31c] & ~0x700u) | (number << 8u) | 0x8000u;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "unsupported color format 7 number type " + std::to_string(number) + " component swap 2");
|
||||
}
|
||||
changed = queue;
|
||||
changed.context[0x1e0] = 0x60010101u;
|
||||
for (std::uint32_t i = 0; i < 4; ++i) changed.context[0x105 + i] = 0;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "blending into an 11_11_10 float color target");
|
||||
changed = queue;
|
||||
changed.context[0x8e] = 6;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "partial writes of an 11_11_10 float color target");
|
||||
changed.context[0x8e] = 7;
|
||||
Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an RGB write of an 11_11_10 float target was refused");
|
||||
for (const auto format : {5u, 6u}) {
|
||||
changed = queue;
|
||||
changed.context[0x1c5] = format;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "color export format " + std::to_string(format) + " into an 11_11_10 float target");
|
||||
}
|
||||
changed = queue;
|
||||
changed.context[0x1c5] = 4;
|
||||
Require(AgcDriver::Graphics::DecodeState(changed).color.packing == ColorExportPacking::Float11_11_10, "an FP16_ABGR export into an 11_11_10 float target was refused");
|
||||
changed = queue;
|
||||
changed.context[0x31c] |= 0x10000000u;
|
||||
changed.context[0x325] = 0x1234;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(changed); }, "DCC-compressed 11_11_10 float color targets");
|
||||
|
||||
constexpr std::array<std::uint32_t, 3> code{0xf800180fu, 0x07060504u, 0xbf810000u};
|
||||
const auto request = packedExportRequest(queue, state, code);
|
||||
auto unorm = request;
|
||||
unorm.context.pixel->targetExportPacking[0] = ColorExportPacking::Unorm10_11_11;
|
||||
const ShaderRecompiler::RequestSerializer serializer;
|
||||
Require(serializer.Deserialize(serializer.Serialize(request)).request.context.pixel->targetExportPacking == request.context.pixel->targetExportPacking, "the 11_11_10 float packing was lost in serialization");
|
||||
const auto packed = ShaderRecompiler::Recompile(request);
|
||||
const auto unormPacked = ShaderRecompiler::Recompile(unorm);
|
||||
Require(packed.variantId == unormPacked.variantId && packed.PipelineVariantId() != unormPacked.PipelineVariantId(), "the 11_11_10 float packing did not key its own specialization of the shared artifact");
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
vertex.spirv = makeModule({});
|
||||
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &packed, 0}}};
|
||||
Require(AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false) == std::set<std::uint32_t>{0u}, "the packed float export did not reach attachment 0");
|
||||
auto invalid = request;
|
||||
invalid.context.pixel->targetExportPacking[0] = static_cast<ColorExportPacking>(3);
|
||||
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(invalid)); }, "invalid prepared fragment export packing");
|
||||
}
|
||||
|
||||
AgcDriver::QueueState dualSourceQueue() {
|
||||
@@ -7992,6 +8456,16 @@ void stagingPoolBudgetTests() {
|
||||
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{128} << 20u, "the staging pool budget on system memory fell below 128 MiB");
|
||||
memory.memoryTypes[0] = {VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, 1};
|
||||
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory) == VkDeviceSize{512} << 20u, "a heap with a device-only memory type was taken for system memory");
|
||||
memory.memoryHeaps[1].size = VkDeviceSize{16} << 30u;
|
||||
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
|
||||
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{2} << 30u, "a zero reported budget did not fall back to an eighth of the heap");
|
||||
reported.heapBudget[0] = VkDeviceSize{1} << 30u;
|
||||
reported.heapBudget[1] = VkDeviceSize{10} << 30u;
|
||||
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{1280} << 20u, "a 10 GiB budget on the 16 GiB heap does not limit the staging pool to an eighth of the budget");
|
||||
reported.heapBudget[1] = VkDeviceSize{2} << 30u;
|
||||
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{512} << 20u, "a 2 GiB budget does not keep the 512 MiB floor");
|
||||
reported.heapBudget[1] = VkDeviceSize{40} << 30u;
|
||||
Require(AgcDriver::Graphics::BufferPool::DeviceBudget(memory, &reported) == VkDeviceSize{2} << 30u, "a budget above the heap size raised the staging pool past an eighth of the heap");
|
||||
}
|
||||
|
||||
void meshArgumentTests() {
|
||||
@@ -8265,6 +8739,35 @@ void ResourceContentKeyTests(bool benchmark) {
|
||||
// into a second device) rebuilds the recorded graphics pipeline so the same configuration hits the
|
||||
// store, hands the compute result to its hook, recovers a tail torn inside a record's padding and
|
||||
// stops when cancelled.
|
||||
void pipelineStoreLazyBufferPoolTests() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
mock = MockVulkan{};
|
||||
auto context = mockContext();
|
||||
context.device = makeHandle<VkDevice>();
|
||||
context.graphicsPipelineLibrary = true;
|
||||
context.pipelineLinkTimeOptimization = false;
|
||||
context.limits.framebufferNoAttachmentsSampleCounts = VK_SAMPLE_COUNT_1_BIT;
|
||||
ShaderRecompiler::RecompileResult vertex, fragment;
|
||||
vertex.spirv = makeModule({});
|
||||
fragment.spirv = makeModule({.fragment = true});
|
||||
vertex.variantId = 201;
|
||||
fragment.variantId = 202;
|
||||
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
|
||||
std::array<std::uint32_t, 1> mainKey{31}, fragmentKey{32};
|
||||
const PipelineSetLayouts resources{makeHandle<VkDescriptorSetLayout>(), makeHandle<VkDescriptorSetLayout>(), true, mainKey, fragmentKey};
|
||||
Require(context.bufferPool == nullptr, "the lazy buffer pool fixture already owns a buffer pool");
|
||||
auto first = CachedPipeline(context, State{}, VertexInputLayout{}, resources, shaders);
|
||||
const auto created = mock.pipelineCreateCount;
|
||||
static_cast<void>(GetBufferPool(context));
|
||||
auto again = CachedPipeline(context, State{}, VertexInputLayout{}, resources, shaders);
|
||||
Require(again == first && mock.pipelineCreateCount == created, "creating the buffer pool dropped a cached pipeline of the live device");
|
||||
ClearCachedPipelines(context.device);
|
||||
again.reset();
|
||||
first.reset();
|
||||
context.bufferPool.reset();
|
||||
Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void pipelineJournalTests() {
|
||||
using namespace AgcDriver::Graphics;
|
||||
using Stage = ShaderRecompiler::ShaderStage;
|
||||
@@ -8553,6 +9056,10 @@ int main(int argc, char** argv) {
|
||||
setenv("APS5_HEAP_MIRROR_MIB", "4", 1);
|
||||
#endif
|
||||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--descriptor-cache") {
|
||||
descriptorCacheTests();
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--unused-stencil-reference") {
|
||||
stencilReferenceUnusedTests();
|
||||
return 0;
|
||||
@@ -8567,6 +9074,11 @@ int main(int argc, char** argv) {
|
||||
std::cout << "pipeline library clear waits for active part builds\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-lazy-buffer-pool") {
|
||||
pipelineStoreLazyBufferPoolTests();
|
||||
std::cout << "cached pipelines survive lazy buffer pool creation\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--sparse-buffer-holes") {
|
||||
bool passed = false;
|
||||
{
|
||||
@@ -8692,6 +9204,7 @@ int main(int argc, char** argv) {
|
||||
RunGuestLeaseWaitTests();
|
||||
ResourceContentKeyTests(false);
|
||||
stateTests();
|
||||
pipelineStoreLazyBufferPoolTests();
|
||||
retainedPixelStageTests();
|
||||
pipelineLibrarySetTests();
|
||||
pipelineLibraryClearWaitsForBuildTests();
|
||||
@@ -8706,6 +9219,7 @@ int main(int argc, char** argv) {
|
||||
memoryValueFillTests();
|
||||
ZExportTests();
|
||||
unorm10_11_11TargetTests();
|
||||
float11_11_10TargetTests();
|
||||
DepthBoundsBiasTests();
|
||||
conservativeZExportTests();
|
||||
earlyFragmentTestsTests();
|
||||
@@ -8719,8 +9233,10 @@ int main(int argc, char** argv) {
|
||||
ConservativeRasterizationTests();
|
||||
DisabledColorTests();
|
||||
OpaqueDestinationAlphaTests();
|
||||
Color565TargetTests();
|
||||
Color1555TargetTests();
|
||||
BlendBypassTests();
|
||||
UnclampedNormalizedColorTests();
|
||||
CompactedExportTests();
|
||||
ReversedComponentOrderTests();
|
||||
ReversedComponentOrderBlendTests();
|
||||
@@ -8761,6 +9277,7 @@ int main(int argc, char** argv) {
|
||||
oversizedDescriptorTests();
|
||||
guestBufferViewBytesTests();
|
||||
descriptorSnapshotTests();
|
||||
dispatchMoveTests();
|
||||
descriptorCacheTests();
|
||||
bufferBudgetPlacementTests();
|
||||
imageTableAllocationTests();
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
#define SPV_ENABLE_UTILITY_CODE
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#undef SPV_ENABLE_UTILITY_CODE
|
||||
#include "IntermediateRepresentation/IrProgram.hpp"
|
||||
#include "Optimization/ShaderStageInputInfo.hpp"
|
||||
#include "SpirvBackend/SpirvEmitter.hpp"
|
||||
#include "PipelineSpecialization.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
void require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct LoadCase {
|
||||
ResourceKind kind;
|
||||
IrOpcode opcode;
|
||||
IrType type;
|
||||
std::uint32_t bits = 32;
|
||||
std::uint32_t components = 1;
|
||||
};
|
||||
|
||||
std::vector<std::uint32_t> emitLoad(const LoadCase& test, bool active, std::uint32_t offset, std::uint32_t records, bool robust) {
|
||||
IrProgram program;
|
||||
program.SetWaveSize(32);
|
||||
program.Resources().stage = IrShaderStage::Compute;
|
||||
program.Resources().srtPlanComplete = true;
|
||||
program.Resources().resourceTrackingComplete = true;
|
||||
program.Metadata().shaderInfoComplete = true;
|
||||
program.Metadata().bindingLayoutComplete = true;
|
||||
auto& block = program.CreateBlock();
|
||||
program.SetEntryBlock(block);
|
||||
program.BlockOrder().push_back(&block);
|
||||
program.Metadata().blockInfo.emplace_back();
|
||||
const auto constant = [&](std::uint32_t number) -> IrValue& {
|
||||
auto& value = program.CreateValue(IrOpcode::Void, IrType::U32);
|
||||
value.SetImmediateU32(number);
|
||||
return value;
|
||||
};
|
||||
const auto emit = [&](IrOpcode opcode, IrType type, std::initializer_list<IrValue*> arguments) -> IrValue& {
|
||||
auto& value = program.CreateValue(opcode, type);
|
||||
for (auto* argument : arguments) value.AddArgument(argument);
|
||||
block.AppendInstruction(&value);
|
||||
return value;
|
||||
};
|
||||
auto& enabled = program.CreateValue(IrOpcode::Void, IrType::Bool);
|
||||
enabled.SetImmediateBool(active);
|
||||
auto& zero = constant(0);
|
||||
auto& address = constant(offset);
|
||||
MemoryInfo memory;
|
||||
memory.kind = test.kind;
|
||||
memory.dataBits = test.bits;
|
||||
memory.dataDwords = test.components;
|
||||
memory.componentCount = test.components;
|
||||
program.Resources().memoryInfo.push_back(memory);
|
||||
if (test.kind == ResourceKind::Scratch) {
|
||||
program.Info().scratchDwords = 16;
|
||||
auto& handle = emit(IrOpcode::GetScratchResource, IrType::AddressResource, {});
|
||||
emit(test.opcode, test.type, {&handle, &address, &zero, &enabled});
|
||||
} else if (test.kind == ResourceKind::Buffer) {
|
||||
program.Resources().memoryInfo.front().coherent = true;
|
||||
program.Info().buffers.emplace_back().read = true;
|
||||
program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Buffers, {0}});
|
||||
program.Metadata().bindings.pushDataStartDword = 0;
|
||||
program.Metadata().bindings.memoryOffsetCount = 1;
|
||||
auto& size = constant(records);
|
||||
auto& handle = emit(IrOpcode::GetBufferResource, IrType::BufferResource, {&zero, &zero, &size, &zero});
|
||||
emit(test.opcode, test.type, {&handle, &zero, &address, &zero, &enabled});
|
||||
} else {
|
||||
if (test.kind == ResourceKind::Gds) program.Metadata().bindings.descriptors.push_back({DescriptorBindingKind::Gds, {0}});
|
||||
emit(test.opcode, test.type, {&address, &enabled});
|
||||
}
|
||||
ShaderComputeInputInfo compute;
|
||||
compute.threadsNum[0] = 32;
|
||||
compute.threadsNum[1] = 1;
|
||||
compute.threadsNum[2] = 1;
|
||||
compute.ldsSizeDwords = 16;
|
||||
compute.scratchSizeDwords = 16;
|
||||
compute.hostSubgroupSize = 32;
|
||||
compute.waveSize = 32;
|
||||
ShaderStageInputInfo inputs;
|
||||
inputs.compute = &compute;
|
||||
CompiledBindingLayout bindings;
|
||||
bindings.layout = program.Metadata().bindings;
|
||||
SpirvTargetOptions target{};
|
||||
target.vulkanVersion = 0x00401000;
|
||||
target.spirvVersion = 0x00010300;
|
||||
target.subgroupSize = 32;
|
||||
target.robustBufferAccess2 = robust;
|
||||
return SpirvEmitter().Emit(program, inputs, bindings, target);
|
||||
}
|
||||
|
||||
class AccessTrace {
|
||||
public:
|
||||
AccessTrace(const std::vector<std::uint32_t>& module, std::uint32_t misalignment = 0, bool valid = true)
|
||||
: words(module), misalignment(misalignment), valid(valid) {
|
||||
require(words.size() >= 5, "SPIR-V module header is missing");
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> 16;
|
||||
require(count != 0 && count <= words.size() - offset, "SPIR-V instruction is truncated");
|
||||
const auto instruction = std::span(words).subspan(offset, count);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffff);
|
||||
bool result = false;
|
||||
bool type = false;
|
||||
spv::HasResultAndType(opcode, &result, &type);
|
||||
if (result) definitions.emplace(instruction[type ? 2 : 1], instruction);
|
||||
if (opcode == spv::OpName) names.emplace(instruction[1], std::string(reinterpret_cast<const char*>(instruction.data() + 2)));
|
||||
if (opcode == spv::OpDecorate && instruction[2] == spv::DecorationSpecId) specializations.emplace(instruction[1], instruction[3]);
|
||||
if (opcode == spv::OpLabel) labels.emplace(instruction[1], offset);
|
||||
if (opcode == spv::OpEntryPoint) entryFunction = instruction[2];
|
||||
offset += count;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Run() {
|
||||
std::size_t cursor = 0;
|
||||
bool entry = false;
|
||||
for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> 16) {
|
||||
const auto opcode = static_cast<spv::Op>(words[offset] & 0xffff);
|
||||
if (opcode == spv::OpFunction) entry = words[offset + 2] == entryFunction;
|
||||
if (entry && opcode == spv::OpLabel) { cursor = offset; break; }
|
||||
}
|
||||
require(cursor != 0, "SPIR-V entry block is missing");
|
||||
std::vector<std::uint32_t> reads;
|
||||
std::uint32_t previous = 0;
|
||||
std::uint32_t current = 0;
|
||||
for (std::size_t steps = 0; steps < 4096; ++steps) {
|
||||
const auto instruction = std::span(words).subspan(cursor, words[cursor] >> 16);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffff);
|
||||
if (opcode == spv::OpLabel) current = instruction[1];
|
||||
if (opcode == spv::OpPhi) {
|
||||
bool found = false;
|
||||
for (std::size_t index = 3; index + 1 < instruction.size(); index += 2) {
|
||||
if (instruction[index + 1] == previous) { phiValues[instruction[2]] = instruction[index]; found = true; }
|
||||
}
|
||||
require(found, "SPIR-V phi has no incoming edge for the executed path");
|
||||
}
|
||||
if (opcode == spv::OpLoad && isGuestPointer(instruction[3])) {
|
||||
const auto pointer = definition(instruction[3]);
|
||||
require((pointer[0] & 0xffff) == spv::OpAccessChain, "guest read is not an indexed access");
|
||||
reads.push_back(value(pointer.back()));
|
||||
if (rootName(instruction[3]) == "scratch_dwords") require(pointerStorage(instruction[3]) == spv::StorageClassFunction, "scratch selection accessed non-private storage");
|
||||
if (rootName(instruction[3]).starts_with("buffers")) require(instruction.size() == 5 && (instruction[4] & spv::MemoryAccessVolatileMask), "coherent buffer read lost its volatile access");
|
||||
}
|
||||
if (opcode == spv::OpBranch || opcode == spv::OpBranchConditional) {
|
||||
const auto destination = opcode == spv::OpBranch ? instruction[1] : instruction[value(instruction[1]) ? 2 : 3];
|
||||
previous = current;
|
||||
cursor = labels.at(destination);
|
||||
continue;
|
||||
}
|
||||
if (opcode == spv::OpReturn) return reads;
|
||||
require(opcode != spv::OpFunctionCall && opcode != spv::OpSwitch && opcode != spv::OpLoopMerge, "access fixture unexpectedly requires a call, switch or loop");
|
||||
cursor += instruction.size();
|
||||
require(cursor < words.size(), "SPIR-V execution fell outside the function");
|
||||
}
|
||||
throw std::runtime_error("SPIR-V access trace exceeded its bounded instruction budget");
|
||||
}
|
||||
|
||||
private:
|
||||
std::span<const std::uint32_t> definition(std::uint32_t id) const {
|
||||
const auto found = definitions.find(id);
|
||||
require(found != definitions.end(), "SPIR-V access condition refers to an undefined value");
|
||||
return found->second;
|
||||
}
|
||||
|
||||
std::string rootName(std::uint32_t id) const {
|
||||
const auto instruction = definition(id);
|
||||
if ((instruction[0] & 0xffff) == spv::OpAccessChain) return rootName(instruction[3]);
|
||||
const auto found = names.find(id);
|
||||
return found == names.end() ? std::string() : found->second;
|
||||
}
|
||||
|
||||
std::uint32_t pointerStorage(std::uint32_t id) const {
|
||||
const auto instruction = definition(id);
|
||||
if ((instruction[0] & 0xffff) == spv::OpAccessChain) return pointerStorage(instruction[3]);
|
||||
require((instruction[0] & 0xffff) == spv::OpVariable, "access pointer has no variable root");
|
||||
return instruction[3];
|
||||
}
|
||||
|
||||
bool isGuestPointer(std::uint32_t id) const {
|
||||
const auto name = rootName(id);
|
||||
return name == "lds_dwords" || name == "gds" || name == "scratch_dwords" || name.starts_with("buffers");
|
||||
}
|
||||
|
||||
std::uint32_t value(std::uint32_t id) const {
|
||||
const auto instruction = definition(id);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffff);
|
||||
const auto argument = [&](std::size_t index) { return value(instruction[index]); };
|
||||
switch (opcode) {
|
||||
case spv::OpConstantTrue: return 1;
|
||||
case spv::OpConstantFalse: case spv::OpConstantNull: return 0;
|
||||
case spv::OpConstant: return instruction[3];
|
||||
case spv::OpSpecConstant: {
|
||||
const auto spec = specializations.at(id);
|
||||
if (spec == PipelineSpecialization::BufferBase) return 0;
|
||||
if (spec == PipelineSpecialization::BufferBase + 1) return 0x30001000;
|
||||
if (spec == PipelineSpecialization::BufferBase + 2) return valid;
|
||||
if (spec == PipelineSpecialization::BufferBase + 3) return misalignment;
|
||||
if (spec == PipelineSpecialization::PushDataOffset) return 0;
|
||||
throw std::runtime_error("unexpected specialization in access fixture");
|
||||
}
|
||||
case spv::OpSelect: return argument(3) ? argument(4) : argument(5);
|
||||
case spv::OpPhi: return value(phiValues.at(id));
|
||||
case spv::OpIAdd: return argument(3) + argument(4);
|
||||
case spv::OpISub: return argument(3) - argument(4);
|
||||
case spv::OpIMul: return argument(3) * argument(4);
|
||||
case spv::OpShiftLeftLogical:
|
||||
require(argument(4) < 32, "undefined shift in access condition");
|
||||
return argument(3) << argument(4);
|
||||
case spv::OpShiftRightLogical:
|
||||
require(argument(4) < 32, "undefined shift in access condition");
|
||||
return argument(3) >> argument(4);
|
||||
case spv::OpBitwiseAnd: return argument(3) & argument(4);
|
||||
case spv::OpBitwiseOr: return argument(3) | argument(4);
|
||||
case spv::OpBitFieldUExtract: {
|
||||
const auto count = argument(5);
|
||||
require(count <= 32 && argument(4) <= 32 - count, "invalid bitfield in access condition");
|
||||
return (argument(3) >> argument(4)) & (count == 32 ? 0xffffffffu : (1u << count) - 1);
|
||||
}
|
||||
case spv::OpIEqual: return argument(3) == argument(4);
|
||||
case spv::OpINotEqual: return argument(3) != argument(4);
|
||||
case spv::OpULessThan: return argument(3) < argument(4);
|
||||
case spv::OpULessThanEqual: return argument(3) <= argument(4);
|
||||
case spv::OpUGreaterThanEqual: return argument(3) >= argument(4);
|
||||
case spv::OpLogicalAnd: return argument(3) && argument(4);
|
||||
case spv::OpLogicalOr: return argument(3) || argument(4);
|
||||
case spv::OpLogicalNot: return !argument(3);
|
||||
case spv::OpArrayLength:
|
||||
require(isGuestPointer(instruction[3]), "unknown array length in access fixture");
|
||||
return 16;
|
||||
case spv::OpLoad:
|
||||
require(!isGuestPointer(instruction[3]), "guest data unexpectedly controls a fixture access");
|
||||
require(rootName(instruction[3]) == "vsharp" || rootName(instruction[3]).find("subgroup") != std::string::npos, "unknown input to access condition");
|
||||
return 0;
|
||||
default: throw std::runtime_error("unsupported opcode in access condition: " + std::to_string(opcode));
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<std::uint32_t>& words;
|
||||
std::uint32_t misalignment;
|
||||
bool valid;
|
||||
std::uint32_t entryFunction = 0;
|
||||
std::map<std::uint32_t, std::span<const std::uint32_t>> definitions;
|
||||
std::map<std::uint32_t, std::string> names;
|
||||
std::map<std::uint32_t, std::uint32_t> specializations;
|
||||
std::map<std::uint32_t, std::size_t> labels;
|
||||
std::map<std::uint32_t, std::uint32_t> phiValues;
|
||||
};
|
||||
|
||||
constexpr std::uint32_t Unreachable = 0xfffffffeu;
|
||||
|
||||
std::uint32_t ldsLength(const std::vector<std::uint32_t>& words) {
|
||||
std::map<std::uint32_t, std::span<const std::uint32_t>> definitions;
|
||||
std::uint32_t variable = 0;
|
||||
for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> 16) {
|
||||
const auto instruction = std::span(words).subspan(offset, words[offset] >> 16);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffff);
|
||||
if (opcode == spv::OpName && std::string(reinterpret_cast<const char*>(instruction.data() + 2)) == "lds_dwords") variable = instruction[1];
|
||||
bool result = false;
|
||||
bool type = false;
|
||||
spv::HasResultAndType(opcode, &result, &type);
|
||||
if (result) definitions.emplace(instruction[type ? 2 : 1], instruction);
|
||||
}
|
||||
require(variable != 0, "LDS array is missing");
|
||||
const auto pointer = definitions.at(definitions.at(variable)[1]);
|
||||
const auto array = definitions.at(pointer[3]);
|
||||
require((array[0] & 0xffff) == spv::OpTypeArray, "LDS variable is not an array");
|
||||
return definitions.at(array[3])[3];
|
||||
}
|
||||
|
||||
void expectReads(const LoadCase& test, bool robust, bool active, std::uint32_t offset, std::vector<std::uint32_t> expected, std::uint32_t records = 64, std::uint32_t misalignment = 0, bool valid = true) {
|
||||
const auto module = emitLoad(test, active, offset, records, robust);
|
||||
const auto actual = AccessTrace(module, misalignment, valid).Run();
|
||||
if (actual != expected) {
|
||||
std::string detail = std::string(robust ? "robust" : "plain") + " guest reads:";
|
||||
for (const auto index : actual) detail += " " + std::to_string(index);
|
||||
detail += "; expected:";
|
||||
for (const auto index : expected) detail += " " + std::to_string(index);
|
||||
throw std::runtime_error(detail);
|
||||
}
|
||||
}
|
||||
|
||||
void ldsLoads(IrOpcode opcode, IrType type, std::uint32_t bits, std::uint32_t components) {
|
||||
const LoadCase test{ResourceKind::Lds, opcode, type, bits, components};
|
||||
require(ldsLength(emitLoad(test, true, 8, 64, false)) == 16u, "LDS array declares more than the program's dwords");
|
||||
for (const bool robust : {false, true}) {
|
||||
expectReads(test, robust, false, 8, {});
|
||||
expectReads(test, robust, true, 84, {});
|
||||
std::vector<std::uint32_t> indices;
|
||||
for (std::uint32_t index = 0; index < components; ++index) indices.push_back(2 + index);
|
||||
expectReads(test, robust, true, 8, indices);
|
||||
expectReads(test, robust, true, 60, {15});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
unsigned failures = 0;
|
||||
const auto run = [&](std::string_view name, const auto& test) {
|
||||
try {
|
||||
test();
|
||||
std::cout << "guarded memory loads: " << name << " passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
++failures;
|
||||
std::cout << "guarded memory loads: " << name << ": " << error.what() << '\n';
|
||||
}
|
||||
};
|
||||
run("LDS word keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU32, IrType::U32, 32, 1); });
|
||||
run("LDS subword keeps its branch", [] {
|
||||
ldsLoads(IrOpcode::LoadSharedU8, IrType::U8, 8, 1);
|
||||
ldsLoads(IrOpcode::LoadSharedU16, IrType::U16, 16, 1);
|
||||
});
|
||||
run("LDS wide keeps its branch", [] { ldsLoads(IrOpcode::LoadSharedU32x4, IrType::U32x4, 32, 4); });
|
||||
run("GDS word", [] {
|
||||
const LoadCase test{ResourceKind::Gds, IrOpcode::LoadSharedU32, IrType::U32, 32, 1};
|
||||
expectReads(test, false, false, 8, {});
|
||||
expectReads(test, false, true, 64, {});
|
||||
expectReads(test, false, true, 8, {2});
|
||||
expectReads(test, true, false, 8, {Unreachable});
|
||||
expectReads(test, true, true, 64, {Unreachable});
|
||||
expectReads(test, true, true, 8, {2});
|
||||
expectReads(test, true, true, 60, {15});
|
||||
});
|
||||
run("coherent buffer bounds", [] {
|
||||
const LoadCase test{ResourceKind::Buffer, IrOpcode::LoadBufferU32, IrType::U32};
|
||||
for (const bool robust : {false, true}) expectReads(test, robust, false, 8, {});
|
||||
expectReads(test, false, true, 64, {}, 128);
|
||||
expectReads(test, false, true, 60, {}, 32);
|
||||
expectReads(test, false, true, 8, {}, 64, 0, false);
|
||||
expectReads(test, true, true, 64, {Unreachable, Unreachable}, 128);
|
||||
expectReads(test, true, true, 60, {Unreachable, Unreachable}, 32);
|
||||
expectReads(test, true, true, 8, {Unreachable, Unreachable}, 64, 0, false);
|
||||
});
|
||||
run("coherent buffer high word", [] {
|
||||
const LoadCase test{ResourceKind::Buffer, IrOpcode::LoadBufferU32, IrType::U32};
|
||||
expectReads(test, false, true, 8, {2});
|
||||
expectReads(test, false, true, 8, {2, 3}, 64, 1);
|
||||
expectReads(test, false, true, 60, {}, 64, 1);
|
||||
expectReads(test, true, true, 8, {2, 2});
|
||||
expectReads(test, true, true, 8, {2, 3}, 64, 1);
|
||||
expectReads(test, true, true, 60, {Unreachable, Unreachable}, 64, 1);
|
||||
});
|
||||
run("private scratch selection", [] {
|
||||
const LoadCase test{ResourceKind::Scratch, IrOpcode::LoadAddressU32, IrType::U32};
|
||||
for (const bool robust : {false, true}) {
|
||||
expectReads(test, robust, false, 8, {0});
|
||||
expectReads(test, robust, true, 64, {0});
|
||||
expectReads(test, robust, true, 8, {2});
|
||||
}
|
||||
});
|
||||
if (failures != 0) {
|
||||
std::cerr << "guarded memory loads: a guest load reached a location the guarded program could not\n";
|
||||
return 1;
|
||||
}
|
||||
std::cout << "Guarded memory load tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp"
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace std::chrono_literals;
|
||||
using AgcDriver::HandoffSpinWindowFor;
|
||||
using AgcDriver::LockSpinning;
|
||||
using AgcDriver::SpinUntil;
|
||||
|
||||
void Require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
void CheckWindowPolicy() {
|
||||
Require(HandoffSpinWindowFor(nullptr, 20) == 150us, "20 usable processors do not spin for 150 us");
|
||||
Require(HandoffSpinWindowFor(nullptr, 9) == 150us, "9 usable processors do not spin for 150 us");
|
||||
Require(HandoffSpinWindowFor(nullptr, 8) == 0ns, "8 usable processors spin");
|
||||
Require(HandoffSpinWindowFor(nullptr, 4) == 0ns, "4 usable processors spin");
|
||||
Require(HandoffSpinWindowFor(nullptr, 0) == 0ns, "an unknown processor count spins");
|
||||
Require(HandoffSpinWindowFor("0", 20) == 0ns, "APS5_HANDOFF_SPIN_US=0 still spins");
|
||||
Require(HandoffSpinWindowFor("400", 2) == 400us, "APS5_HANDOFF_SPIN_US=400 does not set 400 us on 2 logical processors");
|
||||
Require(HandoffSpinWindowFor("-5", 20) == 0ns, "a negative window spins");
|
||||
Require(HandoffSpinWindowFor("text", 20) == 0ns, "a window that is no number spins");
|
||||
Require(HandoffSpinWindowFor("99999999999", 20) == 10000us, "a huge window is not limited to 10000 us");
|
||||
}
|
||||
|
||||
void CheckUsableProcessors() {
|
||||
const unsigned usable = AgcDriver::UsableProcessors();
|
||||
Require(usable >= 1, "no usable processor was counted");
|
||||
Require(usable <= (std::max)(std::thread::hardware_concurrency(), 64u), "more usable processors than the machine has");
|
||||
}
|
||||
|
||||
void CheckSpinUntil() {
|
||||
int calls = 0;
|
||||
Require(SpinUntil(0ns, [&] { ++calls; return true; }) && calls == 1, "a ready condition was evaluated more than once");
|
||||
calls = 0;
|
||||
Require(!SpinUntil(0ns, [&] { ++calls; return false; }) && calls == 1, "a zero window evaluated the condition more than once");
|
||||
calls = 0;
|
||||
Require(SpinUntil(1s, [&] { return ++calls == 5; }) && calls == 5, "a condition that holds on its fifth evaluation was not seen");
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
Require(!SpinUntil(3ms, [] { return false; }), "a condition that never holds was reported ready");
|
||||
Require(std::chrono::steady_clock::now() - start >= 3ms, "the spin returned before its window ended");
|
||||
std::atomic<bool> flag{false};
|
||||
std::thread setter([&] {
|
||||
std::this_thread::sleep_for(500us);
|
||||
flag.store(true, std::memory_order_release);
|
||||
});
|
||||
const bool seen = SpinUntil(5s, [&] { return flag.load(std::memory_order_acquire); });
|
||||
setter.join();
|
||||
Require(seen, "a flag set by another thread was not seen within the window");
|
||||
}
|
||||
|
||||
template<typename TMutex>
|
||||
void Contend(std::chrono::nanoseconds window, const char* what) {
|
||||
constexpr int Workers = 4;
|
||||
constexpr int Rounds = 20000;
|
||||
TMutex mutex;
|
||||
std::atomic<int> inside{0};
|
||||
std::atomic<bool> overlapped{false};
|
||||
std::uint64_t counter = 0;
|
||||
std::vector<std::thread> workers;
|
||||
for (int worker = 0; worker < Workers; ++worker) {
|
||||
workers.emplace_back([&] {
|
||||
for (int round = 0; round < Rounds; ++round) {
|
||||
LockSpinning(mutex, window);
|
||||
if (inside.fetch_add(1) != 0) overlapped.store(true);
|
||||
++counter;
|
||||
inside.fetch_sub(1);
|
||||
mutex.unlock();
|
||||
}
|
||||
});
|
||||
}
|
||||
for (auto& worker : workers) worker.join();
|
||||
Require(!overlapped.load(), std::string(what) + ": two threads held the mutex at once");
|
||||
Require(counter == static_cast<std::uint64_t>(Workers) * Rounds, std::string(what) + ": increments were lost");
|
||||
}
|
||||
|
||||
void CheckBlockingFallback() {
|
||||
std::mutex mutex;
|
||||
std::atomic<bool> held{false};
|
||||
std::atomic<bool> released{false};
|
||||
std::thread holder([&] {
|
||||
mutex.lock();
|
||||
held.store(true);
|
||||
std::this_thread::sleep_for(20ms);
|
||||
released.store(true);
|
||||
mutex.unlock();
|
||||
});
|
||||
while (!held.load()) std::this_thread::yield();
|
||||
LockSpinning(mutex, 100us);
|
||||
const bool afterRelease = released.load();
|
||||
mutex.unlock();
|
||||
holder.join();
|
||||
Require(afterRelease, "a spin that outlasted its window did not wait for the holder");
|
||||
}
|
||||
|
||||
void CheckRecursion() {
|
||||
std::recursive_mutex mutex;
|
||||
LockSpinning(mutex, 100us);
|
||||
LockSpinning(mutex, 100us);
|
||||
mutex.unlock();
|
||||
mutex.unlock();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
CheckWindowPolicy();
|
||||
CheckUsableProcessors();
|
||||
CheckSpinUntil();
|
||||
Contend<std::mutex>(0ns, "plain lock");
|
||||
Contend<std::mutex>(50us, "spin 50 us");
|
||||
Contend<std::mutex>(1ms, "spin 1 ms");
|
||||
Contend<std::recursive_mutex>(150us, "recursive mutex");
|
||||
CheckBlockingFallback();
|
||||
CheckRecursion();
|
||||
std::cout << "handoff spin tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ struct Bind {
|
||||
struct MockDevice {
|
||||
std::uintptr_t next = 0x1000;
|
||||
std::set<VkBuffer> buffers;
|
||||
std::set<VkFence> fences;
|
||||
std::set<VkBuffer> sparse;
|
||||
std::map<VkBuffer, VkDeviceSize> sizes;
|
||||
std::map<VkDeviceMemory, VkDeviceSize> memories;
|
||||
@@ -64,10 +65,25 @@ MockDevice mock;
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockCreateFence(VkDevice, const VkFenceCreateInfo*, const VkAllocationCallbacks*, VkFence* fence) {
|
||||
*fence = reinterpret_cast<VkFence>(mock.next++);
|
||||
mock.fences.insert(*fence);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence, const VkAllocationCallbacks*) {}
|
||||
VKAPI_ATTR void VKAPI_CALL mockDestroyFence(VkDevice, VkFence fence, const VkAllocationCallbacks*) {
|
||||
mock.fences.erase(fence);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockAllocateCommandBuffers(VkDevice, const VkCommandBufferAllocateInfo*, VkCommandBuffer* commands) {
|
||||
*commands = reinterpret_cast<VkCommandBuffer>(mock.next++);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockResetFences(VkDevice, std::uint32_t, const VkFence*) { return VK_SUCCESS; }
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockResetCommandBuffer(VkCommandBuffer, VkCommandBufferResetFlags) { return VK_SUCCESS; }
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockBeginCommandBuffer(VkCommandBuffer, const VkCommandBufferBeginInfo*) { return VK_SUCCESS; }
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockEndCommandBuffer(VkCommandBuffer) { return VK_SUCCESS; }
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockQueueSubmit(VkQueue, std::uint32_t, const VkSubmitInfo*, VkFence) { return VK_SUCCESS; }
|
||||
VKAPI_ATTR void VKAPI_CALL mockCmdCopyBuffer(VkCommandBuffer, VkBuffer, VkBuffer, std::uint32_t, const VkBufferCopy*) {}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL mockWaitForFences(VkDevice, std::uint32_t, const VkFence*, VkBool32, std::uint64_t) {
|
||||
return VK_SUCCESS;
|
||||
@@ -133,6 +149,13 @@ VKAPI_ATTR VkDeviceAddress VKAPI_CALL mockGetBufferDeviceAddress(VkDevice, const
|
||||
|
||||
PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) {
|
||||
static const std::map<std::string_view, PFN_vkVoidFunction> table{
|
||||
{"vkAllocateCommandBuffers", reinterpret_cast<PFN_vkVoidFunction>(mockAllocateCommandBuffers)},
|
||||
{"vkResetFences", reinterpret_cast<PFN_vkVoidFunction>(mockResetFences)},
|
||||
{"vkResetCommandBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockResetCommandBuffer)},
|
||||
{"vkBeginCommandBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockBeginCommandBuffer)},
|
||||
{"vkEndCommandBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockEndCommandBuffer)},
|
||||
{"vkQueueSubmit", reinterpret_cast<PFN_vkVoidFunction>(mockQueueSubmit)},
|
||||
{"vkCmdCopyBuffer", reinterpret_cast<PFN_vkVoidFunction>(mockCmdCopyBuffer)},
|
||||
{"vkCreateFence", reinterpret_cast<PFN_vkVoidFunction>(mockCreateFence)},
|
||||
{"vkDestroyFence", reinterpret_cast<PFN_vkVoidFunction>(mockDestroyFence)},
|
||||
{"vkWaitForFences", reinterpret_cast<PFN_vkVoidFunction>(mockWaitForFences)},
|
||||
@@ -228,6 +251,36 @@ std::uint64_t RangeBytes() {
|
||||
return (UdmabufLimitBytes() / ChunkBytes + 1) * ChunkBytes + (std::uint64_t{1} << 20u);
|
||||
}
|
||||
|
||||
void SettlingSurvivesDeviceReplacement(bool reverse) {
|
||||
const auto baselineBuffers = mock.buffers.size();
|
||||
const auto baselineMemories = mock.memories.size();
|
||||
const auto baselineFences = mock.fences.size();
|
||||
SharedRange range(8u << 20u, 0, true);
|
||||
auto first = mockContext();
|
||||
first.device = reinterpret_cast<VkDevice>(mock.next++);
|
||||
first.settleQueue = reinterpret_cast<VkQueue>(mock.next++);
|
||||
first.settlePool = reinterpret_cast<VkCommandPool>(mock.next++);
|
||||
std::mutex queueMutex;
|
||||
first.settleQueueMutex = &queueMutex;
|
||||
auto second = first;
|
||||
second.device = reinterpret_cast<VkDevice>(mock.next++);
|
||||
second.settlePool = reinterpret_cast<VkCommandPool>(mock.next++);
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
for (const auto& context : {first, second, first}) {
|
||||
const auto* imported = HostImportFor(context, range.Address(), 4096);
|
||||
Expect(imported != nullptr && imported->dmaBuf, "replacement device did not settle its dma-buf import");
|
||||
}
|
||||
ClearHostImports(reverse ? second.device : first.device);
|
||||
Expect(mock.fences.size() == baselineFences + 1, "clearing one device did not retain exactly the other device's settle fence");
|
||||
ClearHostImports(reverse ? first.device : second.device);
|
||||
Expect(mock.buffers.size() == baselineBuffers, "device replacement leaked a settle buffer");
|
||||
Expect(mock.memories.size() == baselineMemories, "device replacement leaked settle memory");
|
||||
Expect(mock.fences.size() == baselineFences, "device replacement leaked a settle fence");
|
||||
ClearHostImports(first.device);
|
||||
ClearHostImports(second.device);
|
||||
Expect(mock.fences.size() == baselineFences, "repeated device cleanup changed the live fences");
|
||||
}
|
||||
|
||||
void ImportsALargeRangeAsOneSparseBuffer() {
|
||||
const auto bytes = RangeBytes();
|
||||
SharedRange range(bytes, 0, true);
|
||||
@@ -299,6 +352,8 @@ int main() {
|
||||
SetHostImportBudget(~std::uint64_t{0});
|
||||
GuestArena::GuestArenaSetSharedBacking_nid_postfix(Resolve);
|
||||
try {
|
||||
SettlingSurvivesDeviceReplacement(false);
|
||||
SettlingSurvivesDeviceReplacement(true);
|
||||
ImportsALargeRangeAsOneSparseBuffer();
|
||||
UnalignedOffsetIsDecidedBeforeImporting();
|
||||
FailureAfterTheDecisionThrows(false, VK_SUCCESS, "UDMABUF_CREATE");
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
#include "Recompiler.hpp"
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
void require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
std::uint64_t workgroupBytes(std::span<const std::uint32_t> words) {
|
||||
std::map<std::uint32_t, std::span<const std::uint32_t>> types;
|
||||
std::map<std::uint32_t, std::uint32_t> constants;
|
||||
std::vector<std::uint32_t> pointers;
|
||||
for (std::size_t offset = 5; offset < words.size();) {
|
||||
const auto count = words[offset] >> 16u;
|
||||
require(count != 0u && count <= words.size() - offset, "malformed SPIR-V instruction");
|
||||
const auto instruction = words.subspan(offset, count);
|
||||
const auto opcode = static_cast<spv::Op>(instruction[0] & 0xffffu);
|
||||
if (opcode == spv::OpTypeInt || opcode == spv::OpTypeFloat || opcode == spv::OpTypeVector || opcode == spv::OpTypeArray || opcode == spv::OpTypeStruct || opcode == spv::OpTypePointer) types.emplace(instruction[1], instruction);
|
||||
if (opcode == spv::OpConstant) constants.emplace(instruction[2], instruction[3]);
|
||||
if (opcode == spv::OpVariable && instruction[3] == spv::StorageClassWorkgroup) pointers.push_back(instruction[1]);
|
||||
offset += count;
|
||||
}
|
||||
const std::function<std::uint64_t(std::uint32_t)> size = [&](std::uint32_t id) -> std::uint64_t {
|
||||
const auto type = types.at(id);
|
||||
switch (static_cast<spv::Op>(type[0] & 0xffffu)) {
|
||||
case spv::OpTypeInt: case spv::OpTypeFloat: return type[2] / 8u;
|
||||
case spv::OpTypeVector: return size(type[2]) * type[3];
|
||||
case spv::OpTypeArray: return size(type[2]) * constants.at(type[3]);
|
||||
case spv::OpTypeStruct: {
|
||||
std::uint64_t bytes = 0;
|
||||
for (const auto member : type.subspan(2)) bytes += size(member);
|
||||
return bytes;
|
||||
}
|
||||
default: throw std::runtime_error("unexpected Workgroup variable type");
|
||||
}
|
||||
};
|
||||
std::uint64_t bytes = 0;
|
||||
for (const auto pointer : pointers) {
|
||||
const auto type = types.at(pointer);
|
||||
require((type[0] & 0xffffu) == spv::OpTypePointer, "Workgroup variable is not a pointer");
|
||||
bytes += size(type[3]);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
RecompileResult compile(std::uint32_t waveSize, std::uint32_t limit) {
|
||||
static constexpr std::array<std::uint32_t, 8> code{
|
||||
0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u,
|
||||
0xd9000000u, 0x00000201u, 0xbf8cc07fu, 0xbf810000u
|
||||
};
|
||||
static constexpr std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
static constexpr std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
static constexpr std::array<std::uint32_t, 4> userData{};
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}};
|
||||
request.context.waveSize = waveSize;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 16u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00403000u;
|
||||
request.target.spirvVersion = 0x00010600u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.bdaAbiVersion = 1;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
|
||||
request.target.maxWorkgroupInvocations = 1024;
|
||||
request.target.maxWorkgroupSharedMemoryBytes = limit;
|
||||
request.layout = {0, 0, 0, 128};
|
||||
request.useCache = false;
|
||||
return Recompile(request);
|
||||
}
|
||||
|
||||
void verify(std::uint32_t waveSize, std::uint32_t limit) {
|
||||
const auto result = compile(waveSize, limit);
|
||||
require(!result.spirv.empty(), "LDS atomic program did not compile");
|
||||
const auto declared = workgroupBytes(result.spirv.Words());
|
||||
require(declared <= limit, "declared " + std::to_string(declared) + " Workgroup bytes exceeds limit " + std::to_string(limit));
|
||||
require(result.workgroupMemoryDwords == (limit < 68u ? 17u : 0u), "incorrect device-memory LDS stride at limit " + std::to_string(limit));
|
||||
if (limit >= 68u) require(declared != 0u, "in-budget LDS unexpectedly lost its Workgroup allocation");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
unsigned failures = 0;
|
||||
for (const auto waveSize : {32u, 64u}) {
|
||||
for (const auto limit : {64u, 68u, 72u}) {
|
||||
const auto name = "wave" + std::to_string(waveSize) + " limit" + std::to_string(limit);
|
||||
try {
|
||||
verify(waveSize, limit);
|
||||
std::cout << "LDS lock budget: " << name << " passed\n";
|
||||
} catch (const std::exception& error) {
|
||||
++failures;
|
||||
const std::string message(error.what());
|
||||
std::cerr << "LDS lock budget: " << name << ": " << message.substr(0, message.find("RecompileRequest:")) << '\n';
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failures != 0) return 1;
|
||||
std::cout << "LDS lock budget tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
|
||||
#include "AllocationFailure.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
using namespace AgcDriver::Graphics;
|
||||
using AgcDriver::Tests::SetAllocationFailureCountdown;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool condition, const char* message) {
|
||||
if (condition) return;
|
||||
std::fprintf(stderr, "FAIL: %s\n", message);
|
||||
std::fflush(stderr);
|
||||
std::_Exit(1);
|
||||
}
|
||||
|
||||
void Schedule(VkDevice device, std::function<void()> work) {
|
||||
OptimizedPipeline pipeline;
|
||||
pipeline.device = device;
|
||||
pipeline.relink = std::move(work);
|
||||
pipeline.deferred = true;
|
||||
pipeline.Schedule();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Check(argc == 2, "expected wait, clear or stop");
|
||||
const std::string_view mode(argv[1]);
|
||||
Check(mode == "wait" || mode == "clear" || mode == "stop", "unknown drain mode");
|
||||
const auto device = reinterpret_cast<VkDevice>(std::uintptr_t{1});
|
||||
const auto otherDevice = reinterpret_cast<VkDevice>(std::uintptr_t{2});
|
||||
std::atomic<unsigned> completed{0};
|
||||
Schedule(device, [&] { ++completed; });
|
||||
WaitForOptimizedPipelines(device);
|
||||
Check(completed == 1, "the initial job did not run");
|
||||
|
||||
bool allocationFailed = false;
|
||||
OptimizedPipeline fresh;
|
||||
fresh.device = otherDevice;
|
||||
fresh.relink = [&] { ++completed; };
|
||||
fresh.deferred = true;
|
||||
SetAllocationFailureCountdown(0);
|
||||
try { fresh.Schedule(); } catch (const std::bad_alloc&) { allocationFailed = true; }
|
||||
SetAllocationFailureCountdown(-1);
|
||||
Check(allocationFailed, "the new device allocation did not fail");
|
||||
WaitForOptimizedPipelines(otherDevice);
|
||||
Check(completed == 1, "a rejected job ran");
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
bool entered = false;
|
||||
bool released = false;
|
||||
Schedule(device, [&] {
|
||||
std::unique_lock lock(mutex);
|
||||
entered = true;
|
||||
changed.notify_all();
|
||||
changed.wait(lock, [&] { return released; });
|
||||
});
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
Check(changed.wait_for(lock, 3s, [&] { return entered; }), "the worker did not start");
|
||||
}
|
||||
|
||||
allocationFailed = false;
|
||||
unsigned accepted = 0;
|
||||
for (unsigned attempt = 0; attempt < 512 && !allocationFailed; ++attempt) {
|
||||
OptimizedPipeline pipeline;
|
||||
pipeline.device = device;
|
||||
pipeline.relink = [&] { ++completed; };
|
||||
pipeline.deferred = true;
|
||||
SetAllocationFailureCountdown(0);
|
||||
try {
|
||||
pipeline.Schedule();
|
||||
++accepted;
|
||||
} catch (const std::bad_alloc&) {
|
||||
allocationFailed = true;
|
||||
}
|
||||
SetAllocationFailureCountdown(-1);
|
||||
}
|
||||
Check(allocationFailed, "the queued jobs never needed another allocation");
|
||||
Check(completed == 1, "queued jobs ran while the worker was held");
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
released = true;
|
||||
}
|
||||
changed.notify_all();
|
||||
|
||||
bool drained = false;
|
||||
std::thread drain([&] {
|
||||
if (mode == "stop") StopPipelineLibraryWorker();
|
||||
if (mode == "clear") ClearPipelineLibraries(device);
|
||||
else WaitForOptimizedPipelines(device);
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
drained = true;
|
||||
}
|
||||
changed.notify_all();
|
||||
});
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
Check(changed.wait_for(lock, 3s, [&] { return drained; }), "a failed queue allocation left an unfinished job");
|
||||
}
|
||||
drain.join();
|
||||
if (mode != "stop") {
|
||||
Check(completed == accepted + 1, "an accepted job was lost or ran twice");
|
||||
Schedule(device, [&] { ++completed; });
|
||||
Schedule(otherDevice, [&] { ++completed; });
|
||||
WaitForOptimizedPipelines(device);
|
||||
WaitForOptimizedPipelines(otherDevice);
|
||||
Check(completed == accepted + 3, "the queue did not recover after allocation failure");
|
||||
StopPipelineLibraryWorker();
|
||||
}
|
||||
WaitForOptimizedPipelines(device);
|
||||
ClearPipelineLibraries(device);
|
||||
ClearPipelineLibraries(otherDevice);
|
||||
Check(PipelineLibraryWorkers() == 0, "the pipeline worker did not stop");
|
||||
std::printf("%.*s: rejected new-device and queue-growth allocations; %u jobs accepted before rejection, queue drained\n",
|
||||
static_cast<int>(mode.size()), mode.data(), accepted);
|
||||
return 0;
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "ResidentPresent.hpp"
|
||||
#include "ImageTableAbi.hpp"
|
||||
#include "SampleLod_spv.h"
|
||||
#include "BufferUpdate_spv.h"
|
||||
#include "SampleArray_spv.h"
|
||||
@@ -311,6 +312,28 @@ public:
|
||||
extensionsEnabled.insert(extensionsEnabled.end(), libraryExtensions.begin(), libraryExtensions.end());
|
||||
address.pNext = &library;
|
||||
}
|
||||
VkPhysicalDeviceDescriptorIndexingFeaturesEXT indexing{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT};
|
||||
if (hasExtension(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &indexing};
|
||||
function<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(context.physical, &features);
|
||||
context.descriptorIndexing = indexing.shaderSampledImageArrayNonUniformIndexing == VK_TRUE && indexing.shaderStorageImageArrayNonUniformIndexing == VK_TRUE;
|
||||
context.partiallyBound = indexing.descriptorBindingPartiallyBound == VK_TRUE;
|
||||
context.variableDescriptorCount = indexing.descriptorBindingVariableDescriptorCount == VK_TRUE;
|
||||
context.runtimeDescriptorArray = indexing.runtimeDescriptorArray == VK_TRUE;
|
||||
}
|
||||
indexing = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DESCRIPTOR_INDEXING_FEATURES_EXT};
|
||||
if (context.descriptorIndexing && context.partiallyBound && context.variableDescriptorCount && context.runtimeDescriptorArray) {
|
||||
extensionsEnabled.push_back(VK_EXT_DESCRIPTOR_INDEXING_EXTENSION_NAME);
|
||||
indexing.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
|
||||
indexing.shaderStorageImageArrayNonUniformIndexing = VK_TRUE;
|
||||
indexing.descriptorBindingPartiallyBound = VK_TRUE;
|
||||
indexing.descriptorBindingVariableDescriptorCount = VK_TRUE;
|
||||
indexing.runtimeDescriptorArray = VK_TRUE;
|
||||
indexing.pNext = address.pNext;
|
||||
address.pNext = &indexing;
|
||||
} else {
|
||||
context.descriptorIndexing = context.partiallyBound = context.variableDescriptorCount = context.runtimeDescriptorArray = false;
|
||||
}
|
||||
VkPhysicalDeviceRobustness2FeaturesEXT robustness{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
|
||||
if (hasExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &robustness};
|
||||
@@ -339,6 +362,7 @@ public:
|
||||
std::cout << "Vulkan device: " << properties.deviceName << '\n';
|
||||
context.limits = properties.limits;
|
||||
context.bufferDeviceAddress = true;
|
||||
context.imageTableCapacity = ImageTableCapacity(context);
|
||||
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
|
||||
context.imageFormatProperties = function<PFN_vkGetPhysicalDeviceImageFormatProperties>("vkGetPhysicalDeviceImageFormatProperties");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
@@ -2484,6 +2508,7 @@ public:
|
||||
struct Contents {
|
||||
std::map<std::pair<std::uint32_t, std::uint32_t>, VkDescriptorBufferInfo> buffers;
|
||||
std::map<std::pair<std::uint32_t, std::uint32_t>, VkDescriptorImageInfo> images;
|
||||
std::map<std::pair<VkDescriptorSet, std::uint32_t>, std::uint32_t> written;
|
||||
};
|
||||
explicit DescriptorUpdates(Context& context) : context(context), previous(context.functions),
|
||||
update(context.Resolved(&DeviceFunctions::updateDescriptorSets, "vkUpdateDescriptorSets")) {
|
||||
@@ -2502,6 +2527,7 @@ private:
|
||||
Contents contents;
|
||||
for (std::uint32_t i = 0; i < count; ++i) {
|
||||
const auto& write = writes[i];
|
||||
contents.written[{write.dstSet, write.dstBinding}] += write.descriptorCount;
|
||||
for (std::uint32_t element = 0; element < write.descriptorCount; ++element) {
|
||||
const auto key = std::pair{write.dstBinding, write.dstArrayElement + element};
|
||||
if (write.descriptorType == VK_DESCRIPTOR_TYPE_STORAGE_BUFFER) contents.buffers[key] = write.pBufferInfo[element];
|
||||
@@ -2572,6 +2598,85 @@ void drawDescriptorWriteTests(const Device& device, Recorder& recorder) {
|
||||
recorder.Activate();
|
||||
}
|
||||
|
||||
void drawSnapshotTableSetTests(const Device& device, Recorder& recorder) {
|
||||
namespace Abi = ShaderRecompiler::ImageTableAbi;
|
||||
using ShaderRecompiler::DescriptorKind;
|
||||
using ShaderRecompiler::DescriptorRole;
|
||||
const auto& base = device.GetContext();
|
||||
TextureDetiler detiler(base);
|
||||
auto context = base;
|
||||
context.detiler = &detiler;
|
||||
if (context.imageTableCapacity < 2u) {
|
||||
std::cout << "image tables unavailable: draw snapshot table sets not tested\n";
|
||||
return;
|
||||
}
|
||||
constexpr std::uint32_t width = 4;
|
||||
constexpr std::uint32_t height = 4;
|
||||
alignas(256) static std::array<float, width * height> texels{};
|
||||
texels.fill(1.0f);
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels.data()));
|
||||
const std::array<std::uint32_t, 8> texture{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (22u << 20u) | (((width - 1u) & 3u) << 30u), ((width - 1u) >> 2u) | ((height - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u};
|
||||
constexpr auto bindingCount = static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::Count);
|
||||
const auto addTable = [&](ShaderRecompiler::RecompileResult& program, ShaderRecompiler::RuntimeAbi::Stage stage, std::uint32_t set, std::uint32_t elements) {
|
||||
ShaderRecompiler::DescriptorBinding map{};
|
||||
map.kind = DescriptorKind::StorageBuffer;
|
||||
map.role = DescriptorRole::ImageTableMap;
|
||||
map.descriptorSet = set;
|
||||
map.binding = static_cast<std::uint32_t>(stage) * bindingCount + static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap);
|
||||
map.count = 1;
|
||||
map.guestDescriptor.assign(Abi::HeaderWords + elements, 0u);
|
||||
map.guestDescriptor[Abi::HeaderVersion] = Abi::Version;
|
||||
map.guestDescriptor[Abi::HeaderElementCount] = elements;
|
||||
map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords;
|
||||
map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + elements;
|
||||
program.bindings.push_back(map);
|
||||
ShaderRecompiler::DescriptorBinding images{};
|
||||
images.kind = DescriptorKind::SampledImage;
|
||||
images.role = DescriptorRole::ImageTable;
|
||||
images.descriptorSet = set;
|
||||
images.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding;
|
||||
images.count = elements;
|
||||
for (std::uint32_t element = 0; element < elements; ++element) images.guestDescriptor.insert(images.guestDescriptor.end(), texture.begin(), texture.end());
|
||||
images.imageShapes.assign(elements, ShaderRecompiler::DescriptorImageShape::Image2D);
|
||||
images.imageDepthCompare.assign(elements, false);
|
||||
images.imageSamplers.assign(elements, 0u);
|
||||
program.bindings.push_back(std::move(images));
|
||||
};
|
||||
DescriptorUpdates updates(context);
|
||||
{
|
||||
DescriptorCache cache(context);
|
||||
context.descriptorCache = &cache;
|
||||
Recorder snapshots(context);
|
||||
snapshots.Activate();
|
||||
ShaderRecompiler::RecompileResult vertex, fragment;
|
||||
ShaderRecompiler::DescriptorBinding data{};
|
||||
data.kind = DescriptorKind::StorageBuffer;
|
||||
data.role = DescriptorRole::FlattenedSrt;
|
||||
data.binding = static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::FlattenedSrt);
|
||||
data.count = 1;
|
||||
data.guestDescriptor = {1, 2, 3, 4};
|
||||
vertex.bindings.push_back(data);
|
||||
addTable(vertex, ShaderRecompiler::RuntimeAbi::Stage::Main, 0u, 1u);
|
||||
addTable(fragment, ShaderRecompiler::RuntimeAbi::Stage::Fragment, ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet, 2u);
|
||||
const ColorTarget target{};
|
||||
ShaderResources resources(context, vertex, fragment, target, 0, 0);
|
||||
Require(resources.SplitSets(), "a fragment image table did not get its own descriptor set");
|
||||
const ShaderResources::MovedBuffer moved{0, 0, 16, {5, 6, 7, 8}};
|
||||
const auto draw = resources.PrepareDrawBindings(snapshots, std::span(&moved, 1));
|
||||
Require(draw != nullptr && draw->snapshots.size() == 1, "moved shader data did not create a draw snapshot");
|
||||
const auto& written = updates.calls.back().written;
|
||||
const auto tableWrites = [&](VkDescriptorSet set) {
|
||||
const auto it = written.find({set, ShaderRecompiler::RuntimeAbi::ImageTableBinding});
|
||||
return it == written.end() ? 0u : it->second;
|
||||
};
|
||||
Require(tableWrites(draw->allocation.set) == 1u, "the draw snapshot wrote the wrong number of image table descriptors into the vertex set");
|
||||
Require(tableWrites(draw->fragmentAllocation.set) == 2u, "the draw snapshot wrote the wrong number of image table descriptors into the fragment set");
|
||||
snapshots.Sync();
|
||||
}
|
||||
ClearCachedTextures(context.device);
|
||||
recorder.Activate();
|
||||
}
|
||||
|
||||
void drawSnapshotReuseTests(const Device& device, Recorder& recorder) {
|
||||
const auto& context = device.GetContext();
|
||||
constexpr std::size_t bytes = 65536;
|
||||
@@ -6532,7 +6637,7 @@ void fmaskDecompressMsaaTests(const Device& device, Recorder& recorder) {
|
||||
#ifdef _WIN32
|
||||
void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
#else
|
||||
void* block = std::aligned_alloc(65536, bytes);
|
||||
void* block = std::aligned_alloc(65536, (bytes + 65535u) & ~std::size_t{65535u});
|
||||
#endif
|
||||
Require(block != nullptr, "cannot allocate the FMASK decompress block");
|
||||
std::memset(block, 0, bytes);
|
||||
@@ -6998,6 +7103,70 @@ void singleCubeTests(const Device& device, Recorder& recorder) {
|
||||
}
|
||||
}
|
||||
|
||||
struct SampledViewProbe {
|
||||
Context& context;
|
||||
PFN_vkGetDeviceProcAddr resolver;
|
||||
std::uint32_t views = 0;
|
||||
bool checking = false;
|
||||
static inline SampledViewProbe* active = nullptr;
|
||||
explicit SampledViewProbe(Context& context) : context(context), resolver(context.deviceProc) {
|
||||
Require(active == nullptr, "sampled view probe is already active");
|
||||
active = this;
|
||||
context.deviceProc = resolve;
|
||||
}
|
||||
~SampledViewProbe() {
|
||||
context.deviceProc = resolver;
|
||||
active = nullptr;
|
||||
}
|
||||
static VKAPI_ATTR VkResult VKAPI_CALL createView(VkDevice device, const VkImageViewCreateInfo* info, const VkAllocationCallbacks* callbacks, VkImageView* created) {
|
||||
if (active->checking && info->format == VK_FORMAT_R8G8B8A8_SRGB) {
|
||||
const auto* next = static_cast<const VkBaseInStructure*>(info->pNext);
|
||||
while (next != nullptr && next->sType != VK_STRUCTURE_TYPE_IMAGE_VIEW_USAGE_CREATE_INFO) next = next->pNext;
|
||||
if (next == nullptr || reinterpret_cast<const VkImageViewUsageCreateInfo*>(next)->usage != VK_IMAGE_USAGE_SAMPLED_BIT) return VK_ERROR_VALIDATION_FAILED_EXT;
|
||||
++active->views;
|
||||
}
|
||||
return reinterpret_cast<PFN_vkCreateImageView>(active->resolver(device, "vkCreateImageView"))(device, info, callbacks, created);
|
||||
}
|
||||
static VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL resolve(VkDevice device, const char* name) {
|
||||
if (std::strcmp(name, "vkCreateImageView") == 0) return reinterpret_cast<PFN_vkVoidFunction>(createView);
|
||||
return active->resolver(device, name);
|
||||
}
|
||||
};
|
||||
|
||||
void sampledStorageBaseLevelUsageTests(const Device& device, Recorder& recorder) {
|
||||
auto context = device.GetContext();
|
||||
TextureDetiler detiler(context);
|
||||
context.detiler = &detiler;
|
||||
GuestTextureResource resource{};
|
||||
resource.width = resource.height = 8;
|
||||
resource.depthOrLastArray = 1;
|
||||
resource.mipCount = 2;
|
||||
resource.lastLevel = 1;
|
||||
resource.minLod = context.imageViewMinLod ? 0x100 : 0;
|
||||
resource.tileMode = TextureTileMode::kLinear;
|
||||
resource.dimension = TextureDimension::k2DArray;
|
||||
resource.format = 130;
|
||||
resource.dstSelX = 4;
|
||||
resource.dstSelY = 5;
|
||||
resource.dstSelZ = 6;
|
||||
resource.dstSelW = 7;
|
||||
const auto geometry = DescribeSurface(resource);
|
||||
std::vector<std::byte> memory(static_cast<std::size_t>(geometry.guestBytes) + 256, std::byte{0x80});
|
||||
resource.baseAddress = (reinterpret_cast<std::uintptr_t>(memory.data()) + 255) & ~std::uintptr_t{255};
|
||||
auto storage = StorageTexture::Create(context, detiler, resource, 0);
|
||||
recorder.Keep(storage);
|
||||
recorder.Sync();
|
||||
SampleProgram program(context, recorder);
|
||||
auto sampledContext = context;
|
||||
SampledViewProbe probe(sampledContext);
|
||||
Texture sampled(sampledContext, storage, resource, VkComponentMapping{});
|
||||
probe.checking = true;
|
||||
const auto base = sampled.BaseLevelView();
|
||||
probe.checking = false;
|
||||
expectRed(program.Red(base, sampled.Layout(), 0), 0.21586f, "sampled sRGB base level");
|
||||
Require(sampled.BaseLevelView() == base && probe.views == 1, "the lazy base-level view of a sampled storage image must restrict usage to SAMPLED and be reused");
|
||||
}
|
||||
|
||||
void firstLayerViewTests(const Device& device, Recorder& recorder) {
|
||||
const auto& context = device.GetContext();
|
||||
TextureDetiler detiler(context);
|
||||
@@ -8230,6 +8399,10 @@ int main(int argc, char** argv) {
|
||||
std::cout << "Single cube snapshot and storage sampling tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--sampled-storage-base-level-usage-only") {
|
||||
sampledStorageBaseLevelUsageTests(device, recorder);
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--unchanged-cpu-stamps-only") {
|
||||
unchangedCpuStampTests(device, recorder);
|
||||
return 0;
|
||||
@@ -8352,6 +8525,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--draw-descriptors-only") {
|
||||
drawDescriptorWriteTests(device, recorder);
|
||||
drawSnapshotTableSetTests(device, recorder);
|
||||
std::cout << "Draw descriptor writes and snapshot overrides passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -8409,6 +8583,7 @@ int main(int argc, char** argv) {
|
||||
misalignedRegionTests(device, recorder);
|
||||
drawSnapshotReuseTests(device, recorder);
|
||||
drawDescriptorWriteTests(device, recorder);
|
||||
drawSnapshotTableSetTests(device, recorder);
|
||||
drawSnapshotCapTests(device, recorder);
|
||||
drawSnapshotEvictionTests(device);
|
||||
unwrittenSnapshotTests(device);
|
||||
@@ -8447,6 +8622,7 @@ int main(int argc, char** argv) {
|
||||
imageMemoTests(device, recorder);
|
||||
minLodTests(device, recorder);
|
||||
firstLayerViewTests(device, recorder);
|
||||
sampledStorageBaseLevelUsageTests(device, recorder);
|
||||
singleCubeTests(device, recorder);
|
||||
atomicViewTests(device, recorder);
|
||||
metadataPassTests(device, recorder);
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
@@ -29,6 +31,7 @@
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
@@ -646,6 +649,7 @@ void verifyKeySensitivity() {
|
||||
changes("f64 denormal preservation", [](SampleRequest& sample) { sample.request.target.float64DenormPreserve = true; });
|
||||
changes("f16 denormal preservation", [](SampleRequest& sample) { sample.request.target.float16DenormPreserve = true; });
|
||||
changes("spanned waves", [](SampleRequest& sample) { sample.request.target.spannedWaves = true; });
|
||||
changes("robust buffer access 2", [](SampleRequest& sample) { sample.request.target.robustBufferAccess2 = true; });
|
||||
changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; });
|
||||
changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; });
|
||||
changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; });
|
||||
@@ -730,6 +734,64 @@ void verifyFloatModeSerialization() {
|
||||
require(back.request.context.floatMode == mode, "the float mode did not survive serialization");
|
||||
}
|
||||
|
||||
int runLargeFileWrite() {
|
||||
constexpr std::size_t size = (std::size_t{2} << 30u) + 4096u;
|
||||
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-large-file-test-" + std::to_string(std::random_device{}()));
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(directory, error);
|
||||
std::filesystem::create_directories(directory, error);
|
||||
const auto space = std::filesystem::space(directory, error);
|
||||
if (error || space.available < size + (std::uintmax_t{1} << 30u)) {
|
||||
std::cout << "skipping: not enough free disk space for a file of more than 2 GiB\n";
|
||||
return 77;
|
||||
}
|
||||
std::vector<std::byte> bytes;
|
||||
try {
|
||||
bytes.resize(size);
|
||||
} catch (const std::bad_alloc&) {
|
||||
std::cout << "skipping: cannot allocate a buffer of more than 2 GiB\n";
|
||||
return 77;
|
||||
}
|
||||
const std::array<std::size_t, 6> samples{0u, (std::size_t{1} << 30u) - 1u, std::size_t{1} << 30u, (std::size_t{2} << 30u) - 1u, std::size_t{2} << 30u, size - 1u};
|
||||
for (std::size_t index = 0; index < samples.size(); ++index) bytes[samples[index]] = static_cast<std::byte>(0xa0u + index);
|
||||
const auto path = directory / "pipelines.bin";
|
||||
auto written = std::make_shared<std::promise<bool>>();
|
||||
auto result = written->get_future();
|
||||
std::thread([written, &bytes, path] {
|
||||
try {
|
||||
written->set_value(WriteFileAtomically(path, bytes));
|
||||
} catch (...) {
|
||||
written->set_exception(std::current_exception());
|
||||
}
|
||||
}).detach();
|
||||
if (result.wait_for(std::chrono::seconds(200)) != std::future_status::ready) {
|
||||
std::cerr << "WriteFileAtomically did not return within 200 s for a file of more than 2 GiB\n";
|
||||
std::cerr.flush();
|
||||
std::_Exit(1);
|
||||
}
|
||||
require(result.get(), "cannot write a cache file of more than 2 GiB");
|
||||
require(std::filesystem::file_size(path) == size, "the cache file of more than 2 GiB has the wrong size");
|
||||
{
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
require(static_cast<bool>(file), "cannot open the cache file of more than 2 GiB");
|
||||
for (std::size_t index = 0; index < samples.size(); ++index) {
|
||||
file.seekg(static_cast<std::streamoff>(samples[index]));
|
||||
char value = 0;
|
||||
require(file.get(value) && static_cast<std::byte>(value) == bytes[samples[index]], "the cache file of more than 2 GiB differs at byte " + std::to_string(samples[index]));
|
||||
}
|
||||
}
|
||||
std::size_t entries = 0;
|
||||
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
|
||||
static_cast<void>(entry);
|
||||
++entries;
|
||||
}
|
||||
require(entries == 1u, "the cache file of more than 2 GiB left a temporary file behind");
|
||||
std::filesystem::remove_all(directory, error);
|
||||
require(!error, "cannot remove the large file test directory: " + error.message());
|
||||
std::cout << "large cache file write test passed\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
void verifyAtomicReplacement(const std::filesystem::path& directory) {
|
||||
struct Reader {
|
||||
#ifdef _WIN32
|
||||
@@ -1135,7 +1197,8 @@ void verifyBindingPlanSelection() {
|
||||
throw std::runtime_error("binding plan accepted an unsupported buffer alignment");
|
||||
};
|
||||
snapshot.buffers[0].dwords[0] |= 2u;
|
||||
rejectsAlignment("buffer atomic on a V# whose base is not DWORD aligned");
|
||||
const auto atomicPlan = builder.Prepare(compiled.layout, info, IrShaderStage::Compute, snapshot);
|
||||
require(std::ranges::any_of(atomicPlan.specialization, [](const PipelineSpecializationConstant& constant) { return constant.id == PipelineSpecialization::BufferBase + 3u && constant.value == 2u; }), "a buffer atomic V# off a DWORD boundary did not record its misalignment");
|
||||
snapshot.buffers[0].dwords[0] &= ~3u;
|
||||
info.buffers[1].typedAlignment = 2u;
|
||||
snapshot.buffers[1].dwords[0] |= 2u;
|
||||
@@ -1443,6 +1506,10 @@ std::vector<PlanFixture> planFixtures() {
|
||||
add("bindless material table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
add("bindless whole table", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
add("bindless sample in a loop latch", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xbea80380u, 0xf09c0f08u, 0x00450000u, 0x80288128u, 0xbf0a8428u, 0xbf85fffbu, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, tables}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
auto keyed = tables;
|
||||
keyed.resize(20u, 0u);
|
||||
keyed[16] = 3u;
|
||||
add("bindless selected record", 64, {0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u, 0xf4000400u, 0xfa000040u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u}, srtPointer, {{PlanSrtAddress, keyed}, {PlanTableAddress, heap}, {PlanMaterialAddress, materials}});
|
||||
|
||||
std::vector<std::uint32_t> phis(32, 0u);
|
||||
place(phis, 0, imageWords(PlanTextureAddress));
|
||||
@@ -1588,6 +1655,8 @@ IrResourcePlan handBuiltPlan() {
|
||||
table.dwordCount = 8u;
|
||||
table.dwords[0] = &readConst(3);
|
||||
table.tableColumn = TableColumn{2u, 32u, 16u, 4u, 8u, 0xffu, false, true, {KeyDomain{3u, 4u, 16u, false}, KeyDomain{2u, 8u, 4u, true}}, false};
|
||||
table.tableColumn->selected = true;
|
||||
table.tableColumn->selection = 2u;
|
||||
DescriptorSource heap;
|
||||
heap.dwordCount = 4u;
|
||||
heap.dwords[1] = &readConst(6);
|
||||
@@ -1836,6 +1905,7 @@ void verifyMalformedPlans() {
|
||||
rejects("a uniform fill of five words", [](IrResourcePlan& plan) { plan.uniformFill.fill.words = 5u; });
|
||||
rejects("an image table heap past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->heapSource = 4u; });
|
||||
rejects("an image table key past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->keyDomains[1].source = 4u; });
|
||||
rejects("an image table selection past the sources", [](IrResourcePlan& plan) { plan.descriptorSources[1].tableColumn->selection = 4u; });
|
||||
auto foreign = handBuiltPlan();
|
||||
IrValue stray(IrOpcode::Void, IrType::U32, 99u);
|
||||
foreign.srtReads[0].value = &stray;
|
||||
@@ -1895,6 +1965,7 @@ void verifyPlanRoundTrip() {
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->keyDomains.empty(); }); }), "no fixture plans a whole image table");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && !source.tableColumn->keyDomains.empty(); }); }), "no fixture plans an image table with a key domain");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->address; }); }), "no fixture plans an image table through a pointer");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.descriptorSources.begin(), plan.descriptorSources.end(), [](const DescriptorSource& source) { return source.tableColumn.has_value() && source.tableColumn->selected; }); }), "no fixture plans a selected image table record");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.controlFlow.begin(), plan.controlFlow.end(), [](const ResourceBlock& block) { return block.condition != nullptr; }); }), "no fixture plans a control-flow condition");
|
||||
require(covers([](const IrResourcePlan& plan) { return std::any_of(plan.valueStorage.begin(), plan.valueStorage.end(), [](const auto& value) { return value.IsPhi(); }); }), "no fixture plans a phi");
|
||||
require(covers([](const IrResourcePlan& plan) { return !plan.guardedSrtSlots.empty(); }), "no fixture plans a guarded SRT read");
|
||||
@@ -2124,6 +2195,7 @@ int main(int argc, char** argv) {
|
||||
std::cout << "artifact codec tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite();
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess();
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo();
|
||||
|
||||
@@ -561,9 +561,25 @@ void verifyMayEvaluate() {
|
||||
counter.AddArgument(&next);
|
||||
auto& latch = make(IrOpcode::ULessThan32, IrType::Bool, {&next, &constant(4)});
|
||||
auto& sum = make(IrOpcode::CompositeExtractU32x2, IrType::U32, {&make(IrOpcode::IAddCarry32, IrType::U32x2, {&input, &constant(1)}), &constant(0)});
|
||||
auto& firstInput = make(IrOpcode::ReadFirstLane, IrType::U32, {&input, &varying});
|
||||
auto& firstLane = make(IrOpcode::ReadFirstLane, IrType::U32, {&lane, &varying});
|
||||
auto& firstActive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &input, &lane}), &varying});
|
||||
auto& firstInactive = make(IrOpcode::ReadFirstLane, IrType::U32, {&make(IrOpcode::SelectU32, IrType::U32, {&varying, &lane, &input}), &varying});
|
||||
plan.srtReads = {{&input, 0u}, {&lane, 4u}, {nullptr, 8u}};
|
||||
auto& constInput = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(0)});
|
||||
auto& constLane = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(1)});
|
||||
auto& constOutside = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(4)});
|
||||
auto& constItself = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(2)});
|
||||
plan.srtReads[2].value = &constItself;
|
||||
auto& carried = make(IrOpcode::Phi, IrType::U32, {&input});
|
||||
auto& firstCarried = make(IrOpcode::ReadFirstLane, IrType::U32, {&carried, &varying});
|
||||
carried.AddArgument(&make(IrOpcode::IAdd32, IrType::U32, {&firstCarried, &constant(1)}));
|
||||
plan.srtReads.push_back({nullptr, 12u});
|
||||
auto& constFirstItself = make(IrOpcode::ReadConst, IrType::U32, {&constant(0), &constant(3)});
|
||||
plan.srtReads[3].value = &make(IrOpcode::ReadFirstLane, IrType::U32, {&constFirstItself, &varying});
|
||||
std::array<std::uint32_t, 1> data{4};
|
||||
SrtRuntime runtime{data};
|
||||
for (const auto& [value, expected, name] : std::array<std::tuple<IrValue*, bool, const char*>, 11>{{
|
||||
for (const auto& [value, expected, name] : std::array<std::tuple<IrValue*, bool, const char*>, 21>{{
|
||||
{&constant(3), true, "a constant"},
|
||||
{&input, true, "user data"},
|
||||
{&uniform, true, "a comparison of user data"},
|
||||
@@ -575,13 +591,23 @@ void verifyMayEvaluate() {
|
||||
{&divergent, false, "a phi whose operands differ"},
|
||||
{&latch, false, "a scalar loop counter's latch condition"},
|
||||
{&sum, true, "the low word of a carry add of user data"},
|
||||
{&firstInput, true, "a first-lane read of user data"},
|
||||
{&firstLane, false, "a first-lane read of a vector register"},
|
||||
{&firstActive, true, "a first-lane read of a select whose active arm is user data"},
|
||||
{&firstInactive, false, "a first-lane read of a select whose active arm is a vector register"},
|
||||
{&constInput, true, "a constant read whose slot holds user data"},
|
||||
{&constLane, false, "a constant read whose slot holds a vector register"},
|
||||
{&constOutside, false, "a constant read past the last slot"},
|
||||
{&constItself, false, "a constant read whose slot reads itself"},
|
||||
{&firstCarried, false, "a first-lane read of a loop-carried value that reaches itself through the read"},
|
||||
{&constFirstItself, false, "a constant read whose slot reads itself through a first-lane read"},
|
||||
}}) {
|
||||
require(Detail::Evaluator::MayEvaluate(plan, value) == expected, (std::string("MayEvaluate misjudged ") + name).c_str());
|
||||
Detail::Evaluator evaluator(plan, runtime);
|
||||
std::uint32_t result = 0;
|
||||
require(evaluator.Evaluate(Detail::IrNode(value), result) == expected, (std::string("MayEvaluate disagreed with the evaluator on ") + name).c_str());
|
||||
}
|
||||
const std::array<IrOpcode, 9> takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::ReadFirstLane, IrOpcode::ReadConst, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer};
|
||||
const std::array<IrOpcode, 7> takenAsEvaluable{IrOpcode::Identity, IrOpcode::Phi, IrOpcode::Void, IrOpcode::GetUserData, IrOpcode::GetShaderBase, IrOpcode::LoadAddressU32, IrOpcode::ReadConstBuffer};
|
||||
for (std::uint16_t raw = 0; raw < static_cast<std::uint16_t>(IrOpcode::Count); ++raw) {
|
||||
const auto opcode = static_cast<IrOpcode>(raw);
|
||||
if (std::ranges::find(takenAsEvaluable, opcode) != takenAsEvaluable.end()) continue;
|
||||
@@ -1030,6 +1056,7 @@ struct TableGuest {
|
||||
std::array<std::uint32_t, 20> srt{};
|
||||
};
|
||||
|
||||
constexpr std::uint32_t TableFormat16UNorm = 7;
|
||||
constexpr std::uint32_t TableFormat8888UNorm = 56;
|
||||
constexpr std::uint32_t TableFormat32UInt = 20;
|
||||
constexpr std::uint32_t TableFormat16SInt = 12;
|
||||
@@ -1109,6 +1136,13 @@ TableCompiled CompileTable(ShaderRecompiler::RecompileRequest request) {
|
||||
return out;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TableSelected(const ShaderRecompiler::ImageTableSnapshot& tables, std::uint32_t table, const char* message) {
|
||||
namespace Abi = ShaderRecompiler::ImageTableAbi;
|
||||
const auto& column = tables.tables.at(table);
|
||||
require(column.records == 1u && column.keys == 1u && column.codes.size() == 1u && (column.codes[0] & Abi::PoisonFlag) == 0u, message);
|
||||
return tables.words.at(column.codes[0]).dwords;
|
||||
}
|
||||
|
||||
void verifyImageTableKeys() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
@@ -1240,6 +1274,43 @@ void verifyImageTableBulkReads() {
|
||||
require(cutByRun->snapshot.tables == cutByWord->snapshot.tables && cutWordReads.wordReads == 12u && cutRunReads.wordReads == 0u && cutRunReads.bulkWords == 12u, "image table bulk reads: a record cut by an unmapped page read other words in runs than word by word");
|
||||
}
|
||||
|
||||
void verifyImageTableBulkKeyScan() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
std::fill(guest.heap.begin(), guest.heap.end(), std::array<std::uint32_t, 8>{});
|
||||
guest.heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
guest.heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
guest.heap[3] = guest.heap[0];
|
||||
guest.materials = {{{0u, 1u, 0u, 0u}, {0u, 0x08000003u, 0u, 0u}, {0u, 1u, 0u, 0u}}};
|
||||
FillTableSrt(guest, guest.heap.data(), 32u, 4u);
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(TableMaterialCode, userData);
|
||||
const auto capture = [&](CountingMemory& memory, bool bulk) {
|
||||
SrtRuntime runtime;
|
||||
runtime.userData = request.context.userData;
|
||||
runtime.shaderBase = request.shader.codeAddress;
|
||||
runtime.userContext = &memory;
|
||||
runtime.readMemory = &CountingRead;
|
||||
runtime.readSpecializationMemory = &CountingRead;
|
||||
runtime.accessible = &CountingAccessible;
|
||||
if (bulk) runtime.readWords = &CountingReadWords;
|
||||
return CaptureResources(request, runtime);
|
||||
};
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(guest.materials.data());
|
||||
CountingMemory byWordReads{begin, begin + sizeof(guest.materials)};
|
||||
CountingMemory byRunReads = byWordReads;
|
||||
const auto byWord = capture(byWordReads, false);
|
||||
const auto byRun = capture(byRunReads, true);
|
||||
require(byWordReads.wordReads == guest.materials.size(), "image table bulk key scan: without a bulk reader the keys were not read one word each");
|
||||
require(byRunReads.wordReads == 0u, "image table bulk key scan: the keys were read word by word although a bulk reader exists");
|
||||
require(byRun->snapshot.tables == byWord->snapshot.tables, "image table bulk key scan: scanning the keys in bulk changed the snapshot");
|
||||
const auto& snapshot = byRun->snapshot.tables.tables.at(0);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(snapshot.keys == 4u && snapshot.codes[0] == snapshot.codes[3] && snapshot.codes[0] != snapshot.codes[1] && snapshot.codes[2] == outside, "image table bulk key scan: the narrowed codes are wrong");
|
||||
}
|
||||
|
||||
void verifyImageTables() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
@@ -1421,7 +1492,6 @@ void verifyImageTables() {
|
||||
|
||||
void verifyImageTableKeyUnion() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[3];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
@@ -1436,24 +1506,25 @@ void verifyImageTableKeyUnion() {
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(TableTwoKeyCode, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table key union: two keyed reads of one table were not planned as one image of one column");
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.has_value() && column->keyDomains.size() == 2u && column->keyDomains[0].value && column->keyDomains[1].value && column->keyDomains[0].source != column->keyDomains[1].source, "image table key union: the column does not carry both value key domains");
|
||||
require(plan->info.images.size() == 2u && plan->info.images[0].table == 0u && plan->info.images[1].table == 1u, "image table key union: two keyed reads of one heap were not planned as two tables");
|
||||
const auto& first = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
const auto& second = plan->descriptorSources.at(plan->info.images[1].source).tableColumn;
|
||||
require(first.has_value() && second.has_value() && first->selected && second->selected && first->heapSource == second->heapSource && first->selection != second->selection, "image table key union: the two keys do not each select a record of the one heap");
|
||||
const auto held = [](const ImageTableSnapshot& tables, std::uint32_t table) { return TableSelected(tables, table, "image table key union: a table does not hold one readable selected record"); };
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(compiled.capture->snapshot.tables.tables.size() == 1u && snapshot.keys == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[1] & Abi::PoisonFlag) == 0u && snapshot.codes[2] == outside && (snapshot.codes[3] & Abi::PoisonFlag) == 0u, "image table key union: the snapshot does not hold the union of the two keys' records");
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
require(tables.tables.size() == 2u && held(tables, 0u) == heap[1] && held(tables, 1u) == heap[3], "image table key union: the tables do not hold the two keys' records");
|
||||
require(TableCovered(compiled.regions, &heap[1], sizeof(heap[1])) && TableCovered(compiled.regions, &heap[3], sizeof(heap[3])) && !TableCovered(compiled.regions, &heap[0], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[2], sizeof(std::uint32_t)), "image table key union: records no key selects were captured");
|
||||
const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable);
|
||||
require(images != nullptr && images->count == 3u, "image table key union: the binding does not hold the three distinct images of the selected records");
|
||||
require(images != nullptr && images->count == 2u, "image table key union: the binding does not hold the two distinct images of the selected records");
|
||||
guest.srt[17] = 2u;
|
||||
const auto moved = CompileTable(request);
|
||||
const auto& movedSnapshot = moved.capture->snapshot.tables.tables.at(0);
|
||||
require((movedSnapshot.codes[2] & Abi::PoisonFlag) == 0u && movedSnapshot.codes[3] == outside && (movedSnapshot.codes[1] & Abi::PoisonFlag) == 0u && moved.result->spirv == compiled.result->spirv, "image table key union: a changed second key did not move the union or changed the module");
|
||||
const auto& movedTables = moved.capture->snapshot.tables;
|
||||
require(held(movedTables, 0u) == heap[1] && held(movedTables, 1u) == heap[2] && TableCovered(moved.regions, &heap[2], sizeof(heap[2])) && !TableCovered(moved.regions, &heap[3], sizeof(std::uint32_t)) && moved.result->spirv == compiled.result->spirv, "image table key union: a changed second key did not move its record alone or changed the module");
|
||||
}
|
||||
|
||||
void verifyImageTableLateKey() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[3];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
@@ -1468,15 +1539,16 @@ void verifyImageTableLateKey() {
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(TableLateKeyCode, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table late key: two keyed reads of one table were not planned as one image of one column");
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.has_value() && column->keyDomains.size() == 2u && column->keyDomains[0].value && column->keyDomains[1].value && column->keyDomains[0].source != column->keyDomains[1].source, "image table late key: the column does not carry both value key domains");
|
||||
require(plan->info.images.size() == 2u && plan->info.images[0].table == 0u && plan->info.images[1].table == 1u, "image table late key: two keyed reads of one heap were not planned as two tables");
|
||||
const auto& first = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
const auto& second = plan->descriptorSources.at(plan->info.images[1].source).tableColumn;
|
||||
require(first.has_value() && second.has_value() && first->selected && second->selected && first->selection != second->selection, "image table late key: a key read after the first image operation does not select its own record");
|
||||
const auto held = [](const ImageTableSnapshot& tables, std::uint32_t table) { return TableSelected(tables, table, "image table late key: a table does not hold one readable selected record"); };
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(compiled.capture->snapshot.tables.tables.size() == 1u && snapshot.keys == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && (snapshot.codes[1] & Abi::PoisonFlag) == 0u && snapshot.codes[2] == outside && (snapshot.codes[3] & Abi::PoisonFlag) == 0u, "image table late key: the snapshot does not hold the union of the two keys' records");
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
require(tables.tables.size() == 2u && held(tables, 0u) == heap[1] && held(tables, 1u) == heap[3], "image table late key: the tables do not hold the two keys' records");
|
||||
const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable);
|
||||
require(images != nullptr && images->count == 3u, "image table late key: the binding does not hold the three distinct images of the selected records");
|
||||
require(images != nullptr && images->count == 2u, "image table late key: the binding does not hold the two distinct images of the selected records");
|
||||
}
|
||||
|
||||
void verifyImageTableUnkeyedAccess() {
|
||||
@@ -1499,13 +1571,19 @@ void verifyImageTableUnkeyedAccess() {
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(*code, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(!plan->info.images.empty() && std::all_of(plan->info.images.begin(), plan->info.images.end(), [](const ImageResource& image) { return image.table == 0u; }), "image table unkeyed access: the two reads were not planned as one table");
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.has_value() && column->unkeyed && column->keyDomains.empty(), "image table unkeyed access: a column shared with a read that has no key domain kept a key domain");
|
||||
const auto& planned = plan->info.images;
|
||||
require(planned.size() == 2u && std::all_of(planned.begin(), planned.end(), [](const ImageResource& image) { return image.table != NoTable; }), "image table unkeyed access: the two reads were not planned as two tables");
|
||||
const auto columnOf = [&](const ImageResource& image) { return *plan->descriptorSources.at(image.source).tableColumn; };
|
||||
const auto selected = std::find_if(planned.begin(), planned.end(), [&](const ImageResource& image) { return columnOf(image).selected; });
|
||||
const auto whole = std::find_if(planned.begin(), planned.end(), [&](const ImageResource& image) { return !columnOf(image).selected; });
|
||||
require(selected != planned.end() && whole != planned.end(), "image table unkeyed access: the read keyed through the SRT does not select its record beside the read keyed per lane");
|
||||
require(columnOf(*whole).unkeyed && columnOf(*whole).keyDomains.empty(), "image table unkeyed access: the read keyed per lane kept a key domain");
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
require(TableSelected(tables, selected->table, "image table unkeyed access: a table does not hold one readable selected record") == heap[2], "image table unkeyed access: the read keyed through the SRT does not hold its record");
|
||||
const auto& snapshot = tables.tables.at(whole->table);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(snapshot.keys == 4u && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t code) { return code == outside; }), "image table unkeyed access: the shared column was narrowed by the keyed read");
|
||||
require(snapshot.keys == 4u && std::none_of(snapshot.codes.begin(), snapshot.codes.end(), [&](std::uint32_t entry) { return entry == outside; }), "image table unkeyed access: the table keyed per lane was narrowed");
|
||||
const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable);
|
||||
require(images != nullptr && images->count == 4u, "image table unkeyed access: the binding does not hold the whole domain");
|
||||
}
|
||||
@@ -1529,18 +1607,19 @@ void verifyImageTableSrtKey() {
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "image table SRT key: the T# was not planned as a table");
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.has_value() && !column->keyDomains.empty(), "image table SRT key: a key read through the SRT has no key domain");
|
||||
require(column.has_value() && column->selected && column->selection < plan->descriptorSources.size(), "image table SRT key: a T# whose key is read through the SRT does not select its record");
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto outside = Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OutsideDomain));
|
||||
require(snapshot.records == 4u && snapshot.keys == 4u && snapshot.codes.size() == 4u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && snapshot.codes[1] == outside && (snapshot.codes[2] & Abi::PoisonFlag) == 0u && snapshot.codes[3] == outside, "image table SRT key: the snapshot was not narrowed to the key read through the SRT");
|
||||
const auto& tables = compiled.capture->snapshot.tables;
|
||||
const auto& snapshot = tables.tables.at(0);
|
||||
require(snapshot.records == 1u && snapshot.keys == 1u && snapshot.codes.size() == 1u && (snapshot.codes[0] & Abi::PoisonFlag) == 0u && tables.words.at(snapshot.codes[0]).dwords == heap[2], "image table SRT key: the snapshot does not hold the selected record alone");
|
||||
const auto* images = TableBinding(*compiled.result, DescriptorRole::ImageTable);
|
||||
require(images != nullptr && images->count == 2u, "image table SRT key: the binding holds other entries than the selected one and the first");
|
||||
require(TableCovered(compiled.regions, &guest.srt[16], sizeof(std::uint32_t)), "image table SRT key: the key was not captured");
|
||||
require(images != nullptr && images->count == 1u, "image table SRT key: the binding holds other entries than the selected one");
|
||||
require(TableCovered(compiled.regions, &guest.srt[16], sizeof(std::uint32_t)) && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), "image table SRT key: the key or the selected record was not captured");
|
||||
require(!TableCovered(compiled.regions, &heap[0], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[1], sizeof(std::uint32_t)) && !TableCovered(compiled.regions, &heap[3], sizeof(std::uint32_t)), "image table SRT key: records the key does not select were captured");
|
||||
guest.srt[16] = 3u;
|
||||
const auto moved = CompileTable(request);
|
||||
const auto& movedSnapshot = moved.capture->snapshot.tables.tables.at(0);
|
||||
require((movedSnapshot.codes[3] & Abi::PoisonFlag) == 0u && movedSnapshot.codes[2] == outside && movedSnapshot.codes[1] == outside && moved.result->spirv == compiled.result->spirv && moved.key == compiled.key, "image table SRT key: a changed key did not move the snapshot alone");
|
||||
const auto& movedTables = moved.capture->snapshot.tables;
|
||||
require(movedTables.words.at(movedTables.tables.at(0).codes.at(0)).dwords == heap[3] && TableCovered(moved.regions, &heap[3], sizeof(heap[3])) && !TableCovered(moved.regions, &heap[2], sizeof(std::uint32_t)) && moved.result->spirv == compiled.result->spirv && moved.key == compiled.key, "image table SRT key: a changed key did not move the selected record alone");
|
||||
}
|
||||
|
||||
void verifyImageTableMaskedOffset() {
|
||||
@@ -1587,11 +1666,24 @@ void verifyImageTableRegistration() {
|
||||
const std::vector<std::uint32_t> storeCode{0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0200f08u, 0x00050200u, 0xbf810000u};
|
||||
const auto store = CompileTable(TableRequest(storeCode, userData));
|
||||
const auto& storePlan = *store.capture->plan;
|
||||
require(storePlan.info.images.size() == 1u && storePlan.info.images[0].table == 0u && storePlan.info.images[0].tableOperation == TableOperation::Unsupported, "image table registration: a table T# feeding image_store was not an unsupported table operation");
|
||||
require(storePlan.info.images.size() == 1u && storePlan.info.images[0].table == 0u && storePlan.info.images[0].tableOperation == TableOperation::Supported, "image table registration: a table T# feeding image_store was not a supported table operation");
|
||||
const auto& storeMap = TableMap(*store.result);
|
||||
const auto storeFault = storeMap[Abi::TableHeader(0) + Abi::TableFault];
|
||||
require((storeFault & Abi::PoisonFlag) != 0u && store.result->imageTablePoison.at(storeFault & ~Abi::PoisonFlag).reason == static_cast<std::uint32_t>(Abi::PoisonReason::UnsupportedOperation) && store.result->imageTableFaults != 0u, "image table registration: the store's table does not fault on use");
|
||||
require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: an unsupported table bound elements");
|
||||
const auto* storeTable = TableBinding(*store.result, DescriptorRole::ImageStorageTable);
|
||||
require((storeFault & Abi::PoisonFlag) == 0u && storeTable != nullptr && storeTable->count != 0u && std::all_of(storeTable->imageWritten.begin(), storeTable->imageWritten.end(), [](bool written) { return written; }), "image table registration: the store's table was not bound as written storage elements");
|
||||
require(TableBinding(*store.result, DescriptorRole::ImageTable) == nullptr || TableBinding(*store.result, DescriptorRole::ImageTable)->count == 0u, "image table registration: a store-only table bound sampled elements");
|
||||
const std::array<std::pair<std::uint32_t, const char*>, 3> unsupportedStores{{{0xf0240f08u, "image_store_mip"}, {0xf0280f08u, "image_store_pck"}, {0xf0440108u, "image_atomic_add"}}};
|
||||
for (const auto& [instruction, name] : unsupportedStores) {
|
||||
auto code = storeCode;
|
||||
code[8] = instruction;
|
||||
const auto unsupported = CompileTable(TableRequest(code, userData));
|
||||
const auto& plan = *unsupported.capture->plan;
|
||||
require(plan.info.images.size() == 1u && plan.info.images[0].table == 0u && (plan.info.images[0].tableOperation == TableOperation::Unsupported || plan.info.images[0].mipMode == ImageMipMode::DynamicStorage), (std::string("image table registration: a table T# feeding ") + name + " was not planned as an unsupported or dynamic-mip table store").c_str());
|
||||
const auto fault = TableMap(*unsupported.result)[Abi::TableHeader(0) + Abi::TableFault];
|
||||
require((fault & Abi::PoisonFlag) != 0u && unsupported.result->imageTablePoison.at(fault & ~Abi::PoisonFlag).reason == static_cast<std::uint32_t>(Abi::PoisonReason::UnsupportedOperation) && unsupported.result->imageTableFaults != 0u, (std::string("image table registration: the ") + name + " table does not fault on use").c_str());
|
||||
const auto* storage = TableBinding(*unsupported.result, DescriptorRole::ImageStorageTable);
|
||||
require(storage == nullptr || storage->count == 0u, (std::string("image table registration: the unsupported ") + name + " bound storage table elements").c_str());
|
||||
}
|
||||
|
||||
const std::vector<std::uint32_t> compareCode{0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf0fc0108u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u};
|
||||
auto compareRequest = TableRequest(compareCode, userData);
|
||||
@@ -1828,6 +1920,281 @@ void verifyPointerTableOffsetCarry() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void verifySelectedRecordFallbacks() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
std::fill(heap.begin(), heap.end(), std::array<std::uint32_t, 8>{});
|
||||
heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[2] = heap[1];
|
||||
heap[3] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
guest.materials = {};
|
||||
guest.srt[16] = 2u;
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto request = TableRequest(TableSrtKeyCode, userData);
|
||||
const auto valid = CompileTable(request);
|
||||
|
||||
heap[2][3] &= 0x0fffffffu;
|
||||
const auto invalid = CompileTable(request);
|
||||
const auto& invalidMap = TableMap(*invalid.result);
|
||||
const auto invalidCodes = TableCodes(invalidMap, 0);
|
||||
require(invalidCodes.size() == 1u && invalidCodes[0] != Abi::NullCode && (invalidCodes[0] & Abi::PoisonFlag) == 0u, "selected record fallbacks: an invalid selected T# has no entry");
|
||||
const auto invalidFlags = invalidMap.at(invalidCodes[0] + Abi::ImageRecordFlags);
|
||||
require((invalidFlags & Abi::PoisonFlag) != 0u, "selected record fallbacks: an invalid selected T# was bound");
|
||||
const auto& poison = invalid.result->imageTablePoison.at(invalidFlags & ~Abi::PoisonFlag);
|
||||
require(poison.reason == static_cast<std::uint32_t>(Abi::PoisonReason::NotImage) && poison.dwordCount == 8u && poison.words[3] == heap[2][3] && invalid.result->imageTableFaults != 0u, "selected record fallbacks: an invalid selected T# was not poisoned with its words");
|
||||
require(invalid.result->variantId == valid.result->variantId, "selected record fallbacks: an invalid selected T# changed the artifact");
|
||||
heap[2] = heap[1];
|
||||
|
||||
guest.srt[16] = 200u;
|
||||
const auto beyond = CompileTable(request);
|
||||
const auto beyondCodes = TableCodes(TableMap(*beyond.result), 0);
|
||||
require(beyondCodes.size() == 1u && beyondCodes[0] == Abi::NullCode && beyond.result->imageTableFaults == 0u && !TableTouched(beyond.regions, heap.data(), sizeof(heap)), "selected record fallbacks: a key past the heap did not select a null image without reading the heap");
|
||||
guest.srt[16] = 2u;
|
||||
|
||||
void* reserved = nullptr;
|
||||
#ifdef _WIN32
|
||||
reserved = VirtualAlloc(nullptr, 4096, MEM_RESERVE, PAGE_NOACCESS);
|
||||
#else
|
||||
reserved = mmap(nullptr, 4096, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (reserved == MAP_FAILED) reserved = nullptr;
|
||||
#endif
|
||||
require(reserved != nullptr, "selected record fallbacks: cannot reserve an inaccessible page");
|
||||
FillTableSrt(guest, reserved, 32u, 4u);
|
||||
const auto unmapped = CompileTable(request);
|
||||
const auto& unmappedTable = unmapped.capture->snapshot.tables.tables.at(0);
|
||||
require(unmappedTable.keys == 1u && unmappedTable.codes.at(0) == Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::Unmapped)) && !TableTouched(unmapped.regions, reserved, 4096u), "selected record fallbacks: an unmapped selected record is not Unmapped poison or was read");
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
|
||||
std::vector<std::uint32_t> loadedKey(TableSrtKeyCode);
|
||||
const auto shift = std::find(loadedKey.begin(), loadedKey.end(), 0x8f108510u);
|
||||
require(shift != loadedKey.end(), "selected record fallbacks: the program has no s_lshl_b32");
|
||||
loadedKey.insert(shift, {0xf4200406u, 0x20000000u});
|
||||
guest.materials[0][1] = 3u;
|
||||
guest.srt[16] = 4u;
|
||||
const auto loadedRequest = TableRequest(loadedKey, userData);
|
||||
const auto loadedPlan = GetResourcePlan(loadedRequest);
|
||||
require(loadedPlan->info.images.size() == 1u && loadedPlan->descriptorSources.at(loadedPlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a key loaded from a constant buffer at an SRT offset does not select its record");
|
||||
const auto loaded = CompileTable(loadedRequest);
|
||||
require(TableSelected(loaded.capture->snapshot.tables, 0u, "selected record fallbacks: a loaded key does not hold one readable selected record") == heap[3] && TableCovered(loaded.regions, &guest.materials[0][1], sizeof(std::uint32_t)), "selected record fallbacks: the record of a loaded key was not selected or its key was not captured");
|
||||
const auto materials = TableV(guest.materials.data(), 16u, 3u);
|
||||
const auto unreadable = TableV(reserved, 16u, 3u);
|
||||
std::copy(unreadable.begin(), unreadable.end(), guest.srt.begin() + 8);
|
||||
const auto lost = CompileTable(loadedRequest);
|
||||
const auto& lostTable = lost.capture->snapshot.tables.tables.at(0);
|
||||
require(lostTable.keys == 0u && lostTable.fault == Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::Unmapped)) && lost.result->imageTableFaults != 0u && lost.result->variantId == loaded.result->variantId, "selected record fallbacks: a key that cannot be read did not fault its table");
|
||||
std::copy(materials.begin(), materials.end(), guest.srt.begin() + 8);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(reserved, 0, MEM_RELEASE);
|
||||
#else
|
||||
munmap(reserved, 4096);
|
||||
#endif
|
||||
|
||||
auto writtenKey = loadedKey;
|
||||
const auto store = std::find(writtenKey.begin(), writtenKey.end(), 0x80070000u);
|
||||
require(store != writtenKey.end(), "selected record fallbacks: the program has no buffer_store_dword");
|
||||
*store = 0x80030000u;
|
||||
const auto writtenRequest = TableRequest(writtenKey, userData);
|
||||
const auto writtenPlan = GetResourcePlan(writtenRequest);
|
||||
const auto& writtenColumn = writtenPlan->descriptorSources.at(writtenPlan->info.images.at(0).source).tableColumn;
|
||||
require(writtenColumn.has_value() && !writtenColumn->selected && writtenColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a buffer the shader writes selected its record");
|
||||
const auto written = CompileTable(writtenRequest);
|
||||
require(written.capture->snapshot.tables.tables.at(0).keys == 4u, "selected record fallbacks: a key loaded from a buffer the shader writes did not keep the whole table");
|
||||
require(!loadedPlan->descriptorSources.at(loadedPlan->info.images[0].source).tableColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a constant buffer has no key domain");
|
||||
auto pointerWritten = loadedKey;
|
||||
const auto end = std::find(pointerWritten.begin(), pointerWritten.end(), 0xbf810000u);
|
||||
require(end != pointerWritten.end(), "selected record fallbacks: the program has no s_endpgm");
|
||||
pointerWritten.insert(end, {0xdc708000u, 0x001c0100u});
|
||||
const auto pointerPlan = GetResourcePlan(TableRequest(pointerWritten, userData));
|
||||
const auto& pointerColumn = pointerPlan->descriptorSources.at(pointerPlan->info.images.at(0).source).tableColumn;
|
||||
require(pointerPlan->info.bdaWrites && pointerColumn.has_value() && !pointerColumn->selected && pointerColumn->keyDomains.empty(), "selected record fallbacks: a key loaded from a constant buffer in a shader that stores through a pointer selected its record or kept its key domain");
|
||||
|
||||
const auto lanePlan = GetResourcePlan(TableRequest(TableWholeCode, userData));
|
||||
require(!lanePlan->info.images.empty() && !lanePlan->descriptorSources.at(lanePlan->info.images[0].source).tableColumn->selected, "selected record fallbacks: a T# selected per lane left the table");
|
||||
|
||||
alignas(4096) static std::array<std::array<std::uint32_t, 8>, 70000> large{};
|
||||
large.back() = heap[1];
|
||||
FillTableSrt(guest, large.data(), 32u, static_cast<std::uint32_t>(large.size()));
|
||||
guest.srt[16] = static_cast<std::uint32_t>(large.size() - 1u);
|
||||
const auto pastCap = CompileTable(request);
|
||||
require(TableSelected(pastCap.capture->snapshot.tables, 0u, "selected record fallbacks: a record past the key cap does not hold one readable selected record") == heap[1] && !pastCap.capture->snapshot.tables.tables[0].outside && pastCap.result->imageTableFaults == 0u, "selected record fallbacks: a record past the key cap was not selected");
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
}
|
||||
|
||||
void verifySelectedRecordOffsetCarry() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture texture;
|
||||
alignas(4096) static TableGuest guest;
|
||||
std::fill(guest.heap.begin(), guest.heap.end(), std::array<std::uint32_t, 8>{});
|
||||
const auto image = TableT(texture.bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
auto* words = guest.heap.front().data();
|
||||
std::copy(image.begin(), image.end(), words + 0x50u / 4u);
|
||||
FillTableSrt(guest, guest.heap.data(), 0u, static_cast<std::uint32_t>(sizeof(guest.heap)));
|
||||
const auto userData = TableUserData(guest);
|
||||
const std::vector<std::uint32_t> code{0xf4080100u, 0xfa000000u, 0xf4080700u, 0xfa000030u, 0xf4080800u, 0xfa000010u, 0xf4000440u, 0xfa000040u, 0x8711ff11u,
|
||||
0x000000ffu, 0x8f128511u, 0x8113ff12u, 0xffffff10u, 0x8114ff12u, 0xffffff20u, 0xf4280902u, 0x26000040u, 0xf4280a02u, 0x28000040u, 0xf09c0f08u,
|
||||
0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u};
|
||||
const auto request = TableRequest(code, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "selected record offset carry: a T# read by two loads of its record was not planned as a table");
|
||||
const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.selected && !column.address && column.stride == 32u && column.addend == 0xffffff10u && column.offset == 0x40u && column.foldedAddend == 0xffffff10u && column.foldedCarry == 16u, "selected record offset carry: the column is wrong");
|
||||
guest.srt[16] = 7u;
|
||||
const auto carried = CompileTable(request);
|
||||
const auto& carriedTable = carried.capture->snapshot.tables.tables.at(0);
|
||||
require(carriedTable.keys == 1u && carriedTable.codes.at(0) == Abi::PoisonCode(static_cast<std::uint32_t>(Abi::PoisonReason::OffsetCarry)) && !TableTouched(carried.regions, guest.heap.data(), sizeof(guest.heap)), "selected record offset carry: the record whose second load wraps past 32 bits is not OffsetCarry poison or was read");
|
||||
guest.srt[16] = 8u;
|
||||
const auto wrapped = CompileTable(request);
|
||||
require(TableSelected(wrapped.capture->snapshot.tables, 0u, "selected record offset carry: the record past the wrap does not hold one readable selected record") == image && TableCovered(wrapped.regions, words + 0x50u / 4u, sizeof(image)), "selected record offset carry: the record past the wrap was not selected");
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> WithImageOperation(std::vector<std::uint32_t> code, std::uint32_t word0, bool keepSample) {
|
||||
const auto sample = std::find(code.begin(), code.end(), 0xf09c0f08u);
|
||||
require(sample != code.end() && sample + 1 != code.end(), "image operation variant: the program has no image_sample_lz");
|
||||
if (keepSample) {
|
||||
code.insert(sample + 2, {word0, 0x00050000u});
|
||||
} else {
|
||||
sample[0] = word0;
|
||||
sample[1] = 0x00050000u;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
void verifySelectedRecordDirectOperations() {
|
||||
using namespace ShaderRecompiler;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
std::fill(heap.begin(), heap.end(), std::array<std::uint32_t, 8>{});
|
||||
heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[2] = heap[1];
|
||||
heap[3] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
guest.srt[16] = 2u;
|
||||
const auto userData = TableUserData(guest);
|
||||
const std::array<std::tuple<const char*, std::uint32_t, std::uint32_t>, 4> operations{{{"image_store", 0xf0200f08u, TableFormat32UInt}, {"image_atomic_add", 0xf0440108u, TableFormat32UInt}, {"image_load_pck", 0xf0080f08u, TableFormat32UInt}, {"image_load_by2", 0xf1080308u, TableFormat16UNorm}}};
|
||||
for (const auto& [name, word0, format] : operations) {
|
||||
const auto what = std::string("selected record direct operations: ") + name;
|
||||
heap[2] = TableT(textures[1].bytes.data(), format, TableType2D);
|
||||
const auto code = WithImageOperation(TableSrtKeyCode, word0, false);
|
||||
const auto request = TableRequest(code, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == NoTable && !plan->descriptorSources.at(plan->info.images[0].source).tableColumn.has_value(), (what + " through a T# whose record the CPU evaluates was planned through an image table").c_str());
|
||||
const auto compiled = CompileTable(request);
|
||||
require(TableBinding(*compiled.result, DescriptorRole::ImageTable) == nullptr && compiled.result->imageTableFaults == 0u && TableCovered(compiled.regions, &heap[2], sizeof(heap[2])), (what + " through a T# whose record the CPU evaluates was not bound from its record").c_str());
|
||||
const auto wholeCode = WithImageOperation(TableWholeCode, word0, false);
|
||||
const auto whole = GetResourcePlan(TableRequest(wholeCode, userData));
|
||||
require(whole->info.images.size() == 1u && whole->info.images[0].table == 0u && whole->info.images[0].tableOperation == (word0 == 0xf0200f08u ? TableOperation::Supported : TableOperation::Unsupported), (what + " through a T# keyed per lane left the image table or has the wrong table operation").c_str());
|
||||
}
|
||||
const auto mixedCode = WithImageOperation(TableSrtKeyCode, 0xf0200f08u, true);
|
||||
const auto mixed = GetResourcePlan(TableRequest(mixedCode, userData));
|
||||
require(mixed->info.images.size() == 2u && std::ranges::all_of(mixed->info.images, [&](const ImageResource& image) { return image.table == NoTable && !mixed->descriptorSources.at(image.source).tableColumn.has_value(); }), "selected record direct operations: a T# sampled and stored was planned through an image table for one of its operations");
|
||||
const auto sampled = GetResourcePlan(TableRequest(TableSrtKeyCode, userData));
|
||||
require(sampled->info.images.size() == 1u && sampled->info.images[0].table == 0u && sampled->descriptorSources.at(sampled->info.images[0].source).tableColumn->selected, "selected record direct operations: a T# that is only sampled no longer selects its record");
|
||||
}
|
||||
|
||||
void verifyEvaluableHeapSamplerDirect() {
|
||||
using namespace ShaderRecompiler;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
std::fill(heap.begin(), heap.end(), std::array<std::uint32_t, 8>{});
|
||||
heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[2] = heap[1];
|
||||
heap[3] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
guest.srt[16] = 2u;
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto heapSampler = [](std::vector<std::uint32_t> code) {
|
||||
const auto direct = std::find(code.begin(), code.end(), 0xf4080200u);
|
||||
require(direct != code.end() && direct[1] == 0xfa000010u, "evaluable heap sampler: the program has no S# load from the SRT");
|
||||
code.erase(direct, direct + 2);
|
||||
const auto shift = std::find(code.begin(), code.end(), 0x8f108510u);
|
||||
require(shift != code.end(), "evaluable heap sampler: the program has no key shift");
|
||||
code.insert(shift + 1, {0xf4280202u, 0x20000000u});
|
||||
return code;
|
||||
};
|
||||
const auto evaluatedCode = heapSampler(TableSrtKeyCode);
|
||||
const auto evaluated = GetResourcePlan(TableRequest(evaluatedCode, userData));
|
||||
require(evaluated->info.samplers.size() == 1u && evaluated->info.samplers[0].table == NoTable && !evaluated->descriptorSources.at(evaluated->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset the SRT walker evaluates was planned through a sampler table");
|
||||
const auto perLaneCode = heapSampler(TableWholeCode);
|
||||
const auto perLane = GetResourcePlan(TableRequest(perLaneCode, userData));
|
||||
require(perLane->info.samplers.size() == 1u && perLane->info.samplers[0].table != NoTable && perLane->descriptorSources.at(perLane->info.samplers[0].source).tableColumn.has_value(), "evaluable heap sampler: an S# read from a heap at an offset keyed per lane was not planned through a sampler table");
|
||||
}
|
||||
|
||||
void verifySelectedRecordUniformIndex() {
|
||||
using namespace ShaderRecompiler;
|
||||
static TableTexture textures[2];
|
||||
alignas(4096) static TableGuest guest;
|
||||
auto& heap = guest.heap;
|
||||
std::fill(heap.begin(), heap.end(), std::array<std::uint32_t, 8>{});
|
||||
heap[0] = TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D);
|
||||
heap[1] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[2] = heap[1];
|
||||
heap[3] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 4u);
|
||||
guest.srt[16] = 2u;
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto twoWaves = [&](const std::vector<std::uint32_t>& code) {
|
||||
auto request = TableRequest(code, userData);
|
||||
request.context.compute->numThreads = {128u, 1u, 1u};
|
||||
return CompileTable(request);
|
||||
};
|
||||
const auto nonUniform = [](const TableCompiled& compiled) {
|
||||
const auto spirv = compiled.result->spirv.Words();
|
||||
std::uint32_t count = 0;
|
||||
for (std::size_t cursor = 5; cursor < spirv.size() && (spirv[cursor] >> 16u) != 0u; cursor += spirv[cursor] >> 16u) {
|
||||
if ((spirv[cursor] & 0xffffu) == spv::OpDecorate && (spirv[cursor] >> 16u) == 3u && spirv[cursor + 2u] == spv::DecorationNonUniform) ++count;
|
||||
}
|
||||
return count;
|
||||
};
|
||||
const auto selected = twoWaves(TableSrtKeyCode);
|
||||
require(TableBinding(*selected.result, DescriptorRole::ImageTable) != nullptr && nonUniform(selected) == 0u, "selected record uniform index: the image table index of a selected record in a two-wave workgroup is decorated NonUniform");
|
||||
const auto whole = twoWaves(TableWholeCode);
|
||||
require(nonUniform(whole) != 0u, "selected record uniform index: the image table index of a table keyed per lane in a two-wave workgroup is not decorated NonUniform");
|
||||
}
|
||||
|
||||
void verifyChainedTableOffset() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
static TableTexture textures[2];
|
||||
alignas(256) static std::array<std::uint32_t, 0x3000> root{};
|
||||
static std::array<std::uint32_t, 8> keys{};
|
||||
static std::array<std::uint32_t, 4> output{};
|
||||
const auto keysV = TableV(keys.data(), 0u, static_cast<std::uint32_t>(sizeof(keys)));
|
||||
const auto outputV = TableV(output.data(), 0u, static_cast<std::uint32_t>(sizeof(output)));
|
||||
std::copy(keysV.begin(), keysV.end(), root.begin());
|
||||
std::copy(outputV.begin(), outputV.end(), root.begin() + 4);
|
||||
const auto at = [&](std::uint32_t offset, std::array<std::uint32_t, 8> words) {
|
||||
std::copy(words.begin(), words.end(), root.begin() + offset / 4u);
|
||||
};
|
||||
at(0x60u + 3u * 32u, TableT(textures[0].bytes.data(), TableFormat8888UNorm, TableType2D));
|
||||
at(0x60u + 5u * 32u, TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D));
|
||||
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(root.data()));
|
||||
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(base), static_cast<std::uint32_t>(base >> 32u)};
|
||||
const std::vector<std::uint32_t> code{0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x7e200500u, 0x8f108210u, 0xf4200444u,
|
||||
0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0x8113a012u, 0x81149013u, 0xf4080900u, 0x26000040u, 0xf4080a00u, 0x28000040u, 0xf09c0f08u,
|
||||
0x01090000u, 0xe0700000u, 0x80070000u, 0xbf810000u};
|
||||
auto request = TableRequest(code, userData);
|
||||
request.shader.codeAddress = 0x37000u;
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u, "chained table offset: a T# whose second load offset adds to the first one's was not planned as a table");
|
||||
const auto& column = *plan->descriptorSources.at(plan->info.images[0].source).tableColumn;
|
||||
require(column.address && column.stride == 32u && column.addend == 0x20u && column.offset == 0x40u && column.maxKey == 0xffu && column.foldedAddend == 0x20u && column.foldedCarry == 16u, "chained table offset: the column is wrong");
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
const auto& words = compiled.capture->snapshot.tables.words;
|
||||
require(snapshot.keys == 256u && (snapshot.codes[3] & Abi::PoisonFlag) == 0u && (snapshot.codes[5] & Abi::PoisonFlag) == 0u, "chained table offset: the records were not resolved");
|
||||
require(words.at(snapshot.codes[3]).dwords[0] == root[(0x60u + 3u * 32u) / 4u] && words.at(snapshot.codes[3]).dwords[7] == root[(0x60u + 3u * 32u) / 4u + 7u] && words.at(snapshot.codes[5]).dwords[0] == root[(0x60u + 5u * 32u) / 4u] && snapshot.codes[3] != snapshot.codes[5], "chained table offset: a record was read from the wrong offset");
|
||||
}
|
||||
|
||||
void verifyDescriptorPhis() {
|
||||
using namespace ShaderRecompiler;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
@@ -2049,15 +2416,15 @@ void verifyUniformTableImage() {
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
const auto plan = GetResourcePlan(request);
|
||||
const auto& column = plan->descriptorSources.at(plan->info.images.at(0).source).tableColumn;
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && column.has_value() && column->stride == 48u && column->dwordCount == 8u, "uniform table image: the T# read from a constant buffer at a user data offset was not planned as one table column");
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && column.has_value() && column->stride == 48u && column->dwordCount == 8u && column->selected, "uniform table image: the T# read from a constant buffer at a user data offset does not select its record");
|
||||
for (const auto& [key, expected] : {std::pair{2u, second}, std::pair{1u, first}}) {
|
||||
userData[2] = key;
|
||||
AgcDriver::ShaderMemory memory({});
|
||||
const auto capture = memory.Capture(request);
|
||||
const auto& tables = capture->snapshot.tables;
|
||||
require(tables.tables.size() == 1u && key < tables.tables[0].codes.size(), "uniform table image: the capture has no table for the user data key");
|
||||
const auto code = tables.tables[0].codes[key];
|
||||
require((code & ImageTableAbi::PoisonFlag) == 0u && std::equal(expected.begin(), expected.end(), tables.words.at(code).dwords.begin()), "uniform table image: the T# of the record the user data selects was not read");
|
||||
require(tables.tables.size() == 1u && TableSelected(tables, 0u, "uniform table image: a table does not hold one readable selected record") == expected, "uniform table image: the T# of the record the user data selects was not read");
|
||||
const auto regions = memory.Regions();
|
||||
require(TableCovered(regions, guest.table[key].data() + 4, sizeof(expected)) && !TableCovered(regions, guest.table[3u - key].data() + 4, sizeof(std::uint32_t)), "uniform table image: the record the user data does not select was read");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4182,6 +4549,15 @@ void verifyUnnormalizedSamplers() {
|
||||
reject(0xf11c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz was accepted");
|
||||
reject(0xf1800308u, userData(unnormalized), "unnormalized guest sampler is used by image_get_lod, which is not implemented", "unnormalized samplers: image_get_lod was accepted");
|
||||
reject(0xf0800f09u, userData(unnormalized), "unnormalized guest sampler is used by an image_sample_*_a variant, which is not implemented", "unnormalized samplers: image_sample_a was accepted");
|
||||
const auto reservedDword3 = [&](std::uint32_t mimg) {
|
||||
auto code = program(mimg);
|
||||
const std::array<std::uint32_t, 4> computed{0x7d8a0080u, 0x870bff6au, 0x000000ffu, 0x8f0b8c0bu};
|
||||
code.insert(code.begin(), computed.begin(), computed.end());
|
||||
return code;
|
||||
};
|
||||
const auto adjusted = recompile(reservedDword3(0xf0800f09u), userData(normalized));
|
||||
require(proven(adjusted, false), "sampler dword 3: an image_sample_a whose S# dword 3 holds only a ballot in its reserved bits was not compiled");
|
||||
expectFailure([&] { static_cast<void>(recompile(reservedDword3(0xf0800f08u), userData(normalized))); }, "GetSamplerResource dword 3 is not a valid runtime value", "sampler dword 3: an image_sample whose S# dword 3 holds a ballot was accepted");
|
||||
reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted");
|
||||
reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted");
|
||||
reject(0xf09c0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array sample was accepted");
|
||||
@@ -5233,6 +5609,7 @@ int main(int argc, char** argv) {
|
||||
verifyImageTableKeys();
|
||||
verifyImageTables();
|
||||
verifyImageTableBulkReads();
|
||||
verifyImageTableBulkKeyScan();
|
||||
verifyImageTableSrtKey();
|
||||
verifyImageTableMaskedOffset();
|
||||
verifyImageTableKeyUnion();
|
||||
@@ -5242,6 +5619,12 @@ int main(int argc, char** argv) {
|
||||
verifyScalarAddressRule();
|
||||
verifyPointerImageTable();
|
||||
verifyPointerTableOffsetCarry();
|
||||
verifySelectedRecordFallbacks();
|
||||
verifySelectedRecordOffsetCarry();
|
||||
verifySelectedRecordDirectOperations();
|
||||
verifyEvaluableHeapSamplerDirect();
|
||||
verifySelectedRecordUniformIndex();
|
||||
verifyChainedTableOffset();
|
||||
std::cout << "Image table keys, maps, records and registration passed\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -5273,6 +5656,7 @@ int main(int argc, char** argv) {
|
||||
verifyScalarAddressRule();
|
||||
verifyPointerImageTable();
|
||||
verifyPointerTableOffsetCarry();
|
||||
verifyChainedTableOffset();
|
||||
verifyDescriptorPhis();
|
||||
verifyUniformTableImage();
|
||||
verifyLaneSelectedDescriptors();
|
||||
|
||||
@@ -32,6 +32,7 @@ struct ElementAddress {
|
||||
std::uint32_t y;
|
||||
std::uint32_t z;
|
||||
std::uint64_t address;
|
||||
std::uint32_t pipeBankXor = 0;
|
||||
};
|
||||
|
||||
std::uint64_t equationOffset(const TextureSwizzleEquation& equation, std::uint32_t x, std::uint32_t y, std::uint32_t z) {
|
||||
@@ -60,7 +61,7 @@ void requireThickAddresses(const GuestTextureResource& resource, std::uint32_t b
|
||||
const auto geometry = DescribeSurface(resource);
|
||||
Require(geometry.thick && geometry.mips.size() == resource.mipCount && geometry.guestBytes == guestBytes, what + ": guest size differs from addrlib");
|
||||
for (std::uint32_t level = 0; level < resource.mipCount; ++level) Require(geometry.mips[level].tail == (level >= firstTailLevel), what + ": mip " + std::to_string(level) + " is on the wrong side of the mip tail");
|
||||
const auto blockBytes = resource.tileMode == TextureTileMode::kStandard4KB ? 4096u : 65536u;
|
||||
const auto blockBytes = resource.tileMode == TextureTileMode::kStandard4KB || resource.tileMode == TextureTileMode::kS4KBX ? 4096u : 65536u;
|
||||
const auto* equation = FindTextureSwizzleEquation(0x100u | (resource.tileMode == TextureTileMode::kStandard4KB ? 5u : resource.tileMode == TextureTileMode::kStandard64KB ? 9u : XorSwizzleMode(resource.tileMode)), bytesPerElement);
|
||||
Require(equation != nullptr, what + ": missing thick swizzle equation");
|
||||
const auto block = ThickBlockExtent(resource.tileMode, bytesPerElement);
|
||||
@@ -73,6 +74,7 @@ void requireThickAddresses(const GuestTextureResource& resource, std::uint32_t b
|
||||
} else {
|
||||
address += (static_cast<std::uint64_t>(element.y / block[1]) * mip.blocksPerRow + element.x / block[0]) * blockBytes + equationOffset(*equation, element.x, element.y, element.z);
|
||||
}
|
||||
address ^= element.pipeBankXor;
|
||||
Require(address == element.address, what + ": mip " + std::to_string(element.mip) + " element (" + std::to_string(element.x) + ", " + std::to_string(element.y) + ", " + std::to_string(element.z) + ") detiles from " + std::to_string(address) + " instead of addrlib's " + std::to_string(element.address));
|
||||
}
|
||||
}
|
||||
@@ -258,6 +260,22 @@ void RunTextureTilingTests() {
|
||||
Require(odd.mips[0].blocksPerRow == 5 && odd.mips[0].tiledOffset == 12288 && odd.mips[0].tiledSize == 40960, "3D mip 0 of a non-power-of-two volume changed");
|
||||
}
|
||||
|
||||
{
|
||||
constexpr ElementAddress s4xTail1[] = {{0, 0, 0, 0, 0x3000, 0x0}, {0, 32, 19, 11, 0x843c, 0x0}, {0, 8, 9, 8, 0x3e08, 0x0}, {0, 15, 2, 8, 0x3163, 0x700}, {1, 0, 0, 0, 0x1000, 0x0}, {1, 15, 9, 5, 0x1acf, 0x0}, {1, 12, 4, 0, 0x1340, 0x0}, {1, 7, 2, 5, 0x17e7, 0x700}, {2, 0, 0, 0, 0x800, 0x0}, {2, 7, 4, 2, 0x953, 0x0}, {2, 2, 2, 0, 0x822, 0x0}, {2, 0, 1, 1, 0xf0c, 0x700}, {3, 0, 0, 0, 0x300, 0x0}, {3, 3, 1, 0, 0x30b, 0x0}, {3, 3, 1, 0, 0x30b, 0x0}, {3, 0, 0, 0, 0x400, 0x700}, {4, 0, 0, 0, 0x200, 0x0}, {4, 1, 0, 0, 0x201, 0x0}, {4, 0, 0, 0, 0x200, 0x0}, {4, 0, 0, 0, 0x500, 0x700}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x100, 0x0}, {5, 0, 0, 0, 0x600, 0x700}};
|
||||
requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 1, 33, 20, 12, 6), 1, 2, 36864, s4xTail1, "SW_4KB_S_X 8 bpp 33x20x12, 6 levels");
|
||||
constexpr ElementAddress s4xTail2[] = {{0, 0, 0, 0, 0x6000, 0x0}, {0, 32, 19, 11, 0xf13c, 0x0}, {0, 8, 9, 8, 0x7408, 0x0}, {0, 15, 2, 8, 0x7962, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x42ce, 0x0}, {1, 12, 4, 0, 0x4b00, 0x0}, {1, 7, 2, 5, 0x35e6, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x1352, 0x0}, {2, 2, 2, 0, 0x1060, 0x0}, {2, 0, 1, 1, 0x170c, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x84a, 0x0}, {3, 3, 1, 0, 0x84a, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x302, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}};
|
||||
requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 12, 33, 20, 12, 6), 2, 3, 65536, s4xTail2, "SW_4KB_S_X 16 bpp 33x20x12, 6 levels");
|
||||
constexpr ElementAddress s4xWide4[] = {{0, 0, 0, 0, 0x20000, 0x0}, {0, 127, 127, 7, 0x9f4fc, 0x0}, {0, 24, 93, 0, 0x73108, 0x0}, {0, 42, 22, 4, 0x35f60, 0x700}, {1, 0, 0, 0, 0x0, 0x0}, {1, 63, 63, 3, 0x1f0fc, 0x0}, {1, 28, 38, 2, 0x13da0, 0x0}, {1, 55, 52, 1, 0x1e754, 0x700}};
|
||||
requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 56, 128, 128, 8, 2), 4, 2, 655360, s4xWide4, "SW_4KB_S_X 32 bpp 128x128x8, 2 levels");
|
||||
constexpr ElementAddress s4xTail4[] = {{0, 0, 0, 0, 0x6000, 0x0}, {0, 32, 19, 11, 0x1fdb8, 0x0}, {0, 8, 9, 8, 0x17808, 0x0}, {0, 15, 2, 8, 0x17564, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x465c, 0x0}, {1, 12, 4, 0, 0x4b00, 0x0}, {1, 7, 2, 5, 0x3174, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x13c4, 0x0}, {2, 2, 2, 0, 0x1060, 0x0}, {2, 0, 1, 1, 0x1718, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x84c, 0x0}, {3, 3, 1, 0, 0x84c, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x304, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}};
|
||||
requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 56, 33, 20, 12, 6), 4, 3, 131072, s4xTail4, "SW_4KB_S_X 32 bpp 33x20x12, 6 levels");
|
||||
constexpr ElementAddress s4xTail8[] = {{0, 0, 0, 0, 0x9000, 0x0}, {0, 32, 19, 11, 0x2fab0, 0x0}, {0, 8, 9, 8, 0x27420, 0x0}, {0, 15, 2, 8, 0x22448, 0x700}, {1, 0, 0, 0, 0x3000, 0x0}, {1, 15, 9, 5, 0x7a78, 0x0}, {1, 12, 4, 0, 0x4200, 0x0}, {1, 7, 2, 5, 0x3058, 0x700}, {2, 0, 0, 0, 0x1000, 0x0}, {2, 7, 4, 2, 0x1ac8, 0x0}, {2, 2, 2, 0, 0x1140, 0x0}, {2, 0, 1, 1, 0x1730, 0x700}, {3, 0, 0, 0, 0x800, 0x0}, {3, 3, 1, 0, 0x868, 0x0}, {3, 3, 1, 0, 0x868, 0x0}, {3, 0, 0, 0, 0xf00, 0x700}, {4, 0, 0, 0, 0x300, 0x0}, {4, 1, 0, 0, 0x308, 0x0}, {4, 0, 0, 0, 0x300, 0x0}, {4, 0, 0, 0, 0x400, 0x700}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x200, 0x0}, {5, 0, 0, 0, 0x500, 0x700}};
|
||||
requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 71, 33, 20, 12, 6), 8, 3, 196608, s4xTail8, "SW_4KB_S_X 64 bpp 33x20x12, 6 levels");
|
||||
constexpr ElementAddress s4xTail16[] = {{0, 0, 0, 0, 0x10000, 0x0}, {0, 32, 19, 11, 0x55bb0, 0x0}, {0, 8, 9, 8, 0x46820, 0x0}, {0, 15, 2, 8, 0x3e040, 0x700}, {1, 0, 0, 0, 0x6000, 0x0}, {1, 15, 9, 5, 0xee70, 0x0}, {1, 12, 4, 0, 0x9400, 0x0}, {1, 7, 2, 5, 0x7850, 0x700}, {2, 0, 0, 0, 0x3000, 0x0}, {2, 7, 4, 2, 0x42c0, 0x0}, {2, 2, 2, 0, 0x3300, 0x0}, {2, 0, 1, 1, 0x3730, 0x700}, {3, 0, 0, 0, 0x1000, 0x0}, {3, 3, 1, 0, 0x1260, 0x0}, {3, 3, 1, 0, 0x1260, 0x0}, {3, 0, 0, 0, 0x1700, 0x700}, {4, 0, 0, 0, 0x800, 0x0}, {4, 1, 0, 0, 0x840, 0x0}, {4, 0, 0, 0, 0x800, 0x0}, {4, 0, 0, 0, 0xf00, 0x700}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x300, 0x0}, {5, 0, 0, 0, 0x400, 0x700}};
|
||||
requireThickAddresses(thickVolume(TextureTileMode::kS4KBX, 77, 33, 20, 12, 6), 16, 4, 352256, s4xTail16, "SW_4KB_S_X 128 bpp 33x20x12, 6 levels");
|
||||
requireBlockVolumeLayout(TextureTileMode::kS4KBX, 169, 71, 60, 36, 20, 5, "SW_4KB_S_X BC1 60x36x20, 5 levels");
|
||||
}
|
||||
|
||||
{
|
||||
constexpr ElementAddress chainInTail[] = {{0, 0, 0, 0, 0x800}, {0, 7, 7, 7, 0xffc}, {0, 4, 2, 4, 0xe20}, {0, 2, 7, 0, 0x968}, {0, 7, 0, 7, 0xed4}, {1, 0, 0, 0, 0x300}, {1, 3, 3, 3, 0x3fc}, {1, 2, 1, 2, 0x3c8}, {1, 1, 3, 0, 0x32c}, {1, 3, 0, 3, 0x3d4}};
|
||||
requireThickAddresses(thickVolume(TextureTileMode::kStandard4KB, 56, 8, 8, 8, 2), 4, 0, 4096, chainInTail, "SW_4KB_S 32 bpp 8x8x8, 2 levels");
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
constexpr std::uint32_t VertexBuffers = 3;
|
||||
constexpr std::size_t VertexTableOffset = 0x300;
|
||||
constexpr std::size_t VertexDataOffset = 0x800;
|
||||
constexpr std::size_t ColorOffset = 0;
|
||||
constexpr std::size_t ColorBytes = 65536;
|
||||
constexpr std::uint32_t RenderTargetTileMode = 0x1b;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 40> VertexCode{
|
||||
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
|
||||
0x7e060280u, 0x7e0802f2u,
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u,
|
||||
0xbf8cc07fu,
|
||||
0xe00c2000u, 0x80010a05u, 0xe00c2000u, 0x80020e05u, 0xe00c2000u, 0x80031205u,
|
||||
0xbf8c3f70u,
|
||||
0x0602030au, 0x0604050bu, 0x0606070cu, 0x0608090du,
|
||||
0x0602030eu, 0x0604050fu, 0x06060710u, 0x06080911u,
|
||||
0x06020312u, 0x06040513u, 0x06060714u, 0x06080915u,
|
||||
0xf80008cfu, 0x04030201u,
|
||||
0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 40> VertexCodeUser8 = [] {
|
||||
auto code = VertexCode;
|
||||
for (const std::size_t load : {10u, 12u, 14u}) code[load] |= 4u;
|
||||
return code;
|
||||
}();
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> PixelRead{0x7e1a0280u, 0xdc308000u, 0x0c02000du, 0xbf8c3f70u, 0x7e10030cu, 0x7e120280u, 0x7e140280u, 0x7e160280u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 9> PixelColorWord{0x7e100280u, 0x7e120280u, 0x7e1402ffu, 0x3f008081u, 0x7e1602ffu, 0x3e7cfcfdu, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> PixelDescriptorStore{0x7e0202f2u, 0x7e040280u, 0xe0701000u, 0x80010102u, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> PixelStore{0x7e1a0280u, 0x7e1802f2u, 0xdc708000u, 0x00020c0du, 0x7e1002f2u, 0x7e1202f2u, 0x7e1402f2u, 0x7e1602f2u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData userData{};
|
||||
};
|
||||
|
||||
alignas(8) Header VertexHeader{};
|
||||
alignas(8) Header ReadHeader{};
|
||||
alignas(8) Header StoreHeader{};
|
||||
alignas(8) Header DescriptorStoreHeader{};
|
||||
alignas(8) Header ColorWordHeader{};
|
||||
|
||||
void Register(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(Header);
|
||||
header.shader.shader_size = static_cast<std::uint32_t>(bytes);
|
||||
header.shader.code = code;
|
||||
header.shader.type = type;
|
||||
header.shader.user_data = &header.userData;
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
}
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, 65536));
|
||||
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes);
|
||||
#else
|
||||
constexpr std::uintptr_t alignment = 65536;
|
||||
void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (mapped != MAP_FAILED) {
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
|
||||
const auto aligned = (begin + alignment - 1) & ~(alignment - 1);
|
||||
if (aligned != begin) munmap(mapped, aligned - begin);
|
||||
if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast<void*>(aligned + BlockBytes), begin + alignment - aligned);
|
||||
block = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes);
|
||||
}
|
||||
#endif
|
||||
Require(block != nullptr, "address passes: cannot allocate a guest block");
|
||||
std::memset(block, 0, BlockBytes);
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
void Map(bool writable) const { GuestAllocations::Mutation().Add(block, BlockBytes, true, writable, true); }
|
||||
std::uint64_t At(std::size_t offset) const { return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(block + offset)); }
|
||||
std::uint8_t* Bytes(std::size_t offset) const { return block + offset; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(std::uint64_t address, std::uint32_t stride, std::uint32_t records, std::uint32_t word3) {
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), records, word3};
|
||||
}
|
||||
|
||||
void FillVertices(const GuestBlock& block) {
|
||||
for (std::uint32_t buffer = 0; buffer < VertexBuffers; ++buffer) {
|
||||
const auto vertices = BufferDescriptor(block.At(VertexDataOffset + buffer * 0x100u), 16, 3, 0x0104dfacu);
|
||||
std::memcpy(block.Bytes(VertexTableOffset + buffer * 16u), vertices.data(), sizeof(vertices));
|
||||
}
|
||||
}
|
||||
|
||||
void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::initializer_list<std::uint32_t> payload) {
|
||||
commands.push_back(0xc0000000u | (static_cast<std::uint32_t>(payload.size() - 1) << 16u) | (opcode << 8u));
|
||||
commands.insert(commands.end(), payload);
|
||||
}
|
||||
|
||||
struct Step {
|
||||
const std::uint32_t* pixel;
|
||||
std::uint64_t pointer;
|
||||
std::array<std::uint32_t, 4> descriptor{};
|
||||
};
|
||||
|
||||
void Submit(const GuestBlock& vertices, const GuestBlock& target, std::initializer_list<Step> steps, bool linear = false) {
|
||||
const auto color = target.At(ColorOffset);
|
||||
const auto vertex = reinterpret_cast<std::uintptr_t>(VertexCodeUser8.data());
|
||||
const auto table = vertices.At(VertexTableOffset);
|
||||
const float scale = 32.0f;
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, (63u << 14u) | 3u},
|
||||
{0x3b8, linear ? 0x9000000u : 0x9000000u | (RenderTargetTileMode << 14u)}, {0x1e0, 0},
|
||||
{0xc, 0}, {0xd, 0x40040},
|
||||
{0x81, 0x80000000}, {0x82, 0x40040},
|
||||
{0x90, 0x80000000}, {0x91, 0x40040},
|
||||
{0x94, 0x80000000}, {0x95, 0x40040},
|
||||
{0x318, static_cast<std::uint32_t>(color >> 8u)}, {0x390, static_cast<std::uint32_t>(color >> 40u)},
|
||||
{0x10f, std::bit_cast<std::uint32_t>(scale)}, {0x110, std::bit_cast<std::uint32_t>(scale)},
|
||||
{0x111, std::bit_cast<std::uint32_t>(-2.0f)}, {0x112, std::bit_cast<std::uint32_t>(2.0f)},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)},
|
||||
{0x2fe, 0}, {0x302, 0}, {0x306, 0}, {0x30a, 0},
|
||||
};
|
||||
std::vector<std::uint32_t> commands;
|
||||
Append(commands, 0x79, {0x242, 4});
|
||||
for (const auto& [offset, value] : context) Append(commands, 0x69, {offset, value});
|
||||
for (const auto& [offset, value] : std::initializer_list<std::pair<std::uint32_t, std::uint32_t>>{{0xc8, static_cast<std::uint32_t>(vertex >> 8u)}, {0xc9, static_cast<std::uint32_t>(vertex >> 40u)}, {0x8b, 2u << 1u}, {0x8c, static_cast<std::uint32_t>(table)}, {0x8d, static_cast<std::uint32_t>(table >> 32u)}, {0x00b, 8u << 1u}, {0x00c, 0}, {0x00d, 0}}) Append(commands, 0x76, {offset, value});
|
||||
for (const auto& step : steps) {
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(step.pixel);
|
||||
Append(commands, 0x76, {0x008, static_cast<std::uint32_t>(pixel >> 8u), static_cast<std::uint32_t>(pixel >> 40u)});
|
||||
Append(commands, 0x76, {0x00e, static_cast<std::uint32_t>(step.pointer), static_cast<std::uint32_t>(step.pointer >> 32u)});
|
||||
Append(commands, 0x76, {0x010, step.descriptor[0], step.descriptor[1], step.descriptor[2], step.descriptor[3]});
|
||||
Append(commands, 0x2d, {3, 2});
|
||||
}
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
Require(sceAgcDriverSubmitDcb(&packet) == 0, "address passes: the draw submission failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
AgcDriver::Graphics::DrawPassCounts AddressDraws() {
|
||||
return AgcDriver::Graphics::DrawPassTotals()[1];
|
||||
}
|
||||
|
||||
AgcDriver::Graphics::DrawPassCounts OtherDraws() {
|
||||
return AgcDriver::Graphics::DrawPassTotals()[0];
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> StoredDword(const GuestBlock& data, std::size_t offset) {
|
||||
return BufferDescriptor(data.At(offset), 0, 4, 0x31016facu);
|
||||
}
|
||||
|
||||
void Expect(bool condition, const std::string& what) {
|
||||
if (!condition) throw std::runtime_error("address passes: " + what);
|
||||
}
|
||||
|
||||
std::string Counts(const AgcDriver::Graphics::DrawPassCounts& before, const AgcDriver::Graphics::DrawPassCounts& after, const char* draws = "address-based draws") {
|
||||
return std::to_string(after.draws - before.draws) + " " + draws + ", " + std::to_string(after.passesBegun - before.passesBegun) + " began a pass, " + std::to_string(after.passesContinued - before.passesContinued) + " continued one";
|
||||
}
|
||||
|
||||
void ReadersShareAPass(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) {
|
||||
const auto before = AddressDraws();
|
||||
Submit(vertices, target, {{PixelRead.data(), data.At(0)}, {PixelRead.data(), data.At(0)}});
|
||||
const auto after = AddressDraws();
|
||||
std::printf("two draws reading through the table: %s\n", Counts(before, after).c_str());
|
||||
Expect(after.draws - before.draws == 2 && after.passesContinued - before.passesContinued == 1, "a draw reading through the table after another did not continue its pass: " + Counts(before, after));
|
||||
}
|
||||
|
||||
void StoreAfterTableReadBreaks(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) {
|
||||
const auto before = AddressDraws();
|
||||
Submit(vertices, target, {{PixelRead.data(), data.At(32)}, {PixelStore.data(), data.At(32)}});
|
||||
const auto after = AddressDraws();
|
||||
std::printf("a draw storing through the table after a draw reading through it: %s\n", Counts(before, after).c_str());
|
||||
Expect(after.draws - before.draws == 2 && after.passesBegun - before.passesBegun == 2, "a draw storing through the table continued the pass of a draw that read through it: " + Counts(before, after));
|
||||
}
|
||||
|
||||
void ReadSeesStore(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) {
|
||||
std::memset(target.Bytes(ColorOffset), 0, ColorBytes);
|
||||
std::memset(data.Bytes(48), 0, 4);
|
||||
const auto before = AddressDraws();
|
||||
Submit(vertices, target, {{PixelStore.data(), data.At(48)}, {PixelRead.data(), data.At(48)}});
|
||||
const auto after = AddressDraws();
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(static_cast<std::uintptr_t>(target.At(ColorOffset)), ColorBytes);
|
||||
float stored = 0;
|
||||
std::memcpy(&stored, data.Bytes(48), sizeof(stored));
|
||||
unsigned brightest = 0;
|
||||
for (std::size_t i = 0; i < ColorBytes; ++i) brightest = std::max(brightest, static_cast<unsigned>(target.Bytes(ColorOffset)[i]));
|
||||
std::printf("a draw reading through the table what the previous draw stored: stored %g, brightest color byte %u, %s\n", static_cast<double>(stored), brightest, Counts(before, after).c_str());
|
||||
Expect(stored == 1.0f, "the store through the table did not reach guest memory");
|
||||
Expect(brightest == 255u, "the read through the table did not see the previous draw's store");
|
||||
Expect(after.passesBegun - before.passesBegun == 2, "a draw reading through the table continued the pass of a draw that stored through it: " + Counts(before, after));
|
||||
}
|
||||
|
||||
void ReadSeesDescriptorStore(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) {
|
||||
std::memset(target.Bytes(ColorOffset), 0, ColorBytes);
|
||||
std::memset(data.Bytes(64), 0, 4);
|
||||
const auto before = AddressDraws();
|
||||
const auto otherBefore = OtherDraws();
|
||||
Submit(vertices, target, {{PixelDescriptorStore.data(), 0, StoredDword(data, 64)}, {PixelRead.data(), data.At(64)}});
|
||||
const auto after = AddressDraws();
|
||||
const auto otherAfter = OtherDraws();
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(static_cast<std::uintptr_t>(target.At(ColorOffset)), ColorBytes);
|
||||
float stored = 0;
|
||||
std::memcpy(&stored, data.Bytes(64), sizeof(stored));
|
||||
unsigned brightest = 0;
|
||||
for (std::size_t i = 0; i < ColorBytes; ++i) brightest = std::max(brightest, static_cast<unsigned>(target.Bytes(ColorOffset)[i]));
|
||||
std::printf("a draw reading through the table what the previous draw stored through a buffer descriptor: stored %g, brightest color byte %u, %s; the storing draw: %s\n", static_cast<double>(stored), brightest, Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str());
|
||||
Expect(otherAfter.draws - otherBefore.draws == 1, "the draw storing through a buffer descriptor was taken for an address-based one: " + Counts(otherBefore, otherAfter, "other draws"));
|
||||
Expect(stored == 1.0f, "the store through the buffer descriptor did not reach guest memory");
|
||||
Expect(after.draws - before.draws == 1 && after.passesBegun - before.passesBegun == 1, "a draw reading through the table continued the pass of a draw that stored through a buffer descriptor: " + Counts(before, after));
|
||||
Expect(brightest == 255u, "the read through the table did not see the previous draw's store through a buffer descriptor");
|
||||
}
|
||||
|
||||
void DescriptorStoreAfterTableReadBreaks(const GuestBlock& vertices, const GuestBlock& data, const GuestBlock& target) {
|
||||
const auto before = AddressDraws();
|
||||
const auto otherBefore = OtherDraws();
|
||||
Submit(vertices, target, {{PixelRead.data(), data.At(80)}, {PixelDescriptorStore.data(), 0, StoredDword(data, 80)}});
|
||||
const auto after = AddressDraws();
|
||||
const auto otherAfter = OtherDraws();
|
||||
std::printf("a draw storing through a buffer descriptor after a draw reading through the table: %s; the storing draw: %s\n", Counts(before, after).c_str(), Counts(otherBefore, otherAfter, "other draws").c_str());
|
||||
Expect(after.draws - before.draws == 1 && otherAfter.draws - otherBefore.draws == 1, "the reading and the storing draw were not one address-based and one other draw");
|
||||
Expect(otherAfter.passesBegun - otherBefore.passesBegun == 1, "a draw storing through a buffer descriptor continued the pass of a draw that read through the table: " + Counts(otherBefore, otherAfter, "other draws"));
|
||||
}
|
||||
|
||||
void TargetReadAsWhenSplit(const GuestBlock& vertices, const GuestBlock& target, bool linear) {
|
||||
const bool split = std::getenv("APS5_NO_ADDRESS_READ_PASSES") != nullptr;
|
||||
Submit(vertices, target, {{PixelRead.data(), target.At(ColorOffset)}}, linear);
|
||||
std::memset(target.Bytes(ColorOffset), 0, ColorBytes);
|
||||
const auto before = AddressDraws();
|
||||
Submit(vertices, target, {{PixelColorWord.data(), 0}, {PixelRead.data(), target.At(ColorOffset)}}, linear);
|
||||
const auto after = AddressDraws();
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(static_cast<std::uintptr_t>(target.At(ColorOffset)), ColorBytes);
|
||||
const unsigned red = target.Bytes(ColorOffset)[0];
|
||||
const auto what = std::string(linear ? "linear" : "tiled") + " target" + (split ? " with APS5_NO_ADDRESS_READ_PASSES=1" : "");
|
||||
std::printf("%s: a draw reading through the table the target the previous draw rendered read %s (red byte %u), %s\n", what.c_str(), red == 255u ? "that draw's color" : "the bytes before it", red, Counts(before, after).c_str());
|
||||
Expect(red == (linear ? 255u : 0u), what + ": a draw reading through the table the target the previous draw rendered read " + (red == 255u ? "that draw's color" : "the bytes before it") + "; whether or not the pass breaks between them, it reads " + (linear ? "that draw's color (stored back after the draw)" : "the bytes before it (no write-back between them)"));
|
||||
if (!linear && !split) Expect(after.passesContinued - before.passesContinued == 1, what + ": the reading draw did not continue the rendering draw's pass, so the case does not test it: " + Counts(before, after));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
#ifdef _WIN32
|
||||
_putenv_s("APS5_PROFILE_DRAW", "1");
|
||||
#else
|
||||
setenv("APS5_PROFILE_DRAW", "1", 1);
|
||||
#endif
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
}
|
||||
Register(VertexHeader, VertexCodeUser8.data(), sizeof(VertexCodeUser8), 2);
|
||||
Register(ReadHeader, PixelRead.data(), sizeof(PixelRead), 1);
|
||||
Register(StoreHeader, PixelStore.data(), sizeof(PixelStore), 1);
|
||||
Register(DescriptorStoreHeader, PixelDescriptorStore.data(), sizeof(PixelDescriptorStore), 1);
|
||||
Register(ColorWordHeader, PixelColorWord.data(), sizeof(PixelColorWord), 1);
|
||||
GuestBlock readOnlyVertices;
|
||||
GuestBlock data;
|
||||
GuestBlock target;
|
||||
FillVertices(readOnlyVertices);
|
||||
readOnlyVertices.Map(false);
|
||||
data.Map(true);
|
||||
target.Map(true);
|
||||
const std::string only = argc > 1 ? argv[1] : "";
|
||||
if (only.empty() || only == "--readers") ReadersShareAPass(readOnlyVertices, data, target);
|
||||
if (only.empty() || only == "--store-after-read") StoreAfterTableReadBreaks(readOnlyVertices, data, target);
|
||||
if (only.empty() || only == "--read-sees-store") ReadSeesStore(readOnlyVertices, data, target);
|
||||
if (only.empty() || only == "--read-sees-descriptor-store") ReadSeesDescriptorStore(readOnlyVertices, data, target);
|
||||
if (only.empty() || only == "--descriptor-store-after-read") DescriptorStoreAfterTableReadBreaks(readOnlyVertices, data, target);
|
||||
if (only.empty() || only == "--targets") {
|
||||
TargetReadAsWhenSplit(readOnlyVertices, target, false);
|
||||
TargetReadAsWhenSplit(readOnlyVertices, target, true);
|
||||
}
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::puts("address passes: draws reading through the BDA table share a render pass, and a store through it still begins one");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try { AgcDriverShutdown_nid_postfix(); } catch (...) {}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -514,6 +514,27 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa
|
||||
}
|
||||
}
|
||||
}
|
||||
if (code == 0 && !alphaOnMsb && cmaskMode == 0) {
|
||||
const auto dccBytes = AgcDriver::Graphics::MultisampledDccBytes(target);
|
||||
Require(dccBytes + 8u <= metadata.bytes, "the DCC fixture has no adjacent-page control");
|
||||
const std::array<std::byte, 4> adjacent{std::byte{0x7b}, std::byte{0x7b}, std::byte{0x7b}, std::byte{0x7b}};
|
||||
AgcDriver::GuestMemory::Write(metadata.Address() + dccBytes, adjacent);
|
||||
device.ColorMetadataPass(pass);
|
||||
Require(ReadBack(device, resolved) == stored, "an adjacent driver store changed the resident DCC target");
|
||||
static_cast<volatile std::uint8_t*>(metadata.data)[dccBytes + 4u] = 0x7c;
|
||||
AgcDriver::GuestMemory::CollectWritesUncached(metadata.Address() + dccBytes, 8u);
|
||||
device.ColorMetadataPass(pass);
|
||||
Require(ReadBack(device, resolved) == stored, "an adjacent CPU store changed the resident DCC target");
|
||||
const std::array<std::byte, 1> same{std::byte{0}};
|
||||
AgcDriver::GuestMemory::Write(metadata.Address(), same);
|
||||
bool refused = false;
|
||||
try {
|
||||
device.ColorMetadataPass(pass);
|
||||
} catch (const std::runtime_error& error) {
|
||||
refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos;
|
||||
}
|
||||
Require(refused, "a same-value driver DCC store did not invalidate multisampled DCC residency");
|
||||
}
|
||||
if (code == 0 && alphaOnMsb && cmaskMode == 0) {
|
||||
static_cast<volatile std::uint8_t*>(source.data)[0] = 0;
|
||||
bool refused = false;
|
||||
|
||||
@@ -42,7 +42,10 @@ alignas(256) constexpr std::array<std::uint32_t, 11> PairCode{
|
||||
0xe0741000, 0x80020302, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 3> AtomicCode{0xe0c81000, 0x80000100, 0xbf810000};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> AtomicCode{
|
||||
0x34040082, 0x4a040408, 0x4a020081, 0xe0c85000, 0x80000102, 0xbf8c3f70, 0x34060082, 0xe0701000,
|
||||
0x80010103, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::uint32_t, 7> FormattedLoadCode(std::uint32_t format, std::uint32_t offset) {
|
||||
return {0x34020082, 0xe8001000u | (format << 19u) | offset, 0x80000201, 0xbf8c0000, 0xe0701000, 0x80010201, 0xbf810000};
|
||||
@@ -218,20 +221,32 @@ void RunFormattedStore(AgcDriver::VulkanDevice& device, std::uint32_t viewOffset
|
||||
}
|
||||
}
|
||||
|
||||
void RunAtomic(AgcDriver::VulkanDevice& device) {
|
||||
void DispatchAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment, std::uint32_t addend) {
|
||||
Output.fill(Fill);
|
||||
std::vector<std::uint32_t> userData;
|
||||
const auto descriptor = BufferDescriptor(reinterpret_cast<std::uint8_t*>(Output.data()) + 2u, 64u);
|
||||
userData.insert(userData.end(), descriptor.begin(), descriptor.end());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(AtomicCode.data()), std::as_bytes(std::span(AtomicCode))}}};
|
||||
try {
|
||||
const auto result = ShaderRecompiler::Recompile(Request(AtomicCode, userData, device.Target(), memory));
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(AtomicCode.data()));
|
||||
device.WaitIdle();
|
||||
} catch (const std::exception& error) {
|
||||
Require(std::string(error.what()).find("buffer atomic on a V# whose base is not DWORD aligned") != std::string::npos, std::string("buffer unaligned base atomic: unexpected error: ") + error.what());
|
||||
return;
|
||||
for (const auto& descriptor : {BufferDescriptor(reinterpret_cast<std::uint8_t*>(Output.data()) + misalignment, (Lanes + 2u) * 4u), BufferDescriptor(Output.data() + 128, Lanes * 4u)}) {
|
||||
userData.insert(userData.end(), descriptor.begin(), descriptor.end());
|
||||
}
|
||||
throw std::runtime_error("buffer unaligned base atomic: an atomic on a V# off a DWORD boundary ran");
|
||||
userData.push_back(addend);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(AtomicCode.data()), std::as_bytes(std::span(AtomicCode))}}};
|
||||
const auto result = ShaderRecompiler::Recompile(Request(AtomicCode, userData, device.Target(), memory));
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(AtomicCode.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void RunAtomic(AgcDriver::VulkanDevice& device, std::uint32_t misalignment) {
|
||||
DispatchAtomic(device, misalignment, 4u - misalignment);
|
||||
const auto run = "buffer unaligned base atomic, view +" + std::to_string(misalignment) + ", offset lane * 4 + " + std::to_string(4u - misalignment);
|
||||
for (std::uint32_t word = 0; word < 128u; ++word) {
|
||||
const auto expected = word >= 1u && word <= Lanes ? Fill + word : Fill;
|
||||
Require(Output[word] == expected, run + ": dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", expected " + Hex(expected));
|
||||
}
|
||||
for (std::uint32_t lane = 0; lane < Lanes; ++lane) Require(Output[128u + lane] == Fill, run + ": lane " + std::to_string(lane) + " returned " + Hex(Output[128u + lane]) + ", expected " + Hex(Fill));
|
||||
}
|
||||
|
||||
void RunAtomicFault(AgcDriver::VulkanDevice& device) {
|
||||
DispatchAtomic(device, 2u, 0u);
|
||||
for (std::uint32_t word = 0; word < Output.size(); ++word) Require(Output[word] == Fill, "buffer unaligned base atomic, view +2, offset lane * 4: dword " + std::to_string(word) + " is " + Hex(Output[word]) + ", but an atomic off a DWORD boundary faults the wave on RDNA2");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -246,7 +261,7 @@ int main() {
|
||||
RunPair(*device, 1);
|
||||
RunPair(*device, 2);
|
||||
RunPair(*device, 3);
|
||||
RunAtomic(*device);
|
||||
for (const auto view : {1u, 2u, 3u}) RunAtomic(*device, view);
|
||||
RunFormattedLoad(*device, 2);
|
||||
RunFormattedStore(*device, 2);
|
||||
for (const auto view : {1u, 3u}) {
|
||||
@@ -257,6 +272,7 @@ int main() {
|
||||
RunFormattedFault(*device, "tbuffer_load_format_x 32_UINT", Load32Code, view);
|
||||
RunFormattedFault(*device, "tbuffer_load_format_x 16_16_UINT", Load16_16Code, view);
|
||||
}
|
||||
RunAtomicFault(*device);
|
||||
std::puts("buffer unaligned base tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "Optimization/ResourceProgram.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::ColorTargetLayout;
|
||||
using AgcDriver::Graphics::ColorTileMode;
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Width = 256;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::uint32_t ScissorWidth = 128;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t Color565StandardInfo = 0x8040u;
|
||||
constexpr std::uint32_t Color565ReversedInfo = 0x9040u;
|
||||
constexpr std::uint32_t TiledAttrib3 = 0x4dc6c000u;
|
||||
constexpr std::uint32_t LinearAttrib3 = 0x09000000u;
|
||||
constexpr std::uint32_t Float32Export = 9;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 6> VertexCode{
|
||||
0xe0382000, 0x80000005, 0xbf8c3f70, 0xf80008cf, 0x03020100, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<float, 4>, 3> Triangle{{
|
||||
{-1.0f, -1.0f, 0.5f, 1.0f}, {3.0f, -1.0f, 0.5f, 1.0f}, {-1.0f, 3.0f, 0.5f, 1.0f}
|
||||
}};
|
||||
|
||||
alignas(256) std::array<std::array<std::uint32_t, 64>, 16> Programs{};
|
||||
std::size_t programCount = 0;
|
||||
|
||||
using Color = std::array<float, 4>;
|
||||
|
||||
std::uint16_t Texel(const Color& color, bool reversed) {
|
||||
const auto unorm = [](float value, std::uint32_t maximum) { return static_cast<std::uint32_t>(std::lround(std::fmin(std::fmax(static_cast<double>(value), 0.0), 1.0) * maximum)); };
|
||||
const auto low = unorm(color[reversed ? 2 : 0], 31);
|
||||
const auto high = unorm(color[reversed ? 0 : 2], 31);
|
||||
return static_cast<std::uint16_t>(low | (unorm(color[1], 63) << 5u) | (high << 11u));
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> Program(const Color& color) {
|
||||
Require(programCount < Programs.size(), "too many test programs");
|
||||
auto& code = Programs[programCount++];
|
||||
std::size_t at = 0;
|
||||
for (std::uint32_t channel = 0; channel < 4; ++channel) {
|
||||
code[at++] = 0x7e0002ffu | ((4u + channel) << 17u);
|
||||
code[at++] = std::bit_cast<std::uint32_t>(color[channel]);
|
||||
}
|
||||
code[at++] = 0xf800180fu;
|
||||
code[at++] = 0x07060504u;
|
||||
code[at++] = 0xbf810000u;
|
||||
return std::span<const std::uint32_t>(code.data(), at);
|
||||
}
|
||||
|
||||
struct Block {
|
||||
explicit Block(std::size_t bytes) : bytes(bytes), watched(AgcDriver::GuestMemory::WriteWatched()) {
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
data = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, BlockBytes));
|
||||
GuestArena::GuestArenaCommit_nid_postfix(data, bytes, PAGE_READWRITE, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
void* raw = mmap(nullptr, bytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
Require(raw != MAP_FAILED, "cannot map the color target");
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(raw);
|
||||
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
|
||||
if (aligned != begin) munmap(raw, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + bytes), begin + BlockBytes - aligned);
|
||||
data = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
data = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, bytes));
|
||||
}
|
||||
#endif
|
||||
Require(data != nullptr, "cannot allocate the color target");
|
||||
Require(!watched || AgcDriver::GuestMemory::Watched(Address(), bytes), "the color target is not write-watched");
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(data, bytes, true, true, true);
|
||||
}
|
||||
~Block() {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(Address(), bytes, nullptr, "test release");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(data);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
if (watched) {
|
||||
GuestArena::GuestArenaReset_nid_postfix(data, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(data, bytes);
|
||||
} else {
|
||||
VirtualFree(data, 0, MEM_RELEASE);
|
||||
}
|
||||
#else
|
||||
if (watched) {
|
||||
munmap(data, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, bytes);
|
||||
} else {
|
||||
std::free(data);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
Block(const Block&) = delete;
|
||||
Block& operator=(const Block&) = delete;
|
||||
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
|
||||
std::size_t bytes;
|
||||
bool watched;
|
||||
std::uint8_t* data = nullptr;
|
||||
};
|
||||
|
||||
AgcDriver::Graphics::ColorTarget DecodeTarget(const Block& block, std::uint32_t attrib3, std::uint32_t info) {
|
||||
AgcDriver::Registers cx;
|
||||
cx[0x318] = static_cast<std::uint32_t>(block.Address() >> 8u);
|
||||
cx[0x31b] = 0;
|
||||
cx[0x31c] = info;
|
||||
cx[0x31d] = 0;
|
||||
cx[0x3b0] = ((Width - 1u) << 14u) | (Height - 1u);
|
||||
cx[0x3b8] = attrib3;
|
||||
cx[0x390] = static_cast<std::uint32_t>(block.Address() >> 40u);
|
||||
auto color = AgcDriver::Graphics::DecodeColorBuffer(cx, 0);
|
||||
Require(color.format == VK_FORMAT_R5G6B5_UNORM_PACK16 && color.elementBytes == 2 && color.bytes == block.bytes, "the 5_6_5 unorm target did not decode as an R5G6B5 surface");
|
||||
return color;
|
||||
}
|
||||
|
||||
void Draw(AgcDriver::VulkanDevice& device, const AgcDriver::Graphics::ColorTarget& color, std::uint32_t exportFormat, std::span<const std::uint32_t> pixelCode) {
|
||||
const auto target = device.Target();
|
||||
constexpr std::uint32_t waveSize = 64;
|
||||
std::vector<std::uint32_t> vertexUserData(4, 0u);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Triangle.data());
|
||||
const std::array<std::uint32_t, 4> vertexBuffer{static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (16u << 16u), static_cast<std::uint32_t>(Triangle.size()), 0x01016facu};
|
||||
std::copy(vertexBuffer.begin(), vertexBuffer.end(), vertexUserData.begin());
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> vertexMemory{{{reinterpret_cast<std::uintptr_t>(VertexCode.data()), std::as_bytes(std::span(VertexCode))}}};
|
||||
ShaderRecompiler::RecompileRequest vertex{
|
||||
{ShaderStage::Vertex, reinterpret_cast<std::uintptr_t>(VertexCode.data()), VertexCode, 0, {}},
|
||||
{waveSize, 0, vertexUserData, std::nullopt, std::nullopt, ShaderRecompiler::ShaderVertexStageInfo{}, vertexMemory},
|
||||
target,
|
||||
{0, 0, 0, 64}
|
||||
};
|
||||
vertex.useCache = false;
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
state.colors = {color};
|
||||
state.color = color;
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {ScissorWidth, Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
state.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
|
||||
VkPipelineColorBlendAttachmentState blend{};
|
||||
blend.colorWriteMask = 0xf;
|
||||
state.blends = {blend};
|
||||
state.blend = blend;
|
||||
state.blendConstants = {};
|
||||
|
||||
const auto codeAddress = reinterpret_cast<std::uintptr_t>(pixelCode.data());
|
||||
const AgcDriver::Registers context{{0x1b6, 0x8000}, {0x1b3, 0}, {0x1b4, 0}, {0x203, 0}, {0x1c5, exportFormat}};
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{codeAddress, std::as_bytes(pixelCode)}}};
|
||||
ShaderRecompiler::RecompileRequest fragment{
|
||||
{ShaderStage::Fragment, codeAddress, pixelCode, 0, {}},
|
||||
{waveSize, 0, {}, std::nullopt, AgcDriver::Graphics::DecodePixelStageInfo(context, {}), std::nullopt, pixelMemory},
|
||||
target,
|
||||
PixelPushLayout(vertexPush, target)
|
||||
};
|
||||
const auto handle = ShaderRecompiler::PrepareShader(fragment);
|
||||
AgcDriver::DriverDetail::ShaderSnapshot snapshot{codeAddress, 0, 1, {pixelCode.begin(), pixelCode.end()}, {}};
|
||||
snapshot.prepared->entries.push_back({0, handle});
|
||||
fragment.shader.code = snapshot.code;
|
||||
fragment.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(context, AgcDriver::Graphics::ExportMappings(state));
|
||||
const ShaderRecompiler::SrtRuntime runtime{};
|
||||
const auto materialize = [&](const ShaderRecompiler::RecompileRequest& request) {
|
||||
const auto invocation = AgcDriver::DriverDetail::InvocationFor(snapshot, 0, request);
|
||||
return invocation.Materialize(*invocation.Capture(runtime));
|
||||
};
|
||||
const auto pixelResult = materialize(fragment);
|
||||
Require(pixelResult->variantId == ShaderRecompiler::GetPreparedArtifact(*handle).variantId, "the draw did not use the registered pixel shader");
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, pixelResult.get(), PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
const AgcDriver::Pm4::DrawParameters draw{0, static_cast<std::uint32_t>(Triangle.size()), 0, 1, 0, false};
|
||||
device.Draw(state, draw, shaders);
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> ReadBack(AgcDriver::VulkanDevice& device, const Block& block) {
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(block.Address(), block.bytes, nullptr, "test read-back");
|
||||
device.WaitIdle();
|
||||
std::vector<std::uint16_t> stored(block.bytes / 2u);
|
||||
AgcDriver::GuestMemory::Read(block.Address(), std::as_writable_bytes(std::span(stored)), 1);
|
||||
return stored;
|
||||
}
|
||||
|
||||
std::vector<std::uint16_t> Fill(Block& block) {
|
||||
std::vector<std::uint16_t> halves(block.bytes / 2u);
|
||||
for (std::size_t index = 0; index < halves.size(); ++index) halves[index] = static_cast<std::uint16_t>((index * 0x9e3779b9u) >> 16u);
|
||||
std::memcpy(block.data, halves.data(), block.bytes);
|
||||
return halves;
|
||||
}
|
||||
|
||||
void ExpectSurface(AgcDriver::VulkanDevice& device, const Block& block, const ColorTargetLayout& layout, const std::vector<std::uint16_t>& before, std::uint16_t texel, const std::string& what) {
|
||||
const auto stored = ReadBack(device, block);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto index = layout.Offset(x, y) / 2u;
|
||||
const bool inside = x < ScissorWidth;
|
||||
const auto expected = inside ? texel : before[index];
|
||||
if (stored[index] == expected) continue;
|
||||
char detail[160];
|
||||
std::snprintf(detail, sizeof(detail), ": pixel (%u, %u) %s the scissor holds 0x%04x, expected 0x%04x", x, y, inside ? "inside" : "outside", stored[index], expected);
|
||||
throw std::runtime_error(what + detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TargetTests(AgcDriver::VulkanDevice& device, Block& block, std::uint32_t attrib3, ColorTileMode mode, bool reversed, const char* kind) {
|
||||
const ColorTargetLayout layout(Width, Height, mode, 2);
|
||||
Require(layout.Bytes() == block.bytes, "the test surface size disagrees with its layout");
|
||||
const auto color = DecodeTarget(block, attrib3, reversed ? Color565ReversedInfo : Color565StandardInfo);
|
||||
auto before = Fill(block);
|
||||
struct Case {
|
||||
Color color;
|
||||
std::uint16_t reversedTexel;
|
||||
std::uint16_t standardTexel;
|
||||
};
|
||||
const std::array<Case, 11> cases{{
|
||||
{{1.0f, 0.0f, 0.0f, 1.0f}, 0xf800, 0x001f},
|
||||
{{0.0f, 1.0f, 0.0f, 0.0f}, 0x07e0, 0x07e0},
|
||||
{{0.0f, 0.0f, 1.0f, 0.0f}, 0x001f, 0xf800},
|
||||
{{1.0f, 0.0f, 1.0f, 0.5f}, 0xf81f, 0xf81f},
|
||||
{{1.0f, 1.0f, 1.0f, 1.0f}, 0xffff, 0xffff},
|
||||
{{0.0f, 0.0f, 0.0f, 1.0f}, 0x0000, 0x0000},
|
||||
{{3.0f / 31.0f, 40.0f / 63.0f, 21.0f / 31.0f, 1.0f}, (3u << 11u) | (40u << 5u) | 21u, 3u | (40u << 5u) | (21u << 11u)},
|
||||
{{21.0f / 31.0f, 3.0f / 63.0f, 10.0f / 31.0f, 0.0f}, (21u << 11u) | (3u << 5u) | 10u, 21u | (3u << 5u) | (10u << 11u)},
|
||||
{{1.0f / 31.0f, 62.0f / 63.0f, 30.0f / 31.0f, 0.75f}, (1u << 11u) | (62u << 5u) | 30u, 1u | (62u << 5u) | (30u << 11u)},
|
||||
{{30.0f / 31.0f, 1.0f / 63.0f, 1.0f / 31.0f, 0.25f}, (30u << 11u) | (1u << 5u) | 1u, 30u | (1u << 5u) | (1u << 11u)},
|
||||
{{2.0f, -1.0f, 10.0f / 31.0f, 0.25f}, (31u << 11u) | 10u, 31u | (10u << 11u)},
|
||||
}};
|
||||
for (const auto& test : cases) {
|
||||
const auto& value = test.color;
|
||||
const auto texel = reversed ? test.reversedTexel : test.standardTexel;
|
||||
Require(Texel(value, reversed) == texel, "the test's expected texel disagrees with the 5_6_5 field layout");
|
||||
Draw(device, color, Float32Export, Program(value));
|
||||
char what[192];
|
||||
std::snprintf(what, sizeof(what), "a 32_ABGR export of (%.9g, %.9g, %.9g, %.9g) into a %s 5_6_5 unorm target with the %s component order", value[0], value[1], value[2], value[3], kind, reversed ? "reversed" : "standard");
|
||||
ExpectSurface(device, block, layout, before, texel, what);
|
||||
before = ReadBack(device, block);
|
||||
programCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
Block tiled(ColorTargetLayout(Width, Height, ColorTileMode::RenderTarget, 2).Bytes());
|
||||
Block linear(ColorTargetLayout(Width, Height, ColorTileMode::Linear, 2).Bytes());
|
||||
for (const bool reversed : {true, false}) {
|
||||
TargetTests(*device, tiled, TiledAttrib3, ColorTileMode::RenderTarget, reversed, "tiled");
|
||||
TargetTests(*device, linear, LinearAttrib3, ColorTileMode::Linear, reversed, "linear");
|
||||
}
|
||||
std::puts("5_6_5 unorm color target tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Side = 128;
|
||||
constexpr std::uint32_t Format8888Unorm = 56;
|
||||
constexpr std::uint32_t TileRenderTarget64KB = 0x1b;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::size_t SurfaceBytes = 65536;
|
||||
constexpr std::size_t KeyBytes = SurfaceBytes / 256;
|
||||
constexpr std::size_t KeysOffset = SurfaceBytes;
|
||||
constexpr std::size_t SourceKeysOffset = SurfaceBytes + 4096;
|
||||
constexpr std::size_t BlockBytes = 2 * 65536;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> CopyKeysCode{
|
||||
0x34020083, 0xe0341000, 0x80000201, 0xbf8c3f70, 0xe0741000, 0x80010201, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 11> SampleSecondTexelCode{
|
||||
0x34060084, 0x7e2802ff, 0x3c400000, 0x7e2a02ff, 0x3b800000, 0xf09c0f08, 0x00610a14, 0xbf8c3f70,
|
||||
0xe0781000, 0x80000a03, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::uint32_t, 4> PointSampler{0x00000092u, 0x00fff000u, 0u, 0u};
|
||||
|
||||
std::uint64_t AddressOf(const void* data) {
|
||||
return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
}
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(65536, BlockBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "DCC key pre-sync: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint8_t* Data() { return block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const std::uint8_t* texels, const std::uint8_t* keys) {
|
||||
const auto address = AddressOf(texels);
|
||||
const auto meta = AddressOf(keys) >> 8u;
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format8888Unorm << 20u) | (((Side - 1u) & 3u) << 30u),
|
||||
((Side - 1u) >> 2u) | ((Side - 1u) << 14u),
|
||||
0xfacu | (TileRenderTarget64KB << 20u) | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
(1u << 20u) | (1u << 21u) | (static_cast<std::uint32_t>(meta & 0xffu) << 24u),
|
||||
static_cast<std::uint32_t>(meta >> 8u),
|
||||
};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = AddressOf(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData) {
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
struct Scenario {
|
||||
std::uint8_t* texels;
|
||||
std::uint8_t* keys;
|
||||
std::uint8_t* source;
|
||||
std::uint32_t* output;
|
||||
std::vector<std::uint32_t> copyData;
|
||||
std::vector<std::uint32_t> sampleData;
|
||||
};
|
||||
|
||||
Scenario Prepare(std::uint8_t* block, std::uint8_t* outputBlock) {
|
||||
Scenario scenario{block, block + KeysOffset, block + SourceKeysOffset, reinterpret_cast<std::uint32_t*>(outputBlock), std::vector<std::uint32_t>(16, 0u), std::vector<std::uint32_t>(24, 0u)};
|
||||
const auto source = BufferDescriptor(scenario.source, KeyBytes);
|
||||
const auto destination = BufferDescriptor(scenario.keys, KeyBytes);
|
||||
std::copy(source.begin(), source.end(), scenario.copyData.begin());
|
||||
std::copy(destination.begin(), destination.end(), scenario.copyData.begin() + 4);
|
||||
const auto results = BufferDescriptor(scenario.output, Threads * 16u);
|
||||
const auto texture = TextureDescriptor(scenario.texels, scenario.keys);
|
||||
std::copy(results.begin(), results.end(), scenario.sampleData.begin());
|
||||
std::copy(texture.begin(), texture.end(), scenario.sampleData.begin() + 4);
|
||||
std::copy(PointSampler.begin(), PointSampler.end(), scenario.sampleData.begin() + 12);
|
||||
return scenario;
|
||||
}
|
||||
|
||||
void Reset(const Scenario& scenario) {
|
||||
std::memset(scenario.texels, 0x55, SurfaceBytes);
|
||||
std::memset(scenario.keys, 0xff, KeyBytes);
|
||||
std::memset(scenario.source, 0x00, KeyBytes);
|
||||
std::fill(scenario.output, scenario.output + Threads * 4u, 0xdeadbeefu);
|
||||
AgcDriver::GuestMemory::BumpCollectEpoch();
|
||||
}
|
||||
|
||||
void RecordKeyCopy(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
const auto copy = Compile(device, CopyKeysCode, scenario.copyData);
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.Dispatch(copy, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(CopyKeysCode.data()));
|
||||
Require(AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "a recorded kernel that copies data over the keys is not seen as a pending key writer");
|
||||
}
|
||||
|
||||
void RequireSampledClear(const Scenario& scenario, const char* what) {
|
||||
for (std::uint32_t index = 0; index < Threads * 4u; ++index) {
|
||||
char message[256];
|
||||
std::snprintf(message, sizeof(message), "%s: component %u of thread %u is 0x%08x, expected 0 (keys stored 0000 by the kernel)", what, index % 4u, index / 4u, scenario.output[index]);
|
||||
Require(scenario.output[index] == 0u, message);
|
||||
}
|
||||
}
|
||||
|
||||
void RunLockedSync(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
Reset(scenario);
|
||||
RecordKeyCopy(device, scenario);
|
||||
const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData);
|
||||
const auto waits = AgcDriver::Graphics::KeyWaitCounts();
|
||||
{
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
const auto after = AgcDriver::Graphics::KeyWaitCounts();
|
||||
Require(after.synced > waits.synced, "a dispatch built under the GPU mutex did not wait for the kernel that copies data over the keys");
|
||||
RequireSampledClear(scenario, "a dispatch built under the GPU mutex");
|
||||
}
|
||||
|
||||
void RunPresync(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
Reset(scenario);
|
||||
RecordKeyCopy(device, scenario);
|
||||
const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData);
|
||||
const auto waits = AgcDriver::Graphics::KeyWaitCounts();
|
||||
const auto presyncs = AgcDriver::VulkanDevice::PresyncCounts();
|
||||
auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()));
|
||||
const auto stageA = AgcDriver::VulkanDevice::PresyncCounts();
|
||||
Require(stageA.keyChecks > presyncs.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync");
|
||||
Require(stageA.keyWaits == presyncs.keyWaits + 1 && stageA.presyncs == presyncs.presyncs + 1, "stage A did not wait for the kernel that copies data over the keys before the GPU mutex");
|
||||
{
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()), std::move(prepared));
|
||||
device.WaitIdle();
|
||||
}
|
||||
const auto after = AgcDriver::Graphics::KeyWaitCounts();
|
||||
Require(after.synced == waits.synced, "the dispatch waited for the key writer under the GPU mutex after the pre-sync waited for it");
|
||||
RequireSampledClear(scenario, "a dispatch whose keys were pre-synced");
|
||||
}
|
||||
|
||||
bool CanFillKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
const std::array<std::uint32_t, 4> pattern{0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffffffffu};
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
const bool filled = device.FillBuffer(AddressOf(scenario.keys), KeyBytes, pattern);
|
||||
device.WaitIdle();
|
||||
return filled;
|
||||
}
|
||||
|
||||
void RunKnownKeys(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
Reset(scenario);
|
||||
const std::array<std::uint32_t, 4> pattern{0u, 0u, 0u, 0u};
|
||||
{
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
Require(device.FillBuffer(AddressOf(scenario.keys), KeyBytes, pattern), "the fill of the keys was not recorded");
|
||||
Require(AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "a recorded fill of the keys is not seen as a pending key writer");
|
||||
Require(!AgcDriver::Graphics::DccKeyWriterNeedsSync(AddressOf(scenario.keys), KeyBytes), "the keys of a recorded constant fill need a synchronization");
|
||||
}
|
||||
const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData);
|
||||
const auto waits = AgcDriver::Graphics::KeyWaitCounts();
|
||||
const auto before = AgcDriver::VulkanDevice::PresyncCounts();
|
||||
auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()));
|
||||
const auto after = AgcDriver::VulkanDevice::PresyncCounts();
|
||||
Require(after.keyChecks > before.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync");
|
||||
Require(after.presyncs == before.presyncs, "stage A waited for a fill whose keys the locked lookup derives from the pending stores");
|
||||
{
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()), std::move(prepared));
|
||||
device.WaitIdle();
|
||||
}
|
||||
Require(AgcDriver::Graphics::KeyWaitCounts().synced == waits.synced, "the dispatch synchronized with the GPU although the pending fill gives the keys");
|
||||
RequireSampledClear(scenario, "a dispatch behind a pending fill of the keys");
|
||||
}
|
||||
|
||||
void RunNoWriter(AgcDriver::VulkanDevice& device, const Scenario& scenario) {
|
||||
const auto sample = Compile(device, SampleSecondTexelCode, scenario.sampleData);
|
||||
const auto before = AgcDriver::VulkanDevice::PresyncCounts();
|
||||
Require(!AgcDriver::Graphics::DccKeyWritersPending(AddressOf(scenario.keys), KeyBytes), "the idle device still reports a pending key writer");
|
||||
auto prepared = device.PrepareDispatch(sample, {}, Threads, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()));
|
||||
const auto after = AgcDriver::VulkanDevice::PresyncCounts();
|
||||
Require(after.keyChecks > before.keyChecks, "stage A did not list the DCC keys of the sampled texture for the pre-sync");
|
||||
Require(after.keyWaits == before.keyWaits && after.presyncs == before.presyncs, "stage A waited although no recorded work writes the keys");
|
||||
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.Dispatch(sample, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(SampleSecondTexelCode.data()), std::move(prepared));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!device->CanWaitUnlocked()) {
|
||||
std::puts("skipped, the device has no timeline semaphores: the pre-sync is not tested");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
GuestBlock locked;
|
||||
GuestBlock lockedOutput;
|
||||
GuestBlock presynced;
|
||||
GuestBlock presyncedOutput;
|
||||
GuestBlock known;
|
||||
GuestBlock knownOutput;
|
||||
const auto control = Prepare(locked.Data(), lockedOutput.Data());
|
||||
const auto changed = Prepare(presynced.Data(), presyncedOutput.Data());
|
||||
const auto fills = Prepare(known.Data(), knownOutput.Data());
|
||||
if (!CanFillKeys(*device, control)) {
|
||||
std::puts("skipped, host imports are unavailable: the key writers of the test are copied writers");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
RunLockedSync(*device, control);
|
||||
RunPresync(*device, changed);
|
||||
RunNoWriter(*device, changed);
|
||||
RunKnownKeys(*device, fills);
|
||||
std::puts("DCC key pre-sync tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ struct DrawOptions {
|
||||
VkCompareOp depthCompare = VK_COMPARE_OP_LESS;
|
||||
bool stencilTest = false;
|
||||
VkStencilOpState stencil{};
|
||||
float minDepth = 0.0f;
|
||||
float maxDepth = 1.0f;
|
||||
};
|
||||
|
||||
inline std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t stride, std::uint32_t count) {
|
||||
@@ -126,7 +128,7 @@ inline std::uint32_t Draw(AgcDriver::VulkanDevice& device, const Shaders& compil
|
||||
state.stencilBack = options.stencil;
|
||||
state.renderExtent = {Width, Height};
|
||||
state.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST;
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), 0, 1};
|
||||
state.viewport = {0, static_cast<float>(Height), static_cast<float>(Width), -static_cast<float>(Height), options.minDepth, options.maxDepth};
|
||||
state.negativeOneToOne = false;
|
||||
state.scissor = {{0, 0}, {Width, Height}};
|
||||
state.cullMode = VK_CULL_MODE_NONE;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include "DepthFastClearHarness.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
using DepthFastClearHarness::Compile;
|
||||
using DepthFastClearHarness::Covered;
|
||||
using DepthFastClearHarness::Draw;
|
||||
using DepthFastClearHarness::DrawOptions;
|
||||
using DepthFastClearHarness::Height;
|
||||
using DepthFastClearHarness::HtileBytes;
|
||||
using DepthFastClearHarness::Require;
|
||||
using DepthFastClearHarness::Surface;
|
||||
using DepthFastClearHarness::Width;
|
||||
|
||||
constexpr std::size_t DepthPlaneBytes = 128u * 128u * 4u;
|
||||
constexpr std::size_t StencilPlaneBytes = 256u * 256u;
|
||||
|
||||
alignas(4096) std::array<std::byte, DepthPlaneBytes> Depth{};
|
||||
alignas(4096) std::array<std::byte, StencilPlaneBytes> Stencil{};
|
||||
alignas(4096) std::array<std::byte, DepthPlaneBytes> HtileDepth{};
|
||||
alignas(256) std::array<std::uint32_t, HtileBytes / 4u> Htile{};
|
||||
|
||||
VkStencilOpState StencilState(VkCompareOp compare, VkStencilOp pass, std::uint32_t reference) {
|
||||
VkStencilOpState state{};
|
||||
state.failOp = VK_STENCIL_OP_KEEP;
|
||||
state.passOp = pass;
|
||||
state.depthFailOp = VK_STENCIL_OP_KEEP;
|
||||
state.compareOp = compare;
|
||||
state.compareMask = 0xffu;
|
||||
state.writeMask = 0xffu;
|
||||
state.reference = reference;
|
||||
return state;
|
||||
}
|
||||
|
||||
DrawOptions StencilEquals(std::uint32_t reference) {
|
||||
return {.depthTest = false, .stencilTest = true, .stencil = StencilState(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, reference)};
|
||||
}
|
||||
|
||||
DrawOptions StencilWrite(std::uint32_t reference) {
|
||||
return {.depthTest = false, .stencilTest = true, .stencil = StencilState(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, reference)};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
Require(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 4u) == DepthPlaneBytes, "the depth plane size of the test surface changed");
|
||||
Require(AgcDriver::Graphics::DepthSliceBytes({Width, Height}, 1u) == StencilPlaneBytes, "the stencil plane size of the test surface changed");
|
||||
const auto shaders = Compile(*device);
|
||||
const Surface surface{reinterpret_cast<std::uintptr_t>(Depth.data()), reinterpret_cast<std::uintptr_t>(Stencil.data()), 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT};
|
||||
const auto depth = surface.depth;
|
||||
const auto stencil = surface.stencil;
|
||||
|
||||
Require(Draw(*device, shaders, surface, StencilWrite(2u)) == Covered, "the stencil mark draw did not cover the target");
|
||||
Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "the stencil mark was not kept");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes - 4u, 0x01010101u);
|
||||
Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "a fill shorter than the stencil plane changed the stencil");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes, 0x01020304u);
|
||||
Require(Draw(*device, shaders, surface, StencilEquals(2u)) == Covered, "a fill whose bytes differ changed the stencil");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(stencil, StencilPlaneBytes, 0x01010101u);
|
||||
Require(Draw(*device, shaders, surface, StencilEquals(2u)) == 0u, "a fill of the whole stencil plane with 1 left the stencil mark 2");
|
||||
Require(Draw(*device, shaders, surface, StencilEquals(1u)) == Covered, "a fill of the whole stencil plane with 1 did not clear the stencil to 1");
|
||||
Require(Draw(*device, shaders, surface, StencilWrite(2u)) == Covered, "the stencil could not be written after the fill");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(stencil - 4096u, StencilPlaneBytes + 8192u, 0u);
|
||||
Require(Draw(*device, shaders, surface, StencilEquals(0u)) == Covered, "a fill around the whole stencil plane with 0 did not clear the stencil to 0");
|
||||
|
||||
Require(Draw(*device, shaders, surface, {.depthWrite = true}) == Covered, "the depth was not written at 0.5");
|
||||
Require(Draw(*device, shaders, surface, {}) == 0u, "a draw at depth 0.5 passed LESS against the 0.5 written before it");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0x3f800000u);
|
||||
Require(Draw(*device, shaders, surface, {}) == Covered, "a fill of the whole depth plane with 1.0 did not clear the depth to 1.0");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0u);
|
||||
Require(Draw(*device, shaders, surface, {}) == 0u, "a fill of the whole depth plane with 0.0 did not clear the depth to 0.0");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes, 0xff000000u);
|
||||
Require(Draw(*device, shaders, surface, {}) == 0u, "a fill of the depth plane with a value outside 0 to 1 changed the depth");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(depth, DepthPlaneBytes - 4u, 0x3f800000u);
|
||||
Require(Draw(*device, shaders, surface, {}) == 0u, "a fill shorter than the depth plane changed the depth");
|
||||
|
||||
Htile.fill(0xffffffffu);
|
||||
const Surface htiled{reinterpret_cast<std::uintptr_t>(HtileDepth.data()), 0, reinterpret_cast<std::uintptr_t>(Htile.data()), false, VK_FORMAT_D32_SFLOAT};
|
||||
Require(Draw(*device, shaders, htiled, {.depthWrite = true}) == Covered, "the depth of the surface with HTILE was not written at 0.5");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(htiled.depth, DepthPlaneBytes, 0u);
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(htiled.htile, HtileBytes, 0u);
|
||||
Require(Draw(*device, shaders, htiled, {.depthWrite = true}) == Covered, "an HTILE fill after a depth plane fill of 0.0 did not clear the depth to DB_DEPTH_CLEAR (1.0)");
|
||||
AgcDriver::Graphics::NoteDepthMetadataFill(htiled.depth, DepthPlaneBytes, 0u);
|
||||
AgcDriver::Graphics::NoteHtileDepthClear(htiled.htile, HtileBytes);
|
||||
Require(Draw(*device, shaders, htiled, {}) == Covered, "an HTILE clear kernel after a depth plane fill of 0.0 did not clear the depth to DB_DEPTH_CLEAR (1.0)");
|
||||
std::puts("depth plane fill tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,7 @@ public:
|
||||
if (hasExtension(VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) context.memoryProperties2 = function<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2");
|
||||
context.Function<PFN_vkGetDeviceQueue>("vkGetDeviceQueue")(context.device, family, 0, &context.queue);
|
||||
VkCommandPoolCreateInfo pool{VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO};
|
||||
pool.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
|
||||
pool.queueFamilyIndex = family;
|
||||
Check(context.Function<PFN_vkCreateCommandPool>("vkCreateCommandPool")(context.device, &pool, nullptr, &context.pool), "vkCreateCommandPool");
|
||||
} catch (...) {
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Words = 64;
|
||||
constexpr std::uint32_t Bytes = Words * 4u;
|
||||
constexpr std::uint32_t Constant = 0x00010000u;
|
||||
constexpr std::uint32_t Sentinel = 0xdeadbeefu;
|
||||
constexpr std::size_t Piece = 65536;
|
||||
constexpr std::size_t BlockBytes = 5 * Piece;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 8> Table{};
|
||||
|
||||
constexpr std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t destination, std::uint32_t source0, std::uint32_t source1) {
|
||||
return (opcode << 25u) | (destination << 17u) | (source1 << 9u) | source0;
|
||||
}
|
||||
|
||||
constexpr std::uint32_t BufferOperands = (0x80u << 24u) | (1u << 16u) | (2u << 8u) | 1u;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 15> Code{
|
||||
Vop2(0x1au, 1u, 0x82u, 0u),
|
||||
0xf4080100u, 0xfa000000u,
|
||||
0xbf8cc07fu,
|
||||
0xe0301000u, BufferOperands,
|
||||
0xbf8c3f70u,
|
||||
0xf4080100u, 0xfa000010u,
|
||||
0xbf8cc07fu,
|
||||
Vop2(0x25u, 2u, 0xffu, 2u), Constant,
|
||||
0xe0701000u, BufferOperands,
|
||||
0xbf810000u,
|
||||
};
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(BlockBytes, Piece));
|
||||
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, BlockBytes, PAGE_READWRITE, BlockBytes);
|
||||
#else
|
||||
constexpr std::uintptr_t alignment = Piece;
|
||||
void* mapped = mmap(nullptr, BlockBytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (mapped != MAP_FAILED) {
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
|
||||
const auto aligned = (begin + alignment - 1) & ~(alignment - 1);
|
||||
if (aligned != begin) munmap(mapped, aligned - begin);
|
||||
if (aligned + BlockBytes != begin + BlockBytes + alignment) munmap(reinterpret_cast<void*>(aligned + BlockBytes), begin + alignment - aligned);
|
||||
block = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, BlockBytes);
|
||||
}
|
||||
#endif
|
||||
Require(block != nullptr, "dispatch move: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(block, BlockBytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, BlockBytes);
|
||||
#else
|
||||
munmap(block, BlockBytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, BlockBytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint32_t* InputA() const { return reinterpret_cast<std::uint32_t*>(block); }
|
||||
std::uint32_t* InputB() const { return reinterpret_cast<std::uint32_t*>(block + Piece + 12); }
|
||||
std::uint32_t* InputC() const { return reinterpret_cast<std::uint32_t*>(block + 2 * Piece + 32); }
|
||||
std::uint32_t* OutputO() const { return reinterpret_cast<std::uint32_t*>(block + 3 * Piece); }
|
||||
std::uint32_t* OutputP() const { return reinterpret_cast<std::uint32_t*>(block + 4 * Piece); }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
void Fill(std::uint32_t* where, std::uint32_t first) {
|
||||
for (std::uint32_t index = 0; index < Words; ++index) where[index] = first + index;
|
||||
}
|
||||
|
||||
void Reset(std::uint32_t* output) {
|
||||
std::fill(output, output + Words, Sentinel);
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const std::uint32_t* input, std::uint32_t inputBytes, const std::uint32_t* output) {
|
||||
const auto in = BufferDescriptor(input, inputBytes);
|
||||
const auto out = BufferDescriptor(output, Bytes);
|
||||
std::copy(in.begin(), in.end(), Table.begin());
|
||||
std::copy(out.begin(), out.end(), Table.begin() + 4);
|
||||
const auto table = reinterpret_cast<std::uintptr_t>(Table.data());
|
||||
const std::vector<std::uint32_t> userData{static_cast<std::uint32_t>(table), static_cast<std::uint32_t>(table >> 32u)};
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const std::span<const std::uint32_t> tableWords(Table);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 2> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}, {table, std::as_bytes(tableWords)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& compiled, const std::uint32_t* input, std::uint32_t inputBytes, const std::uint32_t* output, bool wait) {
|
||||
auto result = compiled;
|
||||
std::size_t elements = 0;
|
||||
for (auto& binding : result.bindings) {
|
||||
if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue;
|
||||
Require(binding.guestDescriptor.size() == static_cast<std::size_t>(binding.count) * 4u && binding.bufferWritten.size() == binding.count, "dispatch move: the program's buffer bindings have another shape");
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element, ++elements) {
|
||||
const auto words = binding.bufferWritten[element] ? BufferDescriptor(output, Bytes) : BufferDescriptor(input, inputBytes);
|
||||
std::copy(words.begin(), words.end(), binding.guestDescriptor.begin() + static_cast<std::ptrdiff_t>(element) * 4);
|
||||
}
|
||||
}
|
||||
Require(elements == 2, "dispatch move: the program does not bind an input and an output");
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
|
||||
if (wait) device.WaitIdle();
|
||||
}
|
||||
|
||||
void Expect(const std::uint32_t* input, const std::uint32_t* output, std::uint32_t added, const std::string& what) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto expected = input[tid] + added;
|
||||
Require(output[tid] == expected, "dispatch move: " + what + ": lane " + std::to_string(tid) + " is " + Hex(output[tid]) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
|
||||
struct Step {
|
||||
std::uint64_t checked, exact, moved, refused, rebuilt, kept, served;
|
||||
};
|
||||
|
||||
Step Since(const AgcDriver::VulkanDevice::DispatchMoveCount& before) {
|
||||
const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
return {now.checked - before.checked, now.exact - before.exact, now.moved - before.moved, now.refused - before.refused, now.rebuilt - before.rebuilt, now.kept - before.kept, now.served - before.served};
|
||||
}
|
||||
|
||||
void Print(const char* what, const Step& step) {
|
||||
std::printf("dispatch move: %s: checked %llu, exact %llu, moved %llu, refused %llu\n", what, static_cast<unsigned long long>(step.checked), static_cast<unsigned long long>(step.exact), static_cast<unsigned long long>(step.moved), static_cast<unsigned long long>(step.refused));
|
||||
}
|
||||
|
||||
bool RunTests(AgcDriver::VulkanDevice& device, const GuestBlock& guest) {
|
||||
Fill(guest.InputA(), 0xa000u);
|
||||
Fill(guest.InputB(), 0xb000u);
|
||||
Fill(guest.InputC(), 0xc000u);
|
||||
|
||||
const auto compiled = Compile(device, guest.InputA(), Bytes, guest.OutputO());
|
||||
Reset(guest.OutputO());
|
||||
Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true);
|
||||
Expect(guest.InputA(), guest.OutputO(), Constant, "the first dispatch");
|
||||
const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
|
||||
Reset(guest.OutputO());
|
||||
Run(device, compiled, guest.InputB(), Bytes, guest.OutputO(), true);
|
||||
Expect(guest.InputB(), guest.OutputO(), Constant, "an input moved to another block");
|
||||
Reset(guest.OutputO());
|
||||
Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true);
|
||||
Expect(guest.InputA(), guest.OutputO(), Constant, "the input back where the template was built");
|
||||
Reset(guest.OutputO());
|
||||
Run(device, compiled, guest.InputC(), Bytes, guest.OutputO(), true);
|
||||
Expect(guest.InputC(), guest.OutputO(), Constant, "an input moved to a third block");
|
||||
const auto repeats = Since(start);
|
||||
Print("an exact repeat between two moved ones", repeats);
|
||||
const bool templates = repeats.checked != 0;
|
||||
if (templates) {
|
||||
Require(repeats.exact == 1 && repeats.moved == 2 && repeats.refused == 0, "dispatch move: the repeats were not served as one exact hit and two moved ones");
|
||||
} else {
|
||||
std::puts("dispatch move: the device kept no template (no host imports); the moved-buffer path is not exercised");
|
||||
}
|
||||
|
||||
Reset(guest.OutputO());
|
||||
Run(device, compiled, guest.InputB(), 192, guest.OutputO(), true);
|
||||
Expect(guest.InputB(), guest.OutputO(), Constant, "an input moved and shortened");
|
||||
|
||||
std::array<std::uint32_t, Words> changed{};
|
||||
for (std::uint32_t index = 0; index < Words; ++index) changed[index] = 0xd000u + index * 3u;
|
||||
AgcDriver::GuestMemory::Write(reinterpret_cast<std::uintptr_t>(guest.InputB()), std::as_bytes(std::span(changed)));
|
||||
Reset(guest.OutputO());
|
||||
Run(device, compiled, guest.InputB(), Bytes, guest.OutputO(), true);
|
||||
Expect(changed.data(), guest.OutputO(), Constant, "an input moved to a range the driver stored over");
|
||||
|
||||
const auto refusals = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
for (std::uint32_t index = 0; index < Words; ++index) guest.OutputO()[index] = 0x7000u + index;
|
||||
Run(device, compiled, guest.OutputO() + Threads, Bytes / 2, guest.OutputO(), true);
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto expected = 0x7000u + Threads + tid + Constant;
|
||||
Require(guest.OutputO()[tid] == expected, "dispatch move: an input moved into the unwritten half of the dispatch's own output: lane " + std::to_string(tid) + " is " + Hex(guest.OutputO()[tid]) + ", expected " + Hex(expected));
|
||||
}
|
||||
|
||||
Reset(guest.OutputO());
|
||||
Reset(guest.OutputP());
|
||||
Run(device, compiled, guest.InputA(), Bytes, guest.OutputP(), false);
|
||||
Run(device, compiled, guest.OutputP(), Bytes, guest.OutputO(), true);
|
||||
Expect(guest.InputA(), guest.OutputO(), 2u * Constant, "an input moved onto a range a recorded dispatch still writes");
|
||||
const auto refused = Since(refusals);
|
||||
Print("two inputs moved onto written ranges", refused);
|
||||
if (templates) {
|
||||
Require(refused.refused == 2, "dispatch move: the inputs moved onto written ranges were not both refused");
|
||||
Require(refused.rebuilt == 2 && refused.kept == 2 && refused.served == 0, "dispatch move: the two refusals were not two rebuilds inserted beside the template");
|
||||
}
|
||||
|
||||
const auto afterRefusals = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
Reset(guest.OutputO());
|
||||
Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true);
|
||||
Expect(guest.InputA(), guest.OutputO(), Constant, "the input moved back after the refusals");
|
||||
const auto back = Since(afterRefusals);
|
||||
Print("the input back after the refusals", back);
|
||||
if (templates) Require(back.checked == 1 && back.exact == 1 && back.moved == 0 && back.refused == 0 && back.rebuilt == 0, "dispatch move: the input back at the template's address was not an exact hit after the refusals");
|
||||
|
||||
for (int round = 0; round < 2; ++round) {
|
||||
const auto alternating = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
Reset(guest.OutputO());
|
||||
Reset(guest.OutputP());
|
||||
Run(device, compiled, guest.InputA(), Bytes, guest.OutputP(), false);
|
||||
Run(device, compiled, guest.OutputP(), Bytes, guest.OutputO(), true);
|
||||
Expect(guest.InputA(), guest.OutputO(), 2u * Constant, "an input refused again while A alternates");
|
||||
Reset(guest.OutputO());
|
||||
Run(device, compiled, guest.InputA(), Bytes, guest.OutputO(), true);
|
||||
Expect(guest.InputA(), guest.OutputO(), Constant, "the input at A after the refused one");
|
||||
const auto step = Since(alternating);
|
||||
Print("A alternating with a refused input", step);
|
||||
if (templates) Require(step.refused == 1 && step.served == 1 && step.rebuilt == 0 && step.exact == 2 && step.moved == 0, "dispatch move: a refused input rebuilt, or evicted the template of A, while A alternated with it");
|
||||
}
|
||||
const auto total = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
std::printf("dispatch move: in total: checked %llu, exact %llu, moved %llu, refused %llu, dropped by stage A %llu\n", static_cast<unsigned long long>(total.checked), static_cast<unsigned long long>(total.exact), static_cast<unsigned long long>(total.moved), static_cast<unsigned long long>(total.refused), static_cast<unsigned long long>(total.filtered));
|
||||
return templates;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
GuestBlock guest;
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!RunTests(*device, guest)) return VulkanTestSkipped;
|
||||
std::puts("dispatch move tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
#include "DepthFastClearHarness.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <sys/mman.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
|
||||
#ifndef _WIN32
|
||||
namespace AgcDriver {
|
||||
|
||||
struct VulkanDeviceTestAccess {
|
||||
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
|
||||
};
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using DepthFastClearHarness::Covered;
|
||||
using DepthFastClearHarness::Draw;
|
||||
using DepthFastClearHarness::Height;
|
||||
using DepthFastClearHarness::Shaders;
|
||||
using DepthFastClearHarness::Surface;
|
||||
using DepthFastClearHarness::Width;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Bytes = Threads * 4u;
|
||||
constexpr std::uint32_t Format32Float = 22;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint32_t Sentinel = 0xdeadbeefu;
|
||||
constexpr std::size_t Piece = 65536;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> RowCode{
|
||||
0x7e3c0300u,
|
||||
0x7e3e0280u,
|
||||
0x34060082u,
|
||||
0xe0301000u, 0x80040b03u,
|
||||
0xbf8c3f70u,
|
||||
0x4a181681u,
|
||||
0xe0701000u, 0x80000c03u,
|
||||
0xf0201108u, 0x00010b1eu,
|
||||
0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> ProbeCode{
|
||||
0x7e3c0300u,
|
||||
0x7e3e020cu,
|
||||
0xf0001108u, 0x00010a1eu,
|
||||
0x34060082u,
|
||||
0xbf8c3f70u,
|
||||
0xe0701000u, 0x80000a03u,
|
||||
0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock(std::size_t pieces, bool inWatch) : bytes(pieces * Piece), watched(inWatch) {
|
||||
if (inWatch) {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Piece));
|
||||
if (block != nullptr) GuestArena::GuestArenaCommit_nid_postfix(block, bytes, PAGE_READWRITE, bytes);
|
||||
#else
|
||||
constexpr std::uintptr_t alignment = Piece;
|
||||
void* mapped = mmap(nullptr, bytes + alignment, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||
if (mapped != MAP_FAILED) {
|
||||
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
|
||||
const auto aligned = (begin + alignment - 1) & ~(alignment - 1);
|
||||
if (aligned != begin) munmap(mapped, aligned - begin);
|
||||
if (aligned + bytes != begin + bytes + alignment) munmap(reinterpret_cast<void*>(aligned + bytes), begin + alignment - aligned);
|
||||
block = reinterpret_cast<std::uint8_t*>(aligned);
|
||||
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(Piece, bytes));
|
||||
#endif
|
||||
if (block != nullptr) std::memset(block, 0, bytes);
|
||||
}
|
||||
Require(block != nullptr, "dispatch move depth: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, bytes, true, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
if (watched) {
|
||||
#ifdef _WIN32
|
||||
GuestArena::GuestArenaReset_nid_postfix(block, bytes);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, bytes);
|
||||
#else
|
||||
munmap(block, bytes);
|
||||
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(block, bytes);
|
||||
#endif
|
||||
} else {
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint8_t* PieceAt(std::size_t index) const { return block + index * Piece; }
|
||||
|
||||
private:
|
||||
std::size_t bytes = 0;
|
||||
bool watched = false;
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
struct Buffers {
|
||||
explicit Buffers(const GuestBlock& block) : block(block) {}
|
||||
std::uint32_t* InputA() const { return reinterpret_cast<std::uint32_t*>(block.PieceAt(4)); }
|
||||
std::uint32_t* InputB() const { return reinterpret_cast<std::uint32_t*>(block.PieceAt(5) + 12); }
|
||||
std::uint32_t* InputC() const { return reinterpret_cast<std::uint32_t*>(block.PieceAt(6) + 32); }
|
||||
std::uint32_t* Output() const { return reinterpret_cast<std::uint32_t*>(block.PieceAt(8)); }
|
||||
|
||||
private:
|
||||
const GuestBlock& block;
|
||||
};
|
||||
|
||||
struct Env {
|
||||
AgcDriver::VulkanDevice& device;
|
||||
const Shaders& shaders;
|
||||
const Buffers& buffers;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const void* data) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format32Float << 20u) | (((Width - 1u) & 3u) << 30u),
|
||||
((Width - 1u) >> 2u) | ((Height - 1u) << 14u),
|
||||
0xfacu | (Type2D << 28u),
|
||||
0u, 0u, 0u, 0u,
|
||||
};
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData) {
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderRecompiler::ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult RowProgram(AgcDriver::VulkanDevice& device, const void* plane, const std::uint32_t* input, const std::uint32_t* output) {
|
||||
std::vector<std::uint32_t> userData(20, 0u);
|
||||
const auto out = BufferDescriptor(output, Bytes);
|
||||
const auto texture = TextureDescriptor(plane);
|
||||
const auto in = BufferDescriptor(input, Bytes);
|
||||
std::copy(out.begin(), out.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
std::copy(in.begin(), in.end(), userData.begin() + 16);
|
||||
return Compile(device, RowCode, userData);
|
||||
}
|
||||
|
||||
void RunRow(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& compiled, const std::uint32_t* input, const std::uint32_t* output) {
|
||||
auto result = compiled;
|
||||
std::size_t elements = 0;
|
||||
for (auto& binding : result.bindings) {
|
||||
if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue;
|
||||
Require(binding.guestDescriptor.size() == static_cast<std::size_t>(binding.count) * 4u, "dispatch move depth: the program's buffer bindings have another shape");
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element, ++elements) {
|
||||
auto* current = binding.guestDescriptor.data() + static_cast<std::size_t>(element) * 4u;
|
||||
const auto built = static_cast<std::uint64_t>(current[0]) | (static_cast<std::uint64_t>(current[1] & 0xffffu) << 32u);
|
||||
const auto words = built == static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output)) ? BufferDescriptor(output, Bytes) : BufferDescriptor(input, Bytes);
|
||||
std::copy(words.begin(), words.end(), current);
|
||||
}
|
||||
}
|
||||
Require(elements == 2, "dispatch move depth: the program does not bind an input and an output");
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(RowCode.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, Threads> ReadRow(const Env& env, const void* plane, std::uint32_t y) {
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
auto* output = env.buffers.Output();
|
||||
const auto out = BufferDescriptor(output, Bytes);
|
||||
const auto texture = TextureDescriptor(plane);
|
||||
std::copy(out.begin(), out.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
userData[12] = y;
|
||||
const auto program = Compile(env.device, ProbeCode, userData);
|
||||
std::fill(output, output + Threads, Sentinel);
|
||||
env.device.Dispatch(program, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(ProbeCode.data()));
|
||||
env.device.WaitIdle();
|
||||
std::array<std::uint32_t, Threads> lanes{};
|
||||
std::copy(output, output + Threads, lanes.begin());
|
||||
return lanes;
|
||||
}
|
||||
|
||||
void FillInput(std::uint32_t* where, float first) {
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) where[lane] = std::bit_cast<std::uint32_t>(first + static_cast<float>(lane) / 1024.0f);
|
||||
}
|
||||
|
||||
void FillPlane(std::uint8_t* plane, float value) {
|
||||
auto* words = reinterpret_cast<std::uint32_t*>(plane);
|
||||
std::fill(words, words + Piece / sizeof(std::uint32_t), std::bit_cast<std::uint32_t>(value));
|
||||
}
|
||||
|
||||
void DrawDepth(const Env& env, const void* plane, float depth) {
|
||||
const Surface surface{reinterpret_cast<std::uintptr_t>(plane), 0, 0, false, VK_FORMAT_D32_SFLOAT};
|
||||
const float low = depth <= 0.5f ? 0.0f : 2.0f * depth - 1.0f;
|
||||
const float high = depth <= 0.5f ? 2.0f * depth : 1.0f;
|
||||
Require(Draw(env.device, env.shaders, surface, {.depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .minDepth = low, .maxDepth = high}) == Covered, "dispatch move depth: the depth producer did not cover the surface");
|
||||
}
|
||||
|
||||
struct PlaneModel {
|
||||
std::array<std::uint32_t, Threads> row0{};
|
||||
std::uint32_t rest = 0;
|
||||
|
||||
void Set(float value) {
|
||||
rest = std::bit_cast<std::uint32_t>(value);
|
||||
row0.fill(rest);
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, Threads> Dispatch(const std::uint32_t* input) {
|
||||
std::array<std::uint32_t, Threads> sums{};
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
sums[lane] = input[lane] + 1u;
|
||||
row0[lane] = input[lane];
|
||||
}
|
||||
return sums;
|
||||
}
|
||||
};
|
||||
|
||||
void Totals(const std::string& what);
|
||||
|
||||
void Step(const Env& env, const ShaderRecompiler::RecompileResult& program, PlaneModel& model, const std::uint32_t* input, const std::string& what) {
|
||||
const auto expected = model.Dispatch(input);
|
||||
auto* output = env.buffers.Output();
|
||||
std::fill(output, output + Threads, Sentinel);
|
||||
RunRow(env.device, program, input, output);
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
Require(output[lane] == expected[lane], "dispatch move depth: " + what + ": lane " + std::to_string(lane) + " is " + Hex(output[lane]) + ", expected " + Hex(expected[lane]));
|
||||
}
|
||||
Totals(what);
|
||||
}
|
||||
|
||||
void Verify(const Env& env, const void* plane, const PlaneModel& model, const std::string& what) {
|
||||
for (const std::uint32_t y : {0u, 1u, Height - 1u}) {
|
||||
const auto lanes = ReadRow(env, plane, y);
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
const auto expected = y == 0 ? model.row0[lane] : model.rest;
|
||||
Require(lanes[lane] == expected, "dispatch move depth: " + what + ": row " + std::to_string(y) + ", texel " + std::to_string(lane) + " of the plane is " + Hex(lanes[lane]) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MoveSteps {
|
||||
std::uint64_t checked, exact, moved, refused;
|
||||
};
|
||||
|
||||
MoveSteps Since(const AgcDriver::VulkanDevice::DispatchMoveCount& before) {
|
||||
const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
return {now.checked - before.checked, now.exact - before.exact, now.moved - before.moved, now.refused - before.refused};
|
||||
}
|
||||
|
||||
void Totals(const std::string& what) {
|
||||
const auto now = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
std::printf("dispatch move depth: %s: in total checked %llu, exact %llu, moved %llu, refused %llu, dropped by stage A %llu\n", what.c_str(), static_cast<unsigned long long>(now.checked), static_cast<unsigned long long>(now.exact), static_cast<unsigned long long>(now.moved), static_cast<unsigned long long>(now.refused), static_cast<unsigned long long>(now.filtered));
|
||||
}
|
||||
|
||||
void DescribeBindings(const ShaderRecompiler::RecompileResult& program) {
|
||||
for (const auto& binding : program.bindings) {
|
||||
std::string written;
|
||||
for (const bool bit : binding.bufferWritten) written += bit ? '1' : '0';
|
||||
std::printf("dispatch move depth: binding role %d kind %d count %u, %zu descriptor words, buffers written [%s], images written %zu\n", static_cast<int>(binding.role), static_cast<int>(binding.kind), binding.count, binding.guestDescriptor.size(), written.c_str(), binding.imageWritten.size());
|
||||
}
|
||||
}
|
||||
|
||||
void Print(const char* what, const MoveSteps& step) {
|
||||
std::printf("dispatch move depth: %s: checked %llu, exact %llu, moved %llu, refused %llu\n", what, static_cast<unsigned long long>(step.checked), static_cast<unsigned long long>(step.exact), static_cast<unsigned long long>(step.moved), static_cast<unsigned long long>(step.refused));
|
||||
}
|
||||
|
||||
void Warm(const Env& env, std::uint8_t* plane) {
|
||||
FillPlane(plane, 0.5f);
|
||||
PlaneModel model;
|
||||
model.Set(0.5f);
|
||||
const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output());
|
||||
Step(env, program, model, env.buffers.InputA(), "warm-up: the first input");
|
||||
Step(env, program, model, env.buffers.InputB(), "warm-up: the second input");
|
||||
Step(env, program, model, env.buffers.InputC(), "warm-up: the third input");
|
||||
}
|
||||
|
||||
bool Control(const Env& env, std::uint8_t* plane) {
|
||||
FillPlane(plane, 0.5f);
|
||||
PlaneModel model;
|
||||
model.Set(0.5f);
|
||||
const auto refusals = AgcDriver::Graphics::ShaderResources::DepthGuardRefusals();
|
||||
const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output());
|
||||
DescribeBindings(program);
|
||||
Step(env, program, model, env.buffers.InputA(), "control: the first dispatch");
|
||||
Totals("control, after the first dispatch");
|
||||
const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
Step(env, program, model, env.buffers.InputB(), "control: an input moved");
|
||||
Totals("control, after an input moved");
|
||||
Step(env, program, model, env.buffers.InputC(), "control: the input moved again");
|
||||
Totals("control, after the input moved again");
|
||||
Step(env, program, model, env.buffers.InputC(), "control: an exact repeat");
|
||||
Totals("control, after an exact repeat");
|
||||
const auto steps = Since(start);
|
||||
Print("control, a plane without a depth surface", steps);
|
||||
Verify(env, plane, model, "control");
|
||||
Require(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() == refusals, "dispatch move depth: the depth guard refused a proof with no depth surface in sight");
|
||||
Require(steps.refused == 0, "dispatch move depth: the control's repeats were refused");
|
||||
if (steps.checked == 0) return false;
|
||||
Require(steps.checked == 3 && steps.moved == 3, "dispatch move depth: the control's repeats were not all served by the template of the first dispatch");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Mirror(const Env& env, std::uint8_t* plane, const char* where) {
|
||||
const std::string at = std::string(where) + ", the mirror: ";
|
||||
FillPlane(plane, 0.5f);
|
||||
PlaneModel model;
|
||||
DrawDepth(env, plane, 0.25f);
|
||||
model.Set(0.25f);
|
||||
const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output());
|
||||
Step(env, program, model, env.buffers.InputA(), at + "the first dispatch");
|
||||
Verify(env, plane, model, at + "after the first dispatch");
|
||||
DrawDepth(env, plane, 0.75f);
|
||||
model.Set(0.75f);
|
||||
const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
Step(env, program, model, env.buffers.InputB(), at + "an input moved after the depth was drawn again");
|
||||
Verify(env, plane, model, at + "after the moved dispatch");
|
||||
Step(env, program, model, env.buffers.InputB(), at + "an exact repeat");
|
||||
DrawDepth(env, plane, 0.25f);
|
||||
model.Set(0.25f);
|
||||
Step(env, program, model, env.buffers.InputC(), at + "the input moved again after another redraw");
|
||||
Verify(env, plane, model, at + "after the last dispatch");
|
||||
Print((at + "repeats and redraws").c_str(), Since(start));
|
||||
}
|
||||
|
||||
void Appears(const Env& env, std::uint8_t* plane, const char* where, bool moved, bool templates) {
|
||||
const std::string at = std::string(where) + (moved ? ", a surface appears, input moved: " : ", a surface appears, exact repeat: ");
|
||||
FillPlane(plane, 0.125f);
|
||||
PlaneModel model;
|
||||
model.Set(0.125f);
|
||||
const auto program = RowProgram(env.device, plane, env.buffers.InputA(), env.buffers.Output());
|
||||
Step(env, program, model, env.buffers.InputA(), at + "the first dispatch, no depth surface yet");
|
||||
DrawDepth(env, plane, 0.25f);
|
||||
model.Set(0.25f);
|
||||
const auto start = AgcDriver::VulkanDevice::DispatchMoveCounts();
|
||||
const auto refusals = AgcDriver::Graphics::ShaderResources::DepthGuardRefusals();
|
||||
Step(env, program, model, moved ? env.buffers.InputB() : env.buffers.InputA(), at + "the dispatch after the surface appeared");
|
||||
std::printf("dispatch move depth: %sthe depth guard refused %llu proofs\n", at.c_str(), static_cast<unsigned long long>(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() - refusals));
|
||||
if (templates) Require(AgcDriver::Graphics::ShaderResources::DepthGuardRefusals() > refusals, "dispatch move depth: " + at + "the template over the plain storage image was proved current although a depth surface covers its memory");
|
||||
Print((at + "the dispatch after the surface appeared").c_str(), Since(start));
|
||||
Totals(at + "after that dispatch");
|
||||
Verify(env, plane, model, at + "after that dispatch");
|
||||
DrawDepth(env, plane, 0.75f);
|
||||
model.Set(0.75f);
|
||||
Step(env, program, model, env.buffers.InputC(), at + "the input moved after the next redraw");
|
||||
Verify(env, plane, model, at + "after the last dispatch");
|
||||
}
|
||||
|
||||
bool RunTests(const Env& env, const GuestBlock& watched, const GuestBlock& unwatched, std::size_t pass) {
|
||||
const auto inWatch = pass * 10;
|
||||
const auto outside = pass * 3;
|
||||
FillInput(env.buffers.InputA(), 0.0625f);
|
||||
FillInput(env.buffers.InputB(), 0.109375f);
|
||||
FillInput(env.buffers.InputC(), 0.15625f);
|
||||
Warm(env, watched.PieceAt(9));
|
||||
const bool templates = Control(env, watched.PieceAt(inWatch));
|
||||
if (!templates) std::puts("dispatch move depth: the device kept no template (no host imports); the moved-buffer path is not exercised");
|
||||
Mirror(env, watched.PieceAt(inWatch + 1), "inside the write watch");
|
||||
Appears(env, watched.PieceAt(inWatch + 2), "inside the write watch", true, templates);
|
||||
Appears(env, watched.PieceAt(inWatch + 3), "inside the write watch", false, templates);
|
||||
Mirror(env, unwatched.PieceAt(outside), "outside the write watch");
|
||||
Appears(env, unwatched.PieceAt(outside + 1), "outside the write watch", true, templates);
|
||||
Appears(env, unwatched.PieceAt(outside + 2), "outside the write watch", false, templates);
|
||||
return templates;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const GuestBlock watched(20, true);
|
||||
const GuestBlock unwatched(6, false);
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
const auto shaders = DepthFastClearHarness::Compile(*device);
|
||||
const Buffers buffers(watched);
|
||||
const Env env{*device, shaders, buffers};
|
||||
bool templates = true;
|
||||
#ifndef _WIN32
|
||||
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device);
|
||||
const auto decided = AgcDriver::Graphics::PrepareImportWatch(context);
|
||||
struct Restore {
|
||||
const AgcDriver::Graphics::Context& context;
|
||||
AgcDriver::Graphics::ImportWatch decided;
|
||||
~Restore() { AgcDriver::Graphics::SetImportWatch(context, decided); }
|
||||
} restore{context, decided};
|
||||
std::size_t pass = 0;
|
||||
for (const auto mode : {AgcDriver::Graphics::ImportWatch::Watch, AgcDriver::Graphics::ImportWatch::Unwatch}) {
|
||||
AgcDriver::Graphics::SetImportWatch(context, mode);
|
||||
std::printf("dispatch move depth: pass %zu, imports %s\n", pass, mode == AgcDriver::Graphics::ImportWatch::Watch ? "under the write watch" : "left unwatched");
|
||||
templates = RunTests(env, watched, unwatched, pass++) && templates;
|
||||
}
|
||||
#else
|
||||
templates = RunTests(env, watched, unwatched, 0);
|
||||
#endif
|
||||
if (!templates) return VulkanTestSkipped;
|
||||
std::puts("dispatch move depth tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <array>
|
||||
@@ -22,7 +23,18 @@ using AgcDriver::DriverDetail::DrawMiss;
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> VertexCode{0x7e000208u, 0x7e020280u, 0x7e040280u, 0x7e0602f2u, 0xf80008cfu, 0x03020100u, 0xbf810000u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> SecondVertexCode = VertexCode;
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> PixelCode{0xf4000100u, 0xfa000000u, 0x7e000204u, 0x7e020280u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 16> TriangleVertexCode{
|
||||
0x7e000d05u, 0x080200f4u, 0x10020300u, 0x100202f4u, 0x060202f3u, 0x060400f3u, 0x10040500u, 0x060404f3u,
|
||||
0x7e060280u, 0x7e0802f2u, 0xf80008cfu, 0x04030201u, 0xbf810000u,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 16> ForwardedPixelCode{
|
||||
0xf4040080u, 0xfa000000u, 0xbf8cc07fu, 0xf4000101u, 0xfa000000u, 0xbf8cc07fu, 0x7e000204u, 0xd5480001u,
|
||||
0x02050800u, 0x7e020d01u, 0xf800180fu, 0x00010100u, 0xbf810000u, 0xbf810000u, 0xbf810000u, 0xbf810000u,
|
||||
};
|
||||
alignas(16) std::array<float, 4> Other{0.5f, 0.0f, 0.0f, 0.0f};
|
||||
alignas(16) std::array<float, 4> ForwardedData{};
|
||||
alignas(32) std::array<std::array<std::uint64_t, 4>, 6> AlignedTables{};
|
||||
alignas(32) std::array<std::uint64_t, 4> OffsetTable{};
|
||||
alignas(256) std::array<std::byte, 1024> ColorMemory{};
|
||||
|
||||
struct Header {
|
||||
@@ -33,6 +45,8 @@ struct Header {
|
||||
alignas(8) Header VertexHeader{};
|
||||
alignas(8) Header PixelHeader{};
|
||||
alignas(8) Header SecondVertexHeader{};
|
||||
alignas(8) Header TriangleVertexHeader{};
|
||||
alignas(8) Header ForwardedPixelHeader{};
|
||||
|
||||
const Shader* Registered(Header& header, const std::uint32_t* code, std::size_t bytes, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
@@ -51,10 +65,10 @@ void Append(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::ini
|
||||
commands.insert(commands.end(), payload);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> DrawFrom(std::initializer_list<const float*> sources, const std::uint32_t* vertexCode = VertexCode.data()) {
|
||||
std::vector<std::uint32_t> DrawFrom(std::initializer_list<const void*> sources, const std::uint32_t* vertexCode = VertexCode.data(), const std::uint32_t* pixelCode = PixelCode.data()) {
|
||||
const auto color = reinterpret_cast<std::uintptr_t>(ColorMemory.data());
|
||||
const auto vertex = reinterpret_cast<std::uintptr_t>(vertexCode);
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(pixelCode);
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x2000},
|
||||
{0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800},
|
||||
@@ -97,7 +111,7 @@ void MovedDataHitsTheCache(bool libraries) {
|
||||
Registered(VertexHeader, VertexCode.data(), sizeof(VertexCode), 2);
|
||||
Registered(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1);
|
||||
const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries";
|
||||
const auto draw = [&](std::initializer_list<const float*> sources) {
|
||||
const auto draw = [&](std::initializer_list<const void*> sources) {
|
||||
auto commands = DrawFrom(sources);
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed");
|
||||
@@ -145,6 +159,48 @@ void UnrelocatedAliasesStayOut(bool libraries) {
|
||||
Require(counters.hits - start.hits == twice.size(), mode + ": the exact key of a repeated draw lost its hits next to draws whose alias never relocates (" + std::to_string(counters.hits - start.hits) + " hits in " + std::to_string(twice.size()) + " draws)");
|
||||
}
|
||||
|
||||
void MovedForwardedWordReachesStageDataHit(bool libraries) {
|
||||
Registered(TriangleVertexHeader, TriangleVertexCode.data(), sizeof(TriangleVertexCode), 2);
|
||||
Registered(ForwardedPixelHeader, ForwardedPixelCode.data(), sizeof(ForwardedPixelCode), 1);
|
||||
const auto mode = std::string(libraries ? "with" : "without") + " graphics pipeline libraries";
|
||||
constexpr std::byte Kept{0x40};
|
||||
const auto draw = [&](const void* table) {
|
||||
ColorMemory.fill(Kept);
|
||||
auto commands = DrawFrom({table}, TriangleVertexCode.data(), ForwardedPixelCode.data());
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
Require(sceAgcDriverSubmitDcb(&packet) == 0, "the draw submission failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(ColorMemory.data()), ColorMemory.size());
|
||||
return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters();
|
||||
};
|
||||
const auto pixels = [&](std::array<std::uint8_t, 4> expected) {
|
||||
std::size_t drawn = 0;
|
||||
for (std::size_t at = 0; at < ColorMemory.size(); at += 4) {
|
||||
const std::array<std::uint8_t, 4> pixel{static_cast<std::uint8_t>(ColorMemory[at]), static_cast<std::uint8_t>(ColorMemory[at + 1]), static_cast<std::uint8_t>(ColorMemory[at + 2]), static_cast<std::uint8_t>(ColorMemory[at + 3])};
|
||||
if (pixel == std::array<std::uint8_t, 4>{0x40, 0x40, 0x40, 0x40}) continue;
|
||||
if (pixel != expected) return "pixel " + std::to_string(at / 4) + " is " + std::to_string(pixel[0]) + "," + std::to_string(pixel[1]) + "," + std::to_string(pixel[2]) + "," + std::to_string(pixel[3]) + ", expected " + std::to_string(expected[0]) + "," + std::to_string(expected[1]) + "," + std::to_string(expected[2]) + "," + std::to_string(expected[3]);
|
||||
++drawn;
|
||||
}
|
||||
return drawn == 0 ? std::string("the draw wrote no pixel") : std::string();
|
||||
};
|
||||
const auto data = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(ForwardedData.data()));
|
||||
for (auto& table : AlignedTables) table[0] = data;
|
||||
OffsetTable[2] = data;
|
||||
ForwardedData = {1.0f, 0.0f, 0.0f, 0.0f};
|
||||
const auto start = AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters();
|
||||
AgcDriver::DriverDetail::DrawEntryCounters counters{};
|
||||
for (const auto& table : AlignedTables) counters = draw(table.data());
|
||||
Require(counters.hits != start.hits, mode + ": draws whose pointer table moved to identical contents never reused the cached draw (" + std::to_string(counters.lookups - start.lookups) + " lookups, " + std::to_string(counters.inserts - start.inserts) + " inserts)");
|
||||
const auto taught = pixels({255, 0, 0, 255});
|
||||
Require(taught.empty(), mode + ": a draw through a pointer table with bit 4 of its address clear: " + taught);
|
||||
ForwardedData[0] = 0.25f;
|
||||
const auto before = counters;
|
||||
counters = draw(&OffsetTable[2]);
|
||||
Require(counters.lookups == before.lookups + 1 && counters.hits == before.hits + 1 && counters.stageDataHits == before.stageDataHits + 1, mode + ": a draw whose pointer table moved and whose data changed in place was not a relocated stage data hit (" + std::to_string(counters.hits - before.hits) + " hits, " + std::to_string(counters.stageDataHits - before.stageDataHits) + " stage data hits)");
|
||||
const auto moved = pixels({64, 255, 255, 64});
|
||||
Require(moved.empty(), mode + ": a relocated stage data hit did not draw with the moved user word: " + moved);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -158,6 +214,7 @@ int main() {
|
||||
if (!libraries && std::getenv("APS5_NO_GPL") == nullptr) return VulkanTestSkipped;
|
||||
MovedDataHitsTheCache(libraries);
|
||||
UnrelocatedAliasesStayOut(libraries);
|
||||
MovedForwardedWordReachesStageDataHit(libraries);
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::cout << "A draw whose data moved reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n";
|
||||
return 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineLibrary.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
@@ -243,6 +244,7 @@ int main() {
|
||||
StageDataHitsRefreshTheirWords(libraries);
|
||||
RetainedPixelCacheTransitions();
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
Require(AgcDriver::Graphics::PipelineLibraryWorkers() == 0, "the driver's shutdown left the pipeline library worker running");
|
||||
std::cout << "A repeated draw reused the cached draw " << (libraries ? "with" : "without") << " graphics pipeline libraries\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -205,13 +205,18 @@ void Run(AgcDriver::VulkanDevice& device, const Mode& mode) {
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
bool Matches(std::uint32_t actual, std::uint32_t expected) {
|
||||
const auto nan = [](std::uint32_t half) { return (half & 0x7fffu) > 0x7c00u; };
|
||||
return actual == expected || (nan(expected) && actual <= 0xffffu && nan(actual));
|
||||
}
|
||||
|
||||
void Check(const Mode& mode) {
|
||||
constexpr const char* Names[Results] = {"v_fma_f16", "v_fmac_f16"};
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const std::uint32_t expected = Expected[tid][mode.column];
|
||||
for (std::uint32_t form = 0; form < Results; ++form) {
|
||||
const std::uint32_t actual = Output[tid * Results + form];
|
||||
Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected));
|
||||
Require(Matches(actual, expected), std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@ constexpr std::uint32_t MaskedThreads = 16;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t CheckedBytes = 0x2000;
|
||||
constexpr std::uint8_t Fill = 0xcd;
|
||||
#ifdef __linux__
|
||||
#if defined(__linux__) && defined(ANYPS5_ENABLE_ASAN)
|
||||
constexpr std::array<std::uintptr_t, 2> GuestWindowAddresses{0x60000000, 0x200000000000};
|
||||
#elif defined(__linux__)
|
||||
constexpr std::array<std::uintptr_t, 2> GuestWindowAddresses{0x80000000000, 0x200000000000};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -166,6 +166,11 @@ void Run(AgcDriver::VulkanDevice& device, const Mode& mode) {
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
bool Matches(std::uint32_t actual, std::uint32_t expected) {
|
||||
const auto nan = [](std::uint32_t single) { return (single & 0x7fffffffu) > 0x7f800000u; };
|
||||
return actual == expected || (nan(expected) && nan(actual));
|
||||
}
|
||||
|
||||
void Check(const Mode& mode) {
|
||||
constexpr const char* Names[Results] = {"v_fma_mix_f32 of three f16 sources", "v_fma_mix_f32 of an f16, an f32 and an f16 source"};
|
||||
const std::uint32_t f32Kept = (mode.value.floatMode >> 4u) & 1u;
|
||||
@@ -174,7 +179,7 @@ void Check(const Mode& mode) {
|
||||
for (std::uint32_t form = 0; form < Results; ++form) {
|
||||
const std::uint32_t actual = Output[tid * Results + form];
|
||||
const std::uint32_t expected = Expected[f32Kept][f16InputsKept][tid][form];
|
||||
Require(actual == expected, std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected));
|
||||
Require(Matches(actual, expected), std::string(Names[form]) + " " + mode.name + " of " + Hex(Rows[tid][0]) + ", " + Hex(Rows[tid][1]) + ", " + Hex(Rows[tid][2]) + " is " + Hex(actual) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,11 @@ using AgcDriver::Graphics::Require;
|
||||
constexpr std::size_t HostStackContextBytes = 16 * 1024;
|
||||
constexpr std::size_t ContextBytes = 64 * 1024;
|
||||
constexpr std::size_t MappingBytes = 4 * 65536;
|
||||
#ifdef ANYPS5_ENABLE_ASAN
|
||||
constexpr std::size_t FiberStackLimit = 16 * 1024;
|
||||
#else
|
||||
constexpr std::size_t FiberStackLimit = 8 * 1024;
|
||||
#endif
|
||||
constexpr std::uint64_t Fill = 0x5aa5c33c5aa5c33cull;
|
||||
|
||||
alignas(16) std::array<std::uint64_t, ContextBytes / sizeof(std::uint64_t)> FiberContext;
|
||||
|
||||
@@ -110,6 +110,20 @@ alignas(256) constexpr std::array<std::uint32_t, 32> TableWave32Code{
|
||||
0x01090401u, 0x7e120202u, 0x34101287u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 29> SelectedTableCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4000400u, 0xfa000060u,
|
||||
0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u,
|
||||
0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u, 0x34101286u,
|
||||
0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 31> SelectedTableCutCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080700u, 0xfa000020u, 0xf4000400u, 0xfa000060u,
|
||||
0xf4200444u, 0x20000000u, 0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e020d01u, 0x060202f0u,
|
||||
0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0x7da400ffu, 0x00001000u, 0xf09c8f08u, 0x01090401u,
|
||||
0x7e120202u, 0x34101286u, 0x4a101100u, 0x34101084u, 0xe0781000u, 0x80070408u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 22> DirectCode{
|
||||
0xf4080700u, 0xfa000020u, 0xf4080800u, 0xfa000030u, 0xf4080900u, 0xfa000040u, 0x360200bfu, 0x7e020d01u,
|
||||
0x060202f0u, 0x100202ffu, 0x3c800000u, 0x7e0402ffu, 0x3e000000u, 0xf09c8f08u, 0x01090401u, 0x7e120202u,
|
||||
@@ -184,12 +198,25 @@ alignas(256) constexpr std::array<std::uint32_t, 15> StoreTableCode{
|
||||
0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0xf0208f08u, 0x00090401u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 16> StoreLaneTableCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u,
|
||||
0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e080301u, 0xf0208f08u, 0x00090401u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 17> StoreTableCutCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0x8f108202u, 0xf4200444u, 0x20000000u, 0x9312b011u,
|
||||
0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7da400ffu, 0x00001000u, 0xf0208f08u, 0x00090401u,
|
||||
0xbf810000u,
|
||||
};
|
||||
|
||||
constexpr std::uint32_t SelectedStoreValue = 0xc0de5eedu;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 18> SelectedStoreCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4000400u, 0xfa000060u, 0xf4200444u, 0x20000000u,
|
||||
0x9312b011u, 0xf42c0802u, 0x24000000u, 0x360200bfu, 0x7e040280u, 0x7e0802ffu, SelectedStoreValue, 0xf0208f08u,
|
||||
0x00090401u, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 30> PointerCode{
|
||||
0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u, 0x8f108202u, 0xf4200444u,
|
||||
0x20000000u, 0x8711ff11u, 0x000000ffu, 0x8f128511u, 0xf40c0900u, 0x24000040u, 0x360200bfu, 0x7e020d01u,
|
||||
@@ -368,7 +395,7 @@ Outcome Execute(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t>
|
||||
outcome.cacheable = AgcDriver::DriverDetail::CacheableResult(result);
|
||||
outcome.imageTableReadRanges = result.imageTableReadRanges;
|
||||
for (const auto& binding : result.bindings) {
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable) outcome.imageTableCount = binding.count;
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::ImageTable || binding.role == ShaderRecompiler::DescriptorRole::ImageStorageTable) outcome.imageTableCount = binding.count;
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers) outcome.samplers = SamplerWords(binding.guestDescriptor);
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::SamplerTable) outcome.samplerTable = SamplerWords(binding.guestDescriptor);
|
||||
}
|
||||
@@ -934,6 +961,43 @@ void RunGuardedTableTests(AgcDriver::VulkanDevice& device) {
|
||||
SetClassRecords();
|
||||
}
|
||||
|
||||
void RunSelectedRecordTests(AgcDriver::VulkanDevice& device) {
|
||||
Fill();
|
||||
SetClassRecords();
|
||||
Keys = {};
|
||||
Srt[24] = 12u;
|
||||
Outcome first;
|
||||
for (const auto record : {2u, 0u, 1u}) {
|
||||
Keys[3] = record;
|
||||
const auto selected = Run(device, SelectedTableCode, 1, Lanes, 64);
|
||||
Require(!Faulted(selected) && selected.imageTableCount == 1u, "image table selected record: record " + std::to_string(record) + " faulted or bound other entries:\n" + selected.log);
|
||||
const auto* words = Table.data() + record * (Stride / 4u);
|
||||
const auto control = RunDirect(device, {words[0], words[1], words[2], words[3]}, {words[4], words[5], words[6], words[7]});
|
||||
Require(Group(selected, 0) == Group(control, 0), "image table selected record: record " + std::to_string(record) + " differs from the direct binding of the same words");
|
||||
if (record == 2u) first = selected;
|
||||
Require(selected.artifactId == first.artifactId && selected.variantId == first.variantId, "image table selected record: another key changed the module");
|
||||
}
|
||||
|
||||
Keys[3] = 1000u;
|
||||
const auto beyond = Run(device, SelectedTableCode, 1, Lanes, 64);
|
||||
Require(!Faulted(beyond) && Zeros(beyond), "image table selected record: a key past the table faulted or did not sample zeros:\n" + beyond.log);
|
||||
|
||||
Keys[3] = 7u;
|
||||
const auto cut = Run(device, SelectedTableCutCode, 1, Lanes, 64);
|
||||
Require(!Faulted(cut), "image table selected record: an invalid record selected with EXEC clear faulted:\n" + cut.log);
|
||||
const auto invalid = Run(device, SelectedTableCode, 1, Lanes, 64);
|
||||
const auto tableAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Table.data())) + 7u * Stride + 16u;
|
||||
char address[32];
|
||||
std::snprintf(address, sizeof(address), "0x%llx", static_cast<unsigned long long>(tableAddress));
|
||||
Require(Faulted(invalid) && invalid.log.find("not an image") != std::string::npos && invalid.log.find(address) != std::string::npos && invalid.log.find("00001234") != std::string::npos && Zeros(invalid), "image table selected record: an invalid record selected with EXEC set did not report its address, words and reason:\n" + invalid.log);
|
||||
|
||||
Keys[3] = 0u;
|
||||
const auto aliased = Run(device, SelectedTableCode, 1, Lanes, 64, true);
|
||||
Require(Faulted(aliased) && aliased.log.find("is written by its own dispatch") != std::string::npos, "image table selected record: a dispatch storing over its selected record did not fault on use:\n" + aliased.log);
|
||||
SetClassRecords();
|
||||
Srt[24] = 0u;
|
||||
}
|
||||
|
||||
template <std::size_t TableWords, std::size_t DirectWords>
|
||||
void RequireModeEntry(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, TableWords>& tableCode, const std::array<std::uint32_t, DirectWords>& directCode, std::uint32_t record, std::span<const std::uint32_t> image, const std::string& what) {
|
||||
Keys = {};
|
||||
@@ -988,15 +1052,61 @@ void RunModeTests(AgcDriver::VulkanDevice& device) {
|
||||
}
|
||||
|
||||
void RunStoreTests(AgcDriver::VulkanDevice& device) {
|
||||
struct Registration {
|
||||
void* data;
|
||||
Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); }
|
||||
~Registration() { GuestAllocations::Mutation().Remove(data); }
|
||||
} registeredUint(UintTexels.data(), sizeof(UintTexels)), registeredFloat(FloatTexels.data(), sizeof(FloatTexels)), registeredSint(SintTexels.data(), sizeof(SintTexels));
|
||||
SetClassRecords();
|
||||
const auto saved = UintTexels;
|
||||
const auto savedUint = UintTexels;
|
||||
const auto savedFloat = FloatTexels;
|
||||
const auto savedSint = SintTexels;
|
||||
const auto untouched = [&] { return UintTexels == savedUint && FloatTexels == savedFloat && SintTexels == savedSint; };
|
||||
Keys = {};
|
||||
Keys[0] = 1;
|
||||
const auto cut = Run(device, StoreTableCutCode, 1, Lanes, 64);
|
||||
AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test");
|
||||
Require(!Faulted(cut), "image table store: a store with EXEC clear faulted:\n" + cut.log);
|
||||
const auto stored = Run(device, StoreTableCode, 1, Lanes, 64);
|
||||
Require(Faulted(stored) && stored.log.find("unsupported operation") != std::string::npos, "image table store: an image_store through a table did not fault as an unsupported operation:\n" + stored.log);
|
||||
Require(UintTexels == saved, "image table store: an unsupported store reached the image");
|
||||
Require(untouched(), "image table store: a store with EXEC clear reached an image");
|
||||
const auto stored = Run(device, StoreLaneTableCode, 1, Lanes, 64);
|
||||
AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test");
|
||||
Require(!Faulted(stored), "image table store: a store through a table faulted:\n" + stored.log);
|
||||
Require(stored.imageTableCount == 2u, "image table store: the storage table does not hold one element per distinct T# (" + std::to_string(stored.imageTableCount) + ")");
|
||||
for (std::uint32_t x = 0; x < Width; ++x) Require(UintTexels[x] == x, "image table store: texel " + std::to_string(x) + " of the selected image is " + Hex(UintTexels[x]) + ", expected " + Hex(x));
|
||||
Require(std::equal(UintTexels.begin() + Width, UintTexels.end(), savedUint.begin() + Width), "image table store: a store to row 0 reached another row");
|
||||
Require(FloatTexels == savedFloat && SintTexels == savedSint, "image table store: a store through key 1 reached another entry's image");
|
||||
UintTexels = savedUint;
|
||||
const auto restored = Run(device, StoreTableCutCode, 1, Lanes, 64);
|
||||
AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test");
|
||||
Require(!Faulted(restored) && untouched(), "image table store: restoring the image failed:\n" + restored.log);
|
||||
Keys[0] = 6;
|
||||
const auto null = Run(device, StoreLaneTableCode, 1, Lanes, 64);
|
||||
AgcDriver::Graphics::StorageTexture::FlushAllPending("image table store test");
|
||||
Require(untouched(), "image table store: a store through a null entry reached an image:\n" + null.log);
|
||||
}
|
||||
|
||||
void RunSelectedStoreTests(AgcDriver::VulkanDevice& device) {
|
||||
alignas(256) static std::array<std::uint32_t, Width * Height> storeTexels{};
|
||||
struct Registration {
|
||||
void* data;
|
||||
Registration(void* data, std::size_t bytes) : data(data) { GuestAllocations::Mutation().Add(data, bytes, true, true, true); }
|
||||
~Registration() { GuestAllocations::Mutation().Remove(data); }
|
||||
} registered(storeTexels.data(), sizeof(storeTexels));
|
||||
for (std::uint32_t texel = 0; texel < storeTexels.size(); ++texel) storeTexels[texel] = 0x5000u + texel;
|
||||
const auto saved = storeTexels;
|
||||
SetClassRecords();
|
||||
SetRecord(9, PointClamp, Image(storeTexels.data(), Format32UInt));
|
||||
Keys = {};
|
||||
Keys[3] = 9u;
|
||||
Srt[24] = 12u;
|
||||
const auto stored = Run(device, SelectedStoreCode, 1, Lanes, 64);
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(storeTexels.data()), sizeof(storeTexels), nullptr, "test");
|
||||
device.WaitIdle();
|
||||
Require(!Faulted(stored) && stored.imageTableCount == 0u, "image table selected store: an image_store through a T# whose record the CPU evaluates faulted or was bound through an image table:\n" + stored.log);
|
||||
for (std::uint32_t texel = 0; texel < storeTexels.size(); ++texel) {
|
||||
const auto expected = texel < Lanes ? SelectedStoreValue : saved[texel];
|
||||
Require(storeTexels[texel] == expected, "image table selected store: texel " + std::to_string(texel) + " holds " + std::to_string(storeTexels[texel]) + " after an image_store through a T# whose record the CPU evaluates, expected " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
|
||||
void RunPointerTests(AgcDriver::VulkanDevice& device) {
|
||||
@@ -1325,8 +1435,10 @@ int main() {
|
||||
RunReadTests(*device);
|
||||
RunLoopKeyTests(*device);
|
||||
RunGuardedTableTests(*device);
|
||||
RunSelectedRecordTests(*device);
|
||||
RunModeTests(*device);
|
||||
RunStoreTests(*device);
|
||||
RunSelectedStoreTests(*device);
|
||||
RunPointerTests(*device);
|
||||
RunPointerRecordTests(*device);
|
||||
RunSplitRecordTests(*device);
|
||||
|
||||
@@ -27,6 +27,7 @@ constexpr std::uint32_t Height = 24;
|
||||
constexpr std::uint32_t Depth = 20;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
constexpr std::uint32_t Type3D = 10;
|
||||
constexpr std::uint32_t TileS4KBX = 0x15;
|
||||
constexpr std::uint32_t TileZ64KBX = 0x18;
|
||||
constexpr std::uint32_t TileS64KBX = 0x19;
|
||||
constexpr std::uint32_t TileD64KBX = 0x1a;
|
||||
@@ -79,6 +80,7 @@ void FillTexels(std::uint32_t tileMode) {
|
||||
Require(geometry.guestBytes <= Texels.size(), "volume does not fit the test allocation");
|
||||
const auto* equation = FindTextureSwizzleEquation(geometry.thick ? 0x100u | XorSwizzleMode(descriptor.tileMode) : XorSwizzleMode(descriptor.tileMode), 4u);
|
||||
Require(equation != nullptr, "volume swizzle equation is missing");
|
||||
const auto blockBytes = ThinBlockLayout(descriptor.tileMode, 4u)[0];
|
||||
const auto& mip = geometry.mips.at(0);
|
||||
const auto block = geometry.thick ? ThickBlockExtent(descriptor.tileMode, 4u) : std::array<std::uint32_t, 3>{ThinBlockLayout(descriptor.tileMode, 4u)[1], ThinBlockLayout(descriptor.tileMode, 4u)[2], 1u};
|
||||
for (std::uint32_t z = 0; z < Depth; ++z) {
|
||||
@@ -86,7 +88,7 @@ void FillTexels(std::uint32_t tileMode) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
auto offset = geometry.GuestLayerOffset(z) + mip.tiledOffset;
|
||||
if (mip.tail) offset += EquationOffset(*equation, x + mip.tailX, y + mip.tailY, z);
|
||||
else offset += (static_cast<std::uint64_t>(y / block[1]) * mip.blocksPerRow + x / block[0]) * 65536u + EquationOffset(*equation, x, y, z);
|
||||
else offset += (static_cast<std::uint64_t>(y / block[1]) * mip.blocksPerRow + x / block[0]) * blockBytes + EquationOffset(*equation, x, y, z);
|
||||
const auto texel = Texel(x, y, z);
|
||||
std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast<std::ptrdiff_t>(offset));
|
||||
}
|
||||
@@ -145,6 +147,7 @@ int main() {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
FillInput();
|
||||
Run(*device, TileS4KBX, "thick SW_4KB_S_X volume");
|
||||
Run(*device, TileS64KBX, "thick SW_64KB_S_X volume");
|
||||
Run(*device, TileD64KBX, "thick SW_64KB_D_X volume");
|
||||
Run(*device, TileZ64KBX, "thin SW_64KB_Z_X volume");
|
||||
|
||||
@@ -160,10 +160,14 @@ void Draw(AgcDriver::VulkanDevice& device, std::uint32_t waveSize) {
|
||||
const auto vertexResult = ShaderRecompiler::Recompile(vertex);
|
||||
const auto vertexPush = static_cast<std::uint32_t>(vertexResult.pushConstants.size());
|
||||
const auto pixelResult = RecompilePixel(PixelCode, waveSize, Fp16Input, target, vertexPush);
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{
|
||||
{ShaderStage::Vertex, &vertexResult, 0},
|
||||
{ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)}
|
||||
}};
|
||||
Require(pixelResult.barycentricEmulation.active != target.fragmentShaderBarycentricEnabled, "16-bit interpolation must emulate barycentrics exactly when the device lacks them");
|
||||
std::vector<AgcDriver::Graphics::CompiledShader> shaders{{ShaderStage::Vertex, &vertexResult, 0}};
|
||||
ShaderRecompiler::RecompileResult geometry;
|
||||
if (pixelResult.barycentricEmulation.active) {
|
||||
geometry = ShaderRecompiler::BuildBarycentricGeometryShader(vertexResult, pixelResult, target, device.GeometryLimits());
|
||||
shaders.push_back({ShaderStage::Geometry, &geometry, 0});
|
||||
}
|
||||
shaders.push_back({ShaderStage::Fragment, &pixelResult, PixelPushOffset(vertexPush, target)});
|
||||
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, waveSize, waveSize, std::nullopt, std::nullopt};
|
||||
@@ -259,6 +263,8 @@ ShaderRecompiler::SpirvTarget OfflineTarget(bool barycentric) {
|
||||
target.spirvVersion = 0x00010300u;
|
||||
target.subgroupSize = 64;
|
||||
target.fragmentShaderBarycentricEnabled = barycentric;
|
||||
static constexpr std::array<std::uint32_t, 1> capabilities{spv::CapabilityFloat64};
|
||||
target.supportedCapabilities = capabilities;
|
||||
return target;
|
||||
}
|
||||
|
||||
@@ -281,7 +287,8 @@ void ExpectRejected(std::uint32_t word0, std::uint32_t word1, const char* expect
|
||||
|
||||
int main() {
|
||||
try {
|
||||
ExpectFailure(PixelCode, Fp16Input, false, "requires fragmentShaderBarycentric", "16-bit interpolation was accepted without barycentrics");
|
||||
Require(RecompilePixel(PixelCode, 64u, Fp16Input, OfflineTarget(false), 0u).barycentricEmulation.active, "16-bit interpolation without barycentrics does not emulate them");
|
||||
Require(!RecompilePixel(PixelCode, 64u, Fp16Input, OfflineTarget(true), 0u).barycentricEmulation.active, "16-bit interpolation emulates barycentrics on a device that has them");
|
||||
ExpectFailure(PixelCode, 0x03000000u, true, "without FP16_INTERP_MODE", "16-bit interpolation of a 32-bit input was accepted");
|
||||
ExpectFailure(PixelCode, 0x03080420u, true, "passes its vertices through unchanged", "16-bit interpolation of a pass-through input was accepted");
|
||||
ExpectRejected(0xd75a001bu, 0x0c6a0200u, "interpolation modifiers", "v_interp_p2_f16 accepted an output modifier");
|
||||
@@ -291,12 +298,18 @@ int main() {
|
||||
ExpectRejected(0xd743001du, 0x003e0140u, "not a vector register", "v_interp_p1lv_f16 accepted a scalar P0");
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (!device->Target().fragmentShaderBarycentricEnabled) {
|
||||
std::puts("skipped, the device has no fragmentShaderBarycentric");
|
||||
if (!device->Target().fragmentShaderBarycentricEnabled && !device->GeometryLimits()) {
|
||||
std::puts("skipped, the device has neither fragmentShaderBarycentric nor geometryShader");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
for (const auto waveSize : {64u, 32u}) {
|
||||
Draw(*device, waveSize);
|
||||
try {
|
||||
Draw(*device, waveSize);
|
||||
} catch (const std::exception& error) {
|
||||
if (std::string(error.what()).find("need the Float64 capability") == std::string::npos) throw;
|
||||
std::puts("skipped, the device has no Float64 capability for the exact interpolation sums");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
Check(waveSize);
|
||||
}
|
||||
std::puts("16-bit interpolation tests passed");
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include "SceTypes.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <new>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
extern "C" void AgcDriverShutdown_nid_postfix();
|
||||
|
||||
struct MemorySizeQueryParams {
|
||||
std::uint32_t version;
|
||||
std::uint32_t workers;
|
||||
std::uint32_t sequences;
|
||||
std::uint32_t invocations;
|
||||
std::uint32_t dispatches;
|
||||
std::uint32_t syncPoints;
|
||||
std::uint32_t waits;
|
||||
std::uint32_t computeSequences;
|
||||
std::uint64_t computeQueueMask;
|
||||
};
|
||||
|
||||
struct ConfigParams {
|
||||
std::uint32_t version;
|
||||
std::uint32_t threadFlags;
|
||||
std::uint32_t stackSize;
|
||||
std::uint32_t priority;
|
||||
const char* threadName;
|
||||
std::uint32_t spinMicroseconds;
|
||||
std::uint32_t computeAffinity;
|
||||
std::uint32_t computePriority;
|
||||
std::uint32_t reserved;
|
||||
};
|
||||
|
||||
using JobEntry = void (APS5_VABI*)(void*, std::uint32_t);
|
||||
|
||||
struct Sequence {
|
||||
APS5_VABI virtual int Dispatch(JobEntry, void*, std::uint32_t, const char*) = 0;
|
||||
APS5_VABI virtual int SyncPoint() = 0;
|
||||
APS5_VABI virtual int Wait() = 0;
|
||||
APS5_VABI virtual void CompleteDestructor() = 0;
|
||||
APS5_VABI virtual void DeletingDestructor() = 0;
|
||||
APS5_VABI virtual int DispatchCompute(const void*, void*, std::int32_t, std::int32_t, std::int32_t, const char*) = 0;
|
||||
APS5_VABI virtual int DispatchComputeIndirect(const void*, void*, const void*, const char*) = 0;
|
||||
};
|
||||
|
||||
struct SequenceFactory {
|
||||
APS5_VABI virtual int Create(Sequence**, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, const char*) = 0;
|
||||
APS5_VABI virtual int Destroy(Sequence*) = 0;
|
||||
APS5_VABI virtual void CompleteDestructor() = 0;
|
||||
APS5_VABI virtual void DeletingDestructor() = 0;
|
||||
APS5_VABI virtual int CreateCompute(Sequence**, std::uint32_t, std::uint32_t, std::uint32_t, std::uint32_t, const char*) = 0;
|
||||
APS5_VABI virtual int DestroyCompute(Sequence*) = 0;
|
||||
};
|
||||
|
||||
extern "C" {
|
||||
void APS5_VABI _ZN3sce3Job10JobManagerC1Ev(void*);
|
||||
std::uint64_t APS5_VABI _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE(const MemorySizeQueryParams*);
|
||||
int APS5_VABI _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE(void*, void*, std::uint64_t, std::uint32_t, std::uint32_t, std::uint32_t, std::uint64_t, const ConfigParams*);
|
||||
int APS5_VABI _ZN3sce3Job10JobManager8shutdownEv(void*);
|
||||
SequenceFactory* APS5_VABI _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE(void*, std::uint32_t);
|
||||
}
|
||||
|
||||
#define Construct _ZN3sce3Job10JobManagerC1Ev
|
||||
#define RequiredSize _ZN3sce3Job10JobManager27calculateRequiredMemorySizeEPKNS1_21MemorySizeQueryParamsE
|
||||
#define Initialize _ZN3sce3Job10JobManager10initializeEPvmjjjmPKNS1_12ConfigParamsE
|
||||
#define Shutdown _ZN3sce3Job10JobManager8shutdownEv
|
||||
#define Factories _ZN3sce3Job10JobManager34getUnifiedSequenceFactoryInterfaceENS1_8PriorityE
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int InvalidArgument = static_cast<int>(0x81581000u);
|
||||
constexpr int OutOfRange = static_cast<int>(0x81580000u);
|
||||
constexpr int NullArgument = static_cast<int>(0x81580001u);
|
||||
constexpr int NoNodes = static_cast<int>(0x81580002u);
|
||||
constexpr int LimitExceeded = static_cast<int>(0x81580003u);
|
||||
constexpr int NoneFree = static_cast<int>(0x81580006u);
|
||||
constexpr int Invalid = static_cast<int>(0x81580007u);
|
||||
|
||||
void Require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 12> ReaderCode{0xf4040100u, 0xfa000000u, 0xf4080201u, 0xfa000000u, 0xbf8cc07fu, 0xf4000182u, 0xfa000000u, 0xbf8cc07fu, 0x7e000206u, 0xe0700000u, 0x80020000u, 0xbf810000u};
|
||||
alignas(256) std::array<std::uint32_t, 4> Value{};
|
||||
alignas(256) std::array<std::uint32_t, 2> Table{};
|
||||
alignas(256) volatile std::uint32_t Output[4]{};
|
||||
alignas(256) std::array<std::uint32_t, 4> OutputDescriptor{};
|
||||
alignas(32) std::array<std::uint32_t, 8> IndirectArguments{1, 1, 1, 0, 0, 0, 0, 0};
|
||||
alignas(16) std::array<std::uint32_t, 8> Specials{};
|
||||
alignas(16) std::array<std::uint32_t, 8> SpecialsWithoutDispatch{};
|
||||
std::array<ShaderRegister, 9> Registers{};
|
||||
alignas(16) std::array<unsigned char, 0x80> Shader{};
|
||||
alignas(16) std::array<unsigned char, 0x80> ShaderWithoutDispatch{};
|
||||
|
||||
std::uint32_t Low(const volatile void* pointer) {
|
||||
return static_cast<std::uint32_t>(reinterpret_cast<std::uintptr_t>(pointer));
|
||||
}
|
||||
|
||||
std::uint32_t High(const volatile void* pointer) {
|
||||
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(pointer)) >> 32u);
|
||||
}
|
||||
|
||||
void BuildShader() {
|
||||
Table = {Low(Value.data()), High(Value.data())};
|
||||
OutputDescriptor = {Low(Output), High(Output) & 0xffffu, sizeof(Output), 0x31016facu};
|
||||
const auto code = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(ReaderCode.data()));
|
||||
Registers = {{{0x20c, static_cast<std::uint32_t>(code >> 8u)}, {0x20d, static_cast<std::uint32_t>(code >> 40u) & 0xffu}, {0x207, 1}, {0x208, 1}, {0x209, 1}, {0x212, 0}, {0x213, 4u << 1u},
|
||||
{0x242, Low(OutputDescriptor.data())}, {0x243, High(OutputDescriptor.data())}}};
|
||||
Specials[4] = 1;
|
||||
const ShaderRegister* registers = Registers.data();
|
||||
for (auto* shader : {&Shader, &ShaderWithoutDispatch}) {
|
||||
const std::uint32_t* specials = shader == &Shader ? Specials.data() : SpecialsWithoutDispatch.data();
|
||||
std::memcpy(shader->data() + 0x20, ®isters, sizeof(registers));
|
||||
std::memcpy(shader->data() + 0x28, &specials, sizeof(specials));
|
||||
(*shader)[0x5c] = static_cast<unsigned char>(Registers.size());
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t SizeFor(std::uint32_t computeSequences, std::uint64_t mask, bool limits) {
|
||||
const MemorySizeQueryParams query{1, 2, 1, limits ? 100u : 0u, limits ? 8u : 0u, limits ? 4u : 0u, limits ? 2u : 0u, computeSequences, mask};
|
||||
return RequiredSize(&query);
|
||||
}
|
||||
|
||||
std::atomic<bool> Released{false};
|
||||
std::atomic<std::uint32_t> SeenByJob{0};
|
||||
|
||||
void APS5_VABI BlockingJob(void*, std::uint32_t) {
|
||||
while (!Released) std::this_thread::yield();
|
||||
}
|
||||
|
||||
void APS5_VABI ReadOutputJob(void*, std::uint32_t) {
|
||||
SeenByJob = Output[0];
|
||||
}
|
||||
|
||||
void APS5_VABI Nothing(void*, std::uint32_t) {
|
||||
}
|
||||
|
||||
void SizeCase() {
|
||||
Require(SizeFor(0, 0, false) == 0x10d0 && SizeFor(0, 0, true) == 0x14c0, "size without compute sequences");
|
||||
Require(SizeFor(1, 1, false) == 0x24fd8 && SizeFor(1, 1, true) == 0x253c8, "size with one compute sequence");
|
||||
Require(SizeFor(2, 3, false) == 0x48e20 && SizeFor(2, 3, true) == 0x49210, "size with two compute sequences");
|
||||
Require(SizeFor(3, 7, false) == 0x6cb68, "size with three compute sequences");
|
||||
Require(SizeFor(1, 3, true) == 0x491c8, "size with two queues for one compute sequence");
|
||||
Require(SizeFor(2, 1, true) == 0, "size with fewer queues than compute sequences");
|
||||
}
|
||||
|
||||
void RunCase() {
|
||||
const std::uint64_t size = SizeFor(1, 2, true);
|
||||
void* memory = ::operator new((size + 255) & ~std::uint64_t{255}, std::align_val_t{256});
|
||||
Require(memory != nullptr, "allocation failed");
|
||||
alignas(64) static unsigned char manager[0x100];
|
||||
Construct(manager);
|
||||
const ConfigParams badHandler{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x1fff, 0x50, 0};
|
||||
const ConfigParams noAffinity{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0, 0x100, 0};
|
||||
const ConfigParams wideAffinity{1, 0x00021fff, 0x00010000, 0x2bc, nullptr, 0, 0x2000, 0x100, 0};
|
||||
Require(Initialize(manager, memory, size, 2, 1, 1, 2, &badHandler) == InvalidArgument, "a handler priority below 0x100 was accepted");
|
||||
Require(Initialize(manager, memory, size, 2, 1, 1, 2, &noAffinity) == InvalidArgument, "an empty handler affinity was accepted");
|
||||
Require(Initialize(manager, memory, size, 2, 1, 1, 2, &wideAffinity) == InvalidArgument, "a handler affinity beyond 13 cores was accepted");
|
||||
Require(Initialize(manager, memory, size, 2, 1, 0xffffffffu, 2, nullptr) == InvalidArgument, "a sequence count that wraps was accepted");
|
||||
Require(Initialize(manager, memory, size, 2, 1, 1, 2, nullptr) == 0, "initialize with one compute sequence failed");
|
||||
SequenceFactory* factory = Factories(manager, 0);
|
||||
Sequence* sequence = nullptr;
|
||||
Sequence* other = nullptr;
|
||||
Require(factory->CreateCompute(&sequence, 100, 4, 4, 2, "compute") == 0, "the compute sequence was not created");
|
||||
Require(factory->CreateCompute(&other, 1, 1, 1, 1, "second") == NoneFree, "a second compute sequence was created");
|
||||
Require(factory->Create(&other, 1, 1, 1, 1, "cpu") == 0, "the CPU sequence was not created");
|
||||
Require(factory->DestroyCompute(other) == Invalid, "a CPU sequence was destroyed as a compute sequence");
|
||||
Require(factory->Destroy(sequence) == Invalid, "a compute sequence was destroyed as a CPU sequence");
|
||||
Require(factory->Destroy(other) == 0, "the CPU sequence was not destroyed");
|
||||
|
||||
Require(sequence->DispatchCompute(nullptr, Table.data(), 1, 1, 1, nullptr) == NullArgument, "a null shader was accepted");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 0, 1, 1, nullptr) == OutOfRange, "a zero group count was accepted");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, -1, nullptr) == OutOfRange, "a negative group count was accepted");
|
||||
Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), nullptr, nullptr) == NullArgument, "null indirect arguments were accepted");
|
||||
Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), reinterpret_cast<const char*>(IndirectArguments.data()) + 4, nullptr) == NullArgument, "misaligned indirect arguments were accepted");
|
||||
|
||||
Value[0] = 0x11110001u;
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "direct") == 0, "the compute dispatch failed");
|
||||
Require(sequence->Wait() == 0, "the wait after a compute dispatch failed");
|
||||
Require(Output[0] == 0x11110001u, "the compute dispatch had not run when the wait returned: " + std::to_string(Output[0]));
|
||||
|
||||
Value[0] = 0x22220002u;
|
||||
Require(sequence->DispatchComputeIndirect(Shader.data(), Table.data(), IndirectArguments.data(), nullptr) == 0, "the indirect compute dispatch failed");
|
||||
Require(sequence->Wait() == 0, "the wait after an indirect compute dispatch failed");
|
||||
Require(Output[0] == 0x22220002u, "the indirect compute dispatch had not run when the wait returned: " + std::to_string(Output[0]));
|
||||
|
||||
Value[0] = 0x33330003u;
|
||||
Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the dispatch of a shader without the dispatch bit failed");
|
||||
Require(sequence->Wait() == 0, "the wait after a shader without the dispatch bit failed");
|
||||
Require(Output[0] == 0x22220002u, "a shader without the dispatch bit ran");
|
||||
|
||||
Require(sequence->Dispatch(BlockingJob, nullptr, 1, "blocking") == 0, "the blocking job was not dispatched");
|
||||
Require(sequence->SyncPoint() == 0, "the sync point failed");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the compute dispatch behind a sync point failed");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
Require(Output[0] == 0x22220002u, "a compute dispatch ran before the CPU job ahead of its sync point finished");
|
||||
Released = true;
|
||||
Require(sequence->SyncPoint() == 0, "the second sync point failed");
|
||||
Require(sequence->Dispatch(ReadOutputJob, nullptr, 1, "reader") == 0, "the reading job was not dispatched");
|
||||
Require(sequence->Wait() == 0, "the wait after the ordered dispatches failed");
|
||||
Require(Output[0] == 0x33330003u, "the compute dispatch behind a sync point did not run");
|
||||
Require(SeenByJob == 0x33330003u, "a CPU job behind a sync point ran before the compute dispatch ahead of it finished");
|
||||
|
||||
Value[0] = 0x44440004u;
|
||||
Require(sequence->DispatchCompute(ShaderWithoutDispatch.data(), Table.data(), 1, 1, 1, nullptr) == 0, "the leading dispatch failed");
|
||||
Require(sequence->SyncPoint() == 0, "the sync point between compute dispatches failed");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, "behind") == 0, "the compute dispatch behind a compute dispatch failed");
|
||||
Require(sequence->Wait() == 0, "the wait after chained compute dispatches failed");
|
||||
Require(Output[0] == 0x44440004u, "a compute dispatch behind a sync point and a compute dispatch did not run");
|
||||
|
||||
for (int index = 0; index < 4; ++index) Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == 0, "a compute dispatch within the limit failed");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == LimitExceeded, "a compute dispatch past the dispatch limit was accepted");
|
||||
Require(sequence->DispatchCompute(Shader.data(), Table.data(), 1, 1, 1, nullptr) == Invalid, "a latched compute sequence accepted a dispatch");
|
||||
Require(factory->DestroyCompute(sequence) == 0, "the compute sequence was not destroyed");
|
||||
|
||||
Require(factory->Create(&other, 0x7fff, 0x3ff, 0xff, 1, "nodes") == 0, "the node counting sequence was not created");
|
||||
int accepted = 0;
|
||||
int result = 0;
|
||||
while (accepted < 0x3ff && (result = other->Dispatch(Nothing, nullptr, 1, nullptr)) == 0) ++accepted;
|
||||
Require(result == NoNodes && accepted == 43, "the node pool of a compute configuration holds " + std::to_string(accepted) + " dispatches");
|
||||
Require(Shutdown(manager) == 0, "shutdown failed");
|
||||
::operator delete(memory, std::align_val_t{256});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
device.reset();
|
||||
BuildShader();
|
||||
SizeCase();
|
||||
RunCase();
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::cout << "job manager compute test passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try {
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
} catch (const std::exception& shutdownError) {
|
||||
std::cerr << shutdownError.what() << '\n';
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -97,9 +97,9 @@ void BuildBands() {
|
||||
}
|
||||
}
|
||||
|
||||
AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume) {
|
||||
AgcDriver::Graphics::ColorTarget VolumeTarget(std::byte* volume, ColorTileMode tileMode = ColorTileMode::RenderTarget) {
|
||||
AgcDriver::Graphics::ColorTarget color{reinterpret_cast<std::uintptr_t>(volume), {Width, Height}, VK_FORMAT_R8G8B8A8_UNORM, 0, 0xe4u};
|
||||
color.tileMode = ColorTileMode::RenderTarget;
|
||||
color.tileMode = tileMode;
|
||||
color.depth = Slices;
|
||||
color.depthSlice = FirstSlice;
|
||||
color.layers = Layers;
|
||||
@@ -239,19 +239,23 @@ std::uint64_t BlockOffset(const AgcDriver::Graphics::TextureSwizzleEquation& equ
|
||||
}
|
||||
|
||||
void Check(const std::byte* volume, const AgcDriver::Graphics::ColorTarget& color, const std::string& what, const std::array<std::uint8_t, Layers>& bands) {
|
||||
using namespace AgcDriver::Graphics;
|
||||
const auto geometry = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::SurfaceForTarget(color));
|
||||
Require(!geometry.thick, "a 64 KiB render-target volume is not stored as thin slices");
|
||||
const auto block = AgcDriver::Graphics::ThinBlockLayout(AgcDriver::Graphics::TextureTileMode::kR64KBX, 4);
|
||||
Require(Width <= block[1] && Height <= block[2], "the volume slices span several tile blocks");
|
||||
const auto* equation = AgcDriver::Graphics::FindTextureSwizzleEquation(static_cast<std::uint32_t>(ColorTileMode::RenderTarget), 4);
|
||||
Require(equation != nullptr, "missing the 64 KiB R_X swizzle equation");
|
||||
Require(geometry.thick == (color.tileMode == ColorTileMode::S4KBX), "unexpected volume addressing family");
|
||||
const auto tileMode = ColorTextureTileMode(color.tileMode);
|
||||
const auto thin = ThinBlockLayout(tileMode, 4);
|
||||
const auto block = geometry.thick ? ThickBlockExtent(tileMode, 4) : std::array<std::uint32_t, 3>{thin[1], thin[2], 1u};
|
||||
const auto& mip = geometry.mips.at(0);
|
||||
const auto* equation = FindTextureSwizzleEquation((geometry.thick ? 0x100u : 0u) | static_cast<std::uint32_t>(color.tileMode), 4);
|
||||
Require(equation != nullptr, "missing the volume's swizzle equation");
|
||||
for (std::uint32_t slice = 0; slice < Slices; ++slice) {
|
||||
const auto* base = volume + geometry.GuestLayerOffset(slice);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const bool drawn = slice >= FirstSlice && x / BandWidth == slice - FirstSlice;
|
||||
const auto expected = drawn ? bands[slice - FirstSlice] : Kept;
|
||||
const auto* texel = base + BlockOffset(*equation, x, y, slice);
|
||||
const auto offset = (static_cast<std::uint64_t>(y / block[1]) * mip.blocksPerRow + x / block[0]) * thin[0] + BlockOffset(*equation, x, y, slice);
|
||||
const auto* texel = base + offset;
|
||||
for (std::uint32_t channel = 0; channel < 4; ++channel) {
|
||||
const auto actual = std::to_integer<std::uint8_t>(texel[channel]);
|
||||
Require(actual == expected || (drawn && expected == 128 && actual == 127), what + ": slice " + std::to_string(slice) + " texel (" + std::to_string(x) + ", " + std::to_string(y) + ") channel " + std::to_string(channel) + " is " + std::to_string(actual) + ", expected " + std::to_string(expected) + (drawn ? " (the band of layer " + std::to_string(slice - FirstSlice) + ")" : slice < FirstSlice ? " (outside the color view)" : " (another layer's band)"));
|
||||
@@ -283,6 +287,10 @@ int main(int argc, char** argv) {
|
||||
const auto fieldColor = VolumeTarget(fieldVolume.Data());
|
||||
Run(*device, fieldColor, false, LayerFieldPixelCode, true);
|
||||
Check(fieldVolume.Data(), fieldColor, "a pixel shader reading the render target index of its packed ancillary word", LayerFieldBands);
|
||||
const GuestVolume thickVolume((VolumeTarget(nullptr, ColorTileMode::S4KBX).bytes + 65535u) & ~std::size_t{65535u});
|
||||
const auto thickColor = VolumeTarget(thickVolume.Data(), ColorTileMode::S4KBX);
|
||||
Run(*device, thickColor, false, PixelCode, false);
|
||||
Check(thickVolume.Data(), thickColor, "thick SW_4KB_S_X vertex draw and writeback", WhiteBands);
|
||||
if (vertexOnly) std::puts("layered volume vertex draw tests passed");
|
||||
else if (target.mesh.has_value()) Run(*device, color, true, PixelCode, false);
|
||||
else std::puts("mesh case skipped, the device has no VK_EXT_mesh_shader");
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -182,14 +186,41 @@ void Check(const char* name, std::uint64_t operations, std::uint32_t run) {
|
||||
}
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const char* name, std::uint64_t operations, std::uint32_t repeats) {
|
||||
std::uint64_t WorkgroupBytes(std::span<const std::uint32_t> words) {
|
||||
std::unordered_map<std::uint32_t, std::span<const std::uint32_t>> types;
|
||||
std::unordered_map<std::uint32_t, std::uint32_t> constants;
|
||||
std::vector<std::uint32_t> pointers;
|
||||
for (std::size_t at = 5; at < words.size();) {
|
||||
const auto count = words[at] >> 16u;
|
||||
Require(count != 0u && count <= words.size() - at, "lds lock budget: malformed SPIR-V");
|
||||
const auto instruction = words.subspan(at, count);
|
||||
const auto opcode = instruction[0] & 0xffffu;
|
||||
if (opcode == spv::OpTypeInt || opcode == spv::OpTypeArray || opcode == spv::OpTypePointer) types[instruction[1]] = instruction;
|
||||
if (opcode == spv::OpConstant) constants[instruction[2]] = instruction[3];
|
||||
if (opcode == spv::OpVariable && instruction[3] == spv::StorageClassWorkgroup) pointers.push_back(instruction[1]);
|
||||
at += count;
|
||||
}
|
||||
std::uint64_t total = 0;
|
||||
for (const auto pointer : pointers) {
|
||||
const auto definition = types.at(pointer);
|
||||
Require((definition[0] & 0xffffu) == spv::OpTypePointer, "lds lock budget: Workgroup storage has no pointer type");
|
||||
const auto array = types.at(definition[3]);
|
||||
Require((array[0] & 0xffffu) == spv::OpTypeArray, "lds lock budget: unexpected Workgroup allocation shape");
|
||||
const auto element = types.at(array[2]);
|
||||
Require((element[0] & 0xffffu) == spv::OpTypeInt && element[2] == 32u, "lds lock budget: Workgroup array is not dwords");
|
||||
total += static_cast<std::uint64_t>(constants.at(array[3])) * sizeof(std::uint32_t);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const char* name, std::uint64_t operations, std::uint32_t repeats, std::uint32_t ldsDwords = LdsDwords, std::optional<std::uint32_t> expectedDeviceDwords = std::nullopt) {
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, LdsDwords, {false, false, false}, false, 1};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, ldsDwords, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
@@ -198,20 +229,46 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, c
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
if (expectedDeviceDwords) {
|
||||
const auto bytes = WorkgroupBytes(result.spirv.Words());
|
||||
const auto limit = request.target.maxWorkgroupSharedMemoryBytes;
|
||||
Require(bytes <= limit, "lds lock budget: guest LDS and lock exceed the device limit before dispatch");
|
||||
Require(result.workgroupMemoryDwords == *expectedDeviceDwords, "lds lock budget: unexpected device-backed LDS allocation before dispatch");
|
||||
if (*expectedDeviceDwords == 0u) Require(bytes == limit, "lds lock budget: exact-fit case did not retain its lock");
|
||||
std::printf("lds lock budget: %s declarations verified; guest=%u; workgroup=%llu; device=%u\n", name, ldsDwords * 4u, static_cast<unsigned long long>(bytes), result.workgroupMemoryDwords);
|
||||
}
|
||||
for (std::uint32_t run = 0; run < repeats; ++run) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
Check(name, operations, run);
|
||||
}
|
||||
if (expectedDeviceDwords) std::printf("lds lock budget: %s GPU readback verified across 256 lanes\n", name);
|
||||
}
|
||||
|
||||
void LockBudget(AgcDriver::VulkanDevice& device) {
|
||||
const auto target = device.Target();
|
||||
const auto limit = target.maxWorkgroupSharedMemoryBytes;
|
||||
std::printf("lds lock budget: robustBufferAccess2=%u; maxWorkgroupSharedMemoryBytes=%u\n", static_cast<unsigned>(target.robustBufferAccess2), limit);
|
||||
if (std::getenv("ANYPS5_REQUIRE_ROBUST2") != nullptr) Require(target.robustBufferAccess2, "lds lock budget: robustBufferAccess2 was not enabled");
|
||||
Require(limit >= 32u && limit % 4u == 0u, "lds lock budget: unexpected device shared-memory limit");
|
||||
const auto operations = static_cast<std::uint64_t>(Threads) * Iterations;
|
||||
Run(device, UniformCode, "limit", operations, 1, limit / 4u, limit / 4u + 1u);
|
||||
Run(device, UniformCode, "limit-4", operations, 1, (limit - 4u) / 4u, 0u);
|
||||
std::puts("lds lock budget tests passed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--lock-budget") {
|
||||
LockBudget(*device);
|
||||
return 0;
|
||||
}
|
||||
Require(argc == 1, "lds mixed width atomics: unexpected argument");
|
||||
Run(*device, UniformCode, "uniform", static_cast<std::uint64_t>(Threads) * Iterations, 1);
|
||||
Run(*device, DivergentCode, "divergent", static_cast<std::uint64_t>(Threads) / 2u * Iterations, 1);
|
||||
Run(*device, ParityCode, "split lanes", Threads / 2u, Repeats);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "../MeshGeometryFixture.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using AgcDriver::DriverDetail::DrawMiss;
|
||||
|
||||
constexpr std::uint32_t Width = 64;
|
||||
constexpr std::uint32_t Height = 64;
|
||||
alignas(256) std::array<std::byte, Width * Height * 4> Pixels{};
|
||||
alignas(256) constexpr std::array<std::uint16_t, 4> Indices{0, 1, 2, 0};
|
||||
alignas(256) constexpr std::array<std::array<float, 8>, 3> Vertices{{
|
||||
{-0.9f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.1f, -0.8f, 0, 1, 1, 1, 1, 1}, {-0.5f, 0.8f, 0, 1, 1, 1, 1, 1}
|
||||
}};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 4> PixelCode{0x7e0e02f2u, 0xf800180fu, 0x07070707u, 0xbf810000u};
|
||||
|
||||
constexpr std::array<std::uint32_t, 3> LoadBufferDescriptor{0xf4080204u, 0xfa000000u, 0xbf8cc07fu};
|
||||
alignas(256) constexpr auto GeometryCode = [] {
|
||||
std::array<std::uint32_t, LoadBufferDescriptor.size() + AgcDriver::Test::MeshGeometryCode.size()> code{};
|
||||
std::copy(LoadBufferDescriptor.begin(), LoadBufferDescriptor.end(), code.begin());
|
||||
std::copy(AgcDriver::Test::MeshGeometryCode.begin(), AgcDriver::Test::MeshGeometryCode.end(), code.begin() + LoadBufferDescriptor.size());
|
||||
return code;
|
||||
}();
|
||||
|
||||
alignas(16) std::array<std::array<std::uint32_t, 4>, 8> Descriptors{};
|
||||
|
||||
struct Header {
|
||||
Shader shader{};
|
||||
ShaderUserData users{};
|
||||
};
|
||||
|
||||
Header GeometryHeader{};
|
||||
Header PixelHeader{};
|
||||
|
||||
void Register(Header& header, const std::uint32_t* code, std::size_t size, std::uint8_t type) {
|
||||
header.shader.file_header = 0x34333231u;
|
||||
header.shader.version = 0x18u;
|
||||
header.shader.header_size = sizeof(header);
|
||||
header.shader.shader_size = static_cast<std::uint32_t>(size);
|
||||
header.shader.code = code;
|
||||
header.shader.type = type;
|
||||
header.shader.user_data = &header.users;
|
||||
AgcDriverRegisterShader_nid_postfix(&header.shader);
|
||||
}
|
||||
|
||||
void Write(std::vector<std::uint32_t>& commands, std::uint32_t opcode, std::uint32_t offset, std::uint32_t value) {
|
||||
commands.insert(commands.end(), {0xc0010000u | (opcode << 8u), offset, value});
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Setup(const std::array<std::uint32_t, 4>& descriptor) {
|
||||
const auto color = reinterpret_cast<std::uintptr_t>(Pixels.data());
|
||||
const auto geometry = reinterpret_cast<std::uintptr_t>(GeometryCode.data());
|
||||
const auto pixel = reinterpret_cast<std::uintptr_t>(PixelCode.data());
|
||||
const auto table = reinterpret_cast<std::uintptr_t>(descriptor.data());
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> context{
|
||||
{0x2d5, 0x20}, {0x1ff, 12}, {0x2ce, 3}, {0x29b, 2}, {0x2ab, 4}, {0x260, 0},
|
||||
{0x1b6, 0}, {0x207, 0}, {0x200, 0}, {0x203, 0x800},
|
||||
{0x2dc, 0xaa00}, {0x2f8, 0}, {0x292, 2}, {0x293, 0},
|
||||
{0x80, 0}, {0x8d, 0}, {0x83, 0xffff}, {0x8c, 0xa},
|
||||
{0x2f9, 0x2d}, {0x313, 0x6000}, {0x30e, 0xffffffff}, {0x30f, 0xffffffff},
|
||||
{0x206, 0x43f}, {0x204, 0x80000}, {0x205, 0x240},
|
||||
{0x8e, 0xf}, {0x8f, 0xf}, {0x202, 0xcc0010},
|
||||
{0x1c4, 0}, {0x1c5, 9}, {0x1c3, 4}, {0x31c, 0x28028},
|
||||
{0x31b, 0}, {0x31d, 0}, {0x3b0, ((Height - 1u) << 14u) | (Width - 1u)},
|
||||
{0x3b8, 0x9000000}, {0x1e0, 0}, {0xc, 0}, {0xd, (Height << 16u) | Width},
|
||||
{0x81, 0x80000000}, {0x82, (Height << 16u) | Width},
|
||||
{0x90, 0x80000000}, {0x91, (Height << 16u) | Width},
|
||||
{0x94, 0x80000000}, {0x95, (Height << 16u) | Width},
|
||||
{0x318, static_cast<std::uint32_t>(color >> 8u)}, {0x390, static_cast<std::uint32_t>(color >> 40u)},
|
||||
{0x10f, std::bit_cast<std::uint32_t>(32.0f)}, {0x110, std::bit_cast<std::uint32_t>(32.0f)},
|
||||
{0x111, std::bit_cast<std::uint32_t>(32.0f)}, {0x112, std::bit_cast<std::uint32_t>(32.0f)},
|
||||
{0x113, std::bit_cast<std::uint32_t>(1.0f)}, {0x114, 0},
|
||||
{0xb4, 0}, {0xb5, std::bit_cast<std::uint32_t>(1.0f)}
|
||||
};
|
||||
const std::vector<std::pair<std::uint32_t, std::uint32_t>> shaders{
|
||||
{0xc8, static_cast<std::uint32_t>(geometry >> 8u)}, {0xc9, static_cast<std::uint32_t>(geometry >> 40u)},
|
||||
{0x8a, 0x600c0000u}, {0x8b, (8u << 19u) | (3u << 16u) | (2u << 1u)},
|
||||
{0x8c, static_cast<std::uint32_t>(table)}, {0x8d, static_cast<std::uint32_t>(table >> 32u)},
|
||||
{0x8, static_cast<std::uint32_t>(pixel >> 8u)}, {0x9, static_cast<std::uint32_t>(pixel >> 40u)}, {0xb, 0}
|
||||
};
|
||||
std::vector<std::uint32_t> commands;
|
||||
Write(commands, 0x79, 0x242, 4);
|
||||
Write(commands, 0x79, 0x25b, (12u << 9u) | 4u);
|
||||
for (const auto& [offset, value] : context) Write(commands, 0x69, offset, value);
|
||||
for (const auto& [offset, value] : shaders) Write(commands, 0x76, offset, value);
|
||||
commands.insert(commands.end(), {0xc0002a00u, 0u});
|
||||
return commands;
|
||||
}
|
||||
|
||||
AgcDriver::DriverDetail::DrawEntryCounters Draw(const std::array<std::uint32_t, 4>& descriptor) {
|
||||
Pixels.fill(std::byte{0});
|
||||
auto commands = Setup(descriptor);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Indices.data());
|
||||
commands.insert(commands.end(), {0xc0042700u, 3u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 3u, 0u});
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
Require(sceAgcDriverSubmitDcb(&packet) == 0, "mesh submission failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
Require(std::to_integer<unsigned>(Pixels[(24u * Width + 16u) * 4u]) > 200u, "the mesh draw did not cover its triangle");
|
||||
return AgcDriver::DriverDetail::Driver::Get().DrawCacheCounters();
|
||||
}
|
||||
|
||||
void MovedDescriptorTableHitsTheCache() {
|
||||
const auto vertices = reinterpret_cast<std::uintptr_t>(Vertices.data());
|
||||
for (auto& descriptor : Descriptors) descriptor = {static_cast<std::uint32_t>(vertices), static_cast<std::uint32_t>(vertices >> 32u) | (32u << 16u), static_cast<std::uint32_t>(Vertices.size()), 0x01016facu};
|
||||
const auto front = static_cast<std::size_t>(DrawMiss::FrontDiffering);
|
||||
AgcDriver::DriverDetail::DrawEntryCounters counters{};
|
||||
for (std::size_t i = 0; i < 4; ++i) counters = Draw(Descriptors[i]);
|
||||
Require(counters.lookups == 4 && counters.hits == 0, "the draws that teach the moved table made " + std::to_string(counters.hits) + " hits in " + std::to_string(counters.lookups) + " lookups, expected 0 in 4");
|
||||
const auto learned = counters;
|
||||
for (std::size_t i = 4; i < Descriptors.size(); ++i) counters = Draw(Descriptors[i]);
|
||||
Require(counters.misses[front] == learned.misses[front], "a relocated mesh stage was refused as a differing front stage (" + std::to_string(counters.misses[front] - learned.misses[front]) + " times)");
|
||||
Require(counters.hits - learned.hits == Descriptors.size() - 4, "mesh draws whose table moved again did not reuse the cached draw (" + std::to_string(counters.hits - learned.hits) + " hits in " + std::to_string(Descriptors.size() - 4) + " draws)");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device || !TargetHasCapability(device->Target(), spv::CapabilityMeshShadingEXT)) return VulkanTestSkipped;
|
||||
if (device->DeviceName().starts_with("llvmpipe")) {
|
||||
std::puts("skipped, the device has no mesh shader compiler that handles these programs (llvmpipe)");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
}
|
||||
Register(GeometryHeader, GeometryCode.data(), sizeof(GeometryCode), 2);
|
||||
Register(PixelHeader, PixelCode.data(), sizeof(PixelCode), 1);
|
||||
MovedDescriptorTableHitsTheCache();
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
std::cout << "A mesh draw whose descriptor table moved reused the cached draw\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
AgcDriverShutdown_nid_postfix();
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 418 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user