mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-11-03 12:57:27 +00:00
chore: refresh native buffer alias guard after depth write fix
This commit is contained in:
commit
b2aad7239c
675 files changed
+27767
-3631
No files matched your search
@@ -0,0 +1,8 @@
|
||||
agc_driver_graphics undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler.
|
||||
agc_driver_graphics_maps_fallback undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp), which #10973 fixes, and then on Graphics/src/DepthSurface.cpp:730, which binds a TextureDetiler reference to a null pointer when the context has no detiler.
|
||||
agc_driver_pm4 undefined UBSan stops on the misaligned guest word loads of the ASan build's CopyGuestBytes (Execution/src/GuestMemory.cpp); #10973 fixes them.
|
||||
agc_performance_overlay address,undefined Runs past its 30 s ctest TIMEOUT in the sanitizer build on ubuntu-latest (timed out at 30.18 s); it passes without sanitizers, and alone it takes about 19 s; #11555 gives it 120 s in ASan builds.
|
||||
guest_backtrace address Fails only in ASan builds, with 'guest backtrace: first frame is not in the calling module', because the sanitizer adds a frame to backtrace(); #9240 starts the frames at the caller.
|
||||
guest_getrlimit address The test lowers RLIMIT_DATA to 1 TiB, below the private memory ASan has already mapped, so ASan's next internal mapping fails with ENOMEM ('failed to allocate ... DTLS_NextBlock').
|
||||
guest_math address,undefined ubuntu-latest's GCC 15 targets x86-64-v3 and fuses libc's float multiply-adds into FMA, so atanf misses the console bits by one ulp; #11059 builds libc with -ffp-contract=off.
|
||||
guest_vorbis_dec undefined The bundled libvorbis left-shifts a negative value (3rdparty/vorbis/lib/psy.c:319); #11480 updates libvorbis to the upstream fix.
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Sanitizers
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '41 3 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: sanitizers-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CONFIGURE: >-
|
||||
-G Ninja -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
|
||||
-DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
-DANYPS5_ENABLE_ASAN=ON
|
||||
SANITIZER_FLAGS: -g1 -fsanitize=undefined -fno-sanitize=vptr
|
||||
|
||||
jobs:
|
||||
address-undefined:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 330
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
persist-credentials: false
|
||||
- id: submodules
|
||||
name: Key the submodules on their commits
|
||||
run: |
|
||||
echo "key=$(git ls-files -s | awk '$1 == "160000" { print $2, $4 }' | cat - .gitmodules | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
|
||||
echo "ffmpeg=$(git rev-parse HEAD:3rdparty/ffmpeg-core)" >> "$GITHUB_OUTPUT"
|
||||
- id: submodule-cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: ${{ runner.temp }}/submodule-gitdirs.tar
|
||||
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
|
||||
- name: Check out the submodules
|
||||
run: |
|
||||
if [ -f "$RUNNER_TEMP/submodule-gitdirs.tar" ]; then tar -xf "$RUNNER_TEMP/submodule-gitdirs.tar"; fi
|
||||
git submodule update --init --recursive --depth 1 --jobs 8
|
||||
- if: steps.submodule-cache.outputs.cache-hit != 'true'
|
||||
run: tar -cf "$RUNNER_TEMP/submodule-gitdirs.tar" .git/modules
|
||||
- if: steps.submodule-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: ${{ runner.temp }}/submodule-gitdirs.tar
|
||||
key: submodule-gitdirs-${{ runner.os }}-${{ steps.submodules.outputs.key }}
|
||||
- name: Install the build and test dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends ninja-build libx11-dev libxext-dev libxrandr-dev libasound2-dev libpulse-dev mesa-vulkan-drivers
|
||||
- name: Free disk space for the sanitizer build
|
||||
run: |
|
||||
df -h /
|
||||
sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /opt/hostedtoolcache/CodeQL
|
||||
df -h /
|
||||
- uses: hendrikmuhs/[email protected]
|
||||
with:
|
||||
key: sanitizers
|
||||
max-size: 2G
|
||||
save: ${{ github.ref == 'refs/heads/main' }}
|
||||
evict-old-files: job
|
||||
- id: ffmpeg-cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: build/externals
|
||||
key: ffmpeg-sanitizers-${{ runner.os }}-${{ steps.submodules.outputs.ffmpeg }}
|
||||
- id: configure
|
||||
run: cmake -S . -B build $CONFIGURE "-DCMAKE_C_FLAGS=$SANITIZER_FLAGS" "-DCMAKE_CXX_FLAGS=$SANITIZER_FLAGS" "-DANYPS5_AGC_EXECUTION_TESTS=F16ToF32Unpack;F64FmaSubnormals;RegistrationPrepareShutdown"
|
||||
- if: steps.ffmpeg-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v6
|
||||
continue-on-error: true
|
||||
with:
|
||||
path: build/externals
|
||||
key: ${{ steps.ffmpeg-cache.outputs.cache-primary-key }}
|
||||
- name: Check that every test executable is registered with ctest
|
||||
run: python3 tools/check_test_registration.py build
|
||||
- name: Build, keeping on after a target fails so its tests report as not run
|
||||
if: ${{ !cancelled() && steps.configure.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
run: cmake --build build -- -k 0
|
||||
- if: ${{ !cancelled() && steps.configure.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
run: cmake --build build --target libs -- -k 0
|
||||
- name: Run every test under ASan and UBSan
|
||||
if: ${{ !cancelled() && steps.configure.outcome == 'success' }}
|
||||
working-directory: build
|
||||
env:
|
||||
ASAN_OPTIONS: detect_leaks=0
|
||||
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
|
||||
run: ctest --output-on-failure --timeout 300 --parallel 4 --output-log ctest.log --output-junit ctest.xml || true
|
||||
- name: Summarize the sanitizer findings
|
||||
if: ${{ !cancelled() && steps.configure.outcome == 'success' }}
|
||||
run: >-
|
||||
python3 tools/sanitizer_report.py build/ctest.xml --title "ASan and UBSan"
|
||||
--known .github/sanitizer-known-failures.txt --sanitizer address --sanitizer undefined
|
||||
--summary "$GITHUB_STEP_SUMMARY"
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: ${{ !cancelled() }}
|
||||
with:
|
||||
name: sanitizer-tests
|
||||
path: |
|
||||
build/ctest.log
|
||||
build/ctest.xml
|
||||
if-no-files-found: ignore
|
||||
@@ -33,7 +33,8 @@ jobs:
|
||||
node --check demo/web/app.mjs
|
||||
node --check demo/web/archive.mjs
|
||||
node --check demo/web/batch.mjs
|
||||
node --check demo/web/failure-report.mjs
|
||||
node --check demo/web/output-directory.mjs
|
||||
node --check demo/web/worker.mjs
|
||||
- name: Test browser converter helpers
|
||||
run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/output-directory.test.mjs
|
||||
run: node --test demo/web/tests/app.test.mjs demo/web/tests/batch.test.mjs demo/web/tests/failure-report.test.mjs demo/web/tests/output-directory.test.mjs
|
||||
+44
-1
@@ -16,6 +16,13 @@ function(add_test_executable target)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(configure_host_test_executable target)
|
||||
if(MINGW)
|
||||
target_compile_options(${target} PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(${target} PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
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}")
|
||||
@@ -89,7 +96,18 @@ if(ANYPS5_ENABLE_TSAN)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ANYPS5_ENABLE_ASAN)
|
||||
option(ANYPS5_BUILD_FUZZERS "Build the relinker libFuzzer targets with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
|
||||
if(ANYPS5_BUILD_FUZZERS)
|
||||
if(NOT ANYPS5_RELINKER_ONLY)
|
||||
message(FATAL_ERROR "ANYPS5_BUILD_FUZZERS requires ANYPS5_RELINKER_ONLY")
|
||||
endif()
|
||||
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR APPLE OR WIN32)
|
||||
message(FATAL_ERROR "ANYPS5_BUILD_FUZZERS requires Clang on Linux")
|
||||
endif()
|
||||
set(ANYPS5_RELINKER_CLI OFF CACHE BOOL "Build the native relinker command line" FORCE)
|
||||
add_compile_options(-fsanitize=address,undefined,fuzzer-no-link -fno-sanitize=function -fno-sanitize-recover=undefined -fno-omit-frame-pointer)
|
||||
add_link_options(-fsanitize=address,undefined)
|
||||
elseif(ANYPS5_ENABLE_ASAN)
|
||||
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux")
|
||||
endif()
|
||||
@@ -228,12 +246,29 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts)
|
||||
add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py)
|
||||
add_test(NAME check_skips_ctest COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips_ctest.py)
|
||||
set_tests_properties(check_skips_ctest PROPERTIES TIMEOUT 30)
|
||||
add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_skips.py)
|
||||
add_test(NAME sanitizer_report COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_sanitizer_report.py)
|
||||
add_test(NAME select_tests COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py)
|
||||
add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py)
|
||||
set(select_tests_source "${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_select_tests.py")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${select_tests_source}")
|
||||
file(STRINGS "${select_tests_source}" select_tests_classes REGEX "^class [A-Za-z0-9_]*Tests\\(")
|
||||
foreach(test_class IN LISTS select_tests_classes)
|
||||
string(REGEX REPLACE "^class ([A-Za-z0-9_]+).*" "\\1" test_class "${test_class}")
|
||||
if(test_class STREQUAL "SelectTests")
|
||||
set_tests_properties(select_tests PROPERTIES ENVIRONMENT "ANYPS5_SELECT_TEST_CLASS=SelectTests" TIMEOUT 30)
|
||||
else()
|
||||
add_test(NAME select_tests_${test_class} COMMAND ${Python3_EXECUTABLE} "${select_tests_source}" ${test_class})
|
||||
set_tests_properties(select_tests_${test_class} PROPERTIES TIMEOUT 30)
|
||||
endif()
|
||||
endforeach()
|
||||
add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedTests)
|
||||
add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE}
|
||||
@@ -267,6 +302,14 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py)
|
||||
add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py)
|
||||
add_test(NAME pr_overlap_branch_scan COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_branch_scan.py)
|
||||
add_test(NAME pr_overlap_progress COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_progress.py)
|
||||
add_test(NAME pr_overlap_pages COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_pages.py)
|
||||
add_test(NAME pr_overlap_export_cache COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_export_cache.py)
|
||||
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
|
||||
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
|
||||
|
||||
+23
-18
@@ -288,6 +288,15 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(guest_kernel_sys_console_identity PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
add_test_executable(guest_kernel_sys_cpumode_tests tests/GuestKernelSysCpumode.cpp)
|
||||
target_include_directories(guest_kernel_sys_cpumode_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(guest_kernel_sys_cpumode_tests PRIVATE libkernel_sys libkernel libc)
|
||||
configure_windows_unwind(guest_kernel_sys_cpumode_tests)
|
||||
add_test(NAME guest_kernel_sys_cpumode COMMAND guest_kernel_sys_cpumode_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_kernel_sys_cpumode PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel_sys>")
|
||||
endif()
|
||||
add_test_executable(nid_buffer_validation_tests nid/tests/BufferValidation.cpp)
|
||||
target_include_directories(nid_buffer_validation_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/nid/include)
|
||||
target_compile_features(nid_buffer_validation_tests PRIVATE cxx_std_20)
|
||||
@@ -369,10 +378,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME nid_pe_references COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/nid/tests/test_pe_references.py $<TARGET_FILE:nid_patcher>)
|
||||
endif()
|
||||
if(MINGW)
|
||||
target_compile_options(patched_libraries_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(patched_libraries_tests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
configure_host_test_executable(patched_libraries_tests)
|
||||
add_test(NAME patched_libraries COMMAND patched_libraries_tests ${PATCHED_DIR})
|
||||
if(MINGW)
|
||||
get_filename_component(patchedLibrariesRuntimeDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY)
|
||||
@@ -452,10 +458,7 @@ if(BUILD_TESTING)
|
||||
${PATCHED_DIR}/libkernel_sys.prx ${PATCHED_DIR}/libkernel.prx ${LIBC_PATCHED})
|
||||
add_dependencies(guest_kernel_sys_threads_tests guest_kernel_sys_threads_libraries)
|
||||
configure_windows_unwind(guest_kernel_sys_threads_tests)
|
||||
if(MINGW)
|
||||
target_compile_options(guest_kernel_sys_threads_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_link_options(guest_kernel_sys_threads_tests PRIVATE -static -static-libgcc -static-libstdc++)
|
||||
endif()
|
||||
configure_host_test_executable(guest_kernel_sys_threads_tests)
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_test(NAME guest_kernel_sys_threads_main_late_load COMMAND guest_kernel_sys_threads_tests ${UNPATCHED_DIR} unpatched main_late_load)
|
||||
add_test(NAME guest_kernel_sys_threads_main_late_load_patched COMMAND guest_kernel_sys_threads_tests ${PATCHED_DIR} patched main_late_load)
|
||||
@@ -812,6 +815,16 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(guest_rudp_poll PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_test_executable(rudp_poll_wait_exception_tests tests/RudpPollWaitException.cpp
|
||||
prx/libSceRudp/Export.cpp prx/libkernel/Time/TimedWait.cpp)
|
||||
target_include_directories(rudp_poll_wait_exception_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(rudp_poll_wait_exception_tests PRIVATE libc)
|
||||
configure_windows_unwind(rudp_poll_wait_exception_tests)
|
||||
add_test(NAME rudp_poll_wait_exception COMMAND rudp_poll_wait_exception_tests)
|
||||
set_tests_properties(rudp_poll_wait_exception PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
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})
|
||||
@@ -922,15 +935,8 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCppWebApi>")
|
||||
endif()
|
||||
|
||||
add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp)
|
||||
target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData)
|
||||
configure_windows_unwind(savedata_core_write_replacement_tests)
|
||||
add_test(NAME savedata_core_write_replacement COMMAND savedata_core_write_replacement_tests)
|
||||
|
||||
add_test_executable(savedata_native_write_replacement_tests tests/SaveDataWriteReplacement.cpp)
|
||||
target_include_directories(savedata_native_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_compile_definitions(savedata_native_write_replacement_tests PRIVATE SAVEDATA_NATIVE_BACKEND)
|
||||
target_link_libraries(savedata_native_write_replacement_tests PRIVATE libSceSaveData.native)
|
||||
configure_windows_unwind(savedata_native_write_replacement_tests)
|
||||
add_test(NAME savedata_native_write_replacement COMMAND savedata_native_write_replacement_tests)
|
||||
@@ -962,13 +968,10 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
|
||||
if(MINGW)
|
||||
target_compile_options(savedata_core_write_replacement_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_compile_options(savedata_native_write_replacement_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
target_compile_options(savedata_replace_file_failure_tests PRIVATE -fasynchronous-unwind-tables)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(savedata_core_write_replacement PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSaveData>")
|
||||
set_tests_properties(savedata_native_write_replacement PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceSaveData.native>")
|
||||
endif()
|
||||
@@ -1005,6 +1008,8 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libScePosixForWebKit>")
|
||||
endif()
|
||||
|
||||
add_subdirectory(tests/floating_prefix)
|
||||
|
||||
add_test_executable(MemoryPoolTest
|
||||
tests/MemoryPoolTest.cpp
|
||||
prx/libkernel/DirectMemory/MemoryPool.cpp
|
||||
|
||||
@@ -42,7 +42,9 @@ void MoveImportFirst(std::vector<unsigned char>& bytes, const std::string& dll)
|
||||
const auto optionalSize = read16(bytes, pe + 20);
|
||||
const auto optional = pe + 24;
|
||||
if (read16(bytes, optional) != 0x20bu) throw std::runtime_error("not a PE32+ image");
|
||||
const auto importRva = read32(bytes, optional + 112 + 8);
|
||||
constexpr std::size_t ImportDirectory = 112 + 8;
|
||||
if (optionalSize < ImportDirectory + 8 || read32(bytes, optional + 108) < 2) throw std::runtime_error("the image has no imports");
|
||||
const auto importRva = read32(bytes, optional + ImportDirectory);
|
||||
if (importRva == 0) throw std::runtime_error("the image has no imports");
|
||||
const auto sectionTable = optional + optionalSize;
|
||||
const auto fileOffset = [&](std::uint32_t rva) -> std::size_t {
|
||||
@@ -62,18 +64,24 @@ void MoveImportFirst(std::vector<unsigned char>& bytes, const std::string& dll)
|
||||
const auto directory = fileOffset(importRva);
|
||||
constexpr std::size_t DescriptorBytes = 20;
|
||||
std::vector<std::array<unsigned char, DescriptorBytes>> descriptors;
|
||||
std::size_t found = SIZE_MAX;
|
||||
for (std::size_t offset = directory;; offset += DescriptorBytes) {
|
||||
if (offset + DescriptorBytes > bytes.size()) throw std::runtime_error("truncated import directory");
|
||||
std::array<unsigned char, DescriptorBytes> descriptor{};
|
||||
std::memcpy(descriptor.data(), bytes.data() + offset, DescriptorBytes);
|
||||
if (std::all_of(descriptor.begin(), descriptor.end(), [](unsigned char value) { return value == 0; })) break;
|
||||
const auto nameOffset = fileOffset(read32(bytes, offset + 12));
|
||||
std::string name;
|
||||
for (auto at = nameOffset; at < bytes.size() && bytes[at] != 0; ++at) name.push_back(static_cast<char>(bytes[at]));
|
||||
if (lower(name) == lower(dll)) found = descriptors.size();
|
||||
descriptors.push_back(descriptor);
|
||||
}
|
||||
std::size_t found = SIZE_MAX;
|
||||
for (std::size_t index = 0; index < descriptors.size(); ++index) {
|
||||
std::uint32_t nameRva = 0;
|
||||
std::memcpy(&nameRva, descriptors[index].data() + 12, sizeof(nameRva));
|
||||
const auto nameOffset = fileOffset(nameRva);
|
||||
if (nameOffset >= bytes.size()) throw std::runtime_error("RVA outside the file data");
|
||||
const auto terminator = std::find(bytes.begin() + static_cast<std::ptrdiff_t>(nameOffset), bytes.end(), 0);
|
||||
if (terminator == bytes.end()) throw std::runtime_error("unterminated import name");
|
||||
const std::string name(bytes.begin() + static_cast<std::ptrdiff_t>(nameOffset), terminator);
|
||||
if (lower(name) == lower(dll)) found = index;
|
||||
}
|
||||
if (found == SIZE_MAX) throw std::runtime_error("the image does not import " + dll);
|
||||
std::rotate(descriptors.begin(), descriptors.begin() + static_cast<std::ptrdiff_t>(found), descriptors.begin() + static_cast<std::ptrdiff_t>(found) + 1);
|
||||
for (std::size_t index = 0; index < descriptors.size(); ++index) std::memcpy(bytes.data() + directory + index * DescriptorBytes, descriptors[index].data(), DescriptorBytes);
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace {
|
||||
constexpr std::size_t PeOffset = 0x40;
|
||||
constexpr std::size_t OptionalOffset = PeOffset + 24;
|
||||
constexpr std::size_t OptionalBytes = 240;
|
||||
constexpr std::size_t SectionTable = OptionalOffset + OptionalBytes;
|
||||
constexpr std::uint32_t SectionRva = 0x1000;
|
||||
constexpr std::size_t SectionOffset = 0x200;
|
||||
constexpr std::size_t SectionBytes = 0x200;
|
||||
@@ -41,7 +40,7 @@ std::uint32_t get32(const std::vector<unsigned char>& bytes, std::size_t offset)
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<unsigned char> image(const std::vector<std::string>& dlls, std::uint16_t magic = 0x20b) {
|
||||
std::vector<unsigned char> image(const std::vector<std::string>& dlls, std::uint16_t magic = 0x20b, std::uint32_t directories = 16, std::uint16_t optionalBytes = OptionalBytes) {
|
||||
std::vector<unsigned char> bytes(SectionOffset + SectionBytes, 0);
|
||||
bytes[0] = 'M';
|
||||
bytes[1] = 'Z';
|
||||
@@ -49,16 +48,17 @@ std::vector<unsigned char> image(const std::vector<std::string>& dlls, std::uint
|
||||
put32(bytes, PeOffset, 0x00004550u);
|
||||
put16(bytes, PeOffset + 4, 0x8664);
|
||||
put16(bytes, PeOffset + 6, 1);
|
||||
put16(bytes, PeOffset + 20, OptionalBytes);
|
||||
put16(bytes, PeOffset + 20, optionalBytes);
|
||||
put16(bytes, OptionalOffset, magic);
|
||||
put32(bytes, OptionalOffset + 108, 16);
|
||||
put32(bytes, OptionalOffset + 108, directories);
|
||||
put32(bytes, OptionalOffset + 112 + 8, SectionRva);
|
||||
put32(bytes, OptionalOffset + 112 + 12, static_cast<std::uint32_t>((dlls.size() + 1) * DescriptorBytes));
|
||||
std::memcpy(bytes.data() + SectionTable, ".idata", 6);
|
||||
put32(bytes, SectionTable + 8, SectionBytes);
|
||||
put32(bytes, SectionTable + 12, SectionRva);
|
||||
put32(bytes, SectionTable + 16, SectionBytes);
|
||||
put32(bytes, SectionTable + 20, SectionOffset);
|
||||
const auto sectionTable = OptionalOffset + optionalBytes;
|
||||
std::memcpy(bytes.data() + sectionTable, ".idata", 6);
|
||||
put32(bytes, sectionTable + 8, SectionBytes);
|
||||
put32(bytes, sectionTable + 12, SectionRva);
|
||||
put32(bytes, sectionTable + 16, SectionBytes);
|
||||
put32(bytes, sectionTable + 20, SectionOffset);
|
||||
auto name = (dlls.size() + 1) * DescriptorBytes;
|
||||
for (std::size_t index = 0; index < dlls.size(); ++index) {
|
||||
const auto descriptor = SectionOffset + index * DescriptorBytes;
|
||||
@@ -110,6 +110,28 @@ void movesTheNamedImportFirst() {
|
||||
require(unchanged == original, "moving the first import changed the image");
|
||||
}
|
||||
|
||||
void movesTheImportFirstWithATwoDirectoryOptionalHeader(const std::filesystem::path& directory) {
|
||||
const auto original = image({"kernel32.dll", "ucrtbase.dll", "mimalloc-redirect.dll", "advapi32.dll"}, 0x20b, 2, 128);
|
||||
auto moved = original;
|
||||
Allocator::MoveImportFirst(moved, "mimalloc-redirect.dll");
|
||||
require(importName(moved, 0) == "mimalloc-redirect.dll" && importName(moved, 1) == "kernel32.dll" && importName(moved, 2) == "ucrtbase.dll" && importName(moved, 3) == "advapi32.dll",
|
||||
"the import did not move first with a 128-byte optional header holding two directories");
|
||||
require(descriptor(moved, 0) == descriptor(original, 2) && descriptor(moved, 1) == descriptor(original, 0) && descriptor(moved, 2) == descriptor(original, 1) && descriptor(moved, 3) == descriptor(original, 3),
|
||||
"a descriptor did not move whole with a 128-byte optional header");
|
||||
const auto directoryEnd = SectionOffset + 4 * DescriptorBytes;
|
||||
require(std::equal(moved.begin(), moved.begin() + SectionOffset, original.begin()) && std::equal(moved.begin() + directoryEnd, moved.end(), original.begin() + directoryEnd),
|
||||
"bytes outside the import descriptors changed with a 128-byte optional header");
|
||||
auto repeated = moved;
|
||||
Allocator::MoveImportFirst(repeated, "mimalloc-redirect.dll");
|
||||
require(repeated == moved, "repeating the move changed the image with a 128-byte optional header");
|
||||
const auto path = directory / "two-directory.dll";
|
||||
writeFile(path, original);
|
||||
Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll");
|
||||
require(readFile(path) == moved, "the file move differs from the in-memory move with a 128-byte optional header");
|
||||
Allocator::MoveImportFirstInFile(path, "mimalloc-redirect.dll");
|
||||
require(readFile(path) == moved, "repeating the move changed the file with a 128-byte optional header");
|
||||
}
|
||||
|
||||
void movesTheImportInAFile(const std::filesystem::path& directory) {
|
||||
const auto path = directory / "moved.dll";
|
||||
writeFile(path, image({"kernel32.dll", "mimalloc-redirect.dll"}));
|
||||
@@ -139,12 +161,32 @@ int main() {
|
||||
try {
|
||||
std::filesystem::create_directories(directory);
|
||||
movesTheNamedImportFirst();
|
||||
movesTheImportFirstWithATwoDirectoryOptionalHeader(directory);
|
||||
movesTheImportInAFile(directory);
|
||||
failsWithoutTouchingTheFile(directory, "missing-import", image({"kernel32.dll", "ucrtbase.dll"}), "does not import mimalloc-redirect.dll");
|
||||
auto truncated = image({"kernel32.dll", "mimalloc-redirect.dll", "ucrtbase.dll"});
|
||||
truncated.resize(SectionOffset + DescriptorBytes + DescriptorBytes / 2);
|
||||
failsWithoutTouchingTheFile(directory, "truncated-directory", truncated, "truncated import directory");
|
||||
failsWithoutTouchingTheFile(directory, "pe32", image({"kernel32.dll", "mimalloc-redirect.dll"}, 0x10b), "not a PE32+ image");
|
||||
auto undeclared = image({"kernel32.dll", "mimalloc-redirect.dll"});
|
||||
put32(undeclared, OptionalOffset + 108, 0);
|
||||
failsWithoutTouchingTheFile(directory, "directory-count-zero", undeclared, "the image has no imports");
|
||||
auto oneDirectory = image({"kernel32.dll", "mimalloc-redirect.dll"});
|
||||
put32(oneDirectory, OptionalOffset + 108, 1);
|
||||
failsWithoutTouchingTheFile(directory, "directory-count-one", oneDirectory, "the image has no imports");
|
||||
auto shortHeader = image({"kernel32.dll", "mimalloc-redirect.dll"});
|
||||
put16(shortHeader, PeOffset + 20, 112);
|
||||
failsWithoutTouchingTheFile(directory, "optional-header-without-imports", shortHeader, "the image has no imports");
|
||||
auto incompleteEntry = image({"kernel32.dll", "mimalloc-redirect.dll"}, 0x20b, 2, 128);
|
||||
put16(incompleteEntry, PeOffset + 20, 127);
|
||||
failsWithoutTouchingTheFile(directory, "optional-header-incomplete-import-entry", incompleteEntry, "the image has no imports");
|
||||
auto unterminated = image({"kernel32.dll", "mimalloc-redirect.dll"});
|
||||
unterminated.resize(SectionOffset + 3 * DescriptorBytes + 13 + 21);
|
||||
failsWithoutTouchingTheFile(directory, "unterminated-name", unterminated, "unterminated import name");
|
||||
auto outsideFile = image({"kernel32.dll", "mimalloc-redirect.dll", "ucrtbase.dll"});
|
||||
put32(outsideFile, SectionOffset + 2 * DescriptorBytes + 12, static_cast<std::uint32_t>(SectionRva + 0x1ff));
|
||||
outsideFile.resize(0x300);
|
||||
failsWithoutTouchingTheFile(directory, "name-past-the-file", outsideFile, "RVA outside the file data");
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
std::error_code ignored;
|
||||
|
||||
@@ -253,12 +253,6 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
|
||||
std::uint32_t FirstThunk;
|
||||
};
|
||||
|
||||
if (options.executable) {
|
||||
for (const auto index : {4u, 11u, 13u}) {
|
||||
const auto directory = Read<PeDataDirectory>(pe, dataDirectoryOffset + index * sizeof(PeDataDirectory));
|
||||
if (directory.VirtualAddress || directory.Size) throw std::runtime_error("signed, bound or delay-loaded PE imports are not supported in --exec mode");
|
||||
}
|
||||
}
|
||||
struct ImportUse {
|
||||
std::size_t NameOffset;
|
||||
std::size_t LookupOffset;
|
||||
@@ -315,19 +309,27 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
|
||||
importDescOffset += sizeof(PeImportDescriptor);
|
||||
}
|
||||
const auto importMap = ResolveImportNids(importNames, excludedExports, options);
|
||||
if (!options.executable) {
|
||||
for (const auto& item : imports) {
|
||||
const auto& name = importMap.at(item.Name);
|
||||
if (name.size() > item.Name.size()) throw std::runtime_error("import NID longer than original name: " + item.Name);
|
||||
std::memcpy(pe.data() + item.NameOffset, name.c_str(), name.size() + 1);
|
||||
}
|
||||
return;
|
||||
const auto fitsInPlace = [&](const ImportUse& item) {
|
||||
return !options.executable && importMap.at(item.Name).size() <= item.Name.size();
|
||||
};
|
||||
const bool growsImports = options.executable || std::any_of(imports.begin(), imports.end(), [&](const ImportUse& item) {
|
||||
return !fitsInPlace(item) && importMap.at(item.Name) != item.Name;
|
||||
});
|
||||
for (const auto index : {4u, 11u, 13u}) {
|
||||
const auto directory = Read<PeDataDirectory>(pe, dataDirectoryOffset + index * sizeof(PeDataDirectory));
|
||||
if (growsImports && (directory.VirtualAddress || directory.Size))
|
||||
throw std::runtime_error("signed, bound or delay-loaded PE imports are not supported when import names move to a new section");
|
||||
}
|
||||
for (const auto& item : imports) {
|
||||
if (!fitsInPlace(item)) continue;
|
||||
const auto& name = importMap.at(item.Name);
|
||||
std::memcpy(pe.data() + item.NameOffset, name.c_str(), name.size() + 1);
|
||||
}
|
||||
std::vector<std::uint8_t> strings;
|
||||
std::unordered_map<std::string, std::uint32_t> stringOffsets;
|
||||
for (const auto& item : imports) {
|
||||
const auto& name = importMap.at(item.Name);
|
||||
if (name == item.Name || stringOffsets.contains(name)) continue;
|
||||
if (name == item.Name || fitsInPlace(item) || stringOffsets.contains(name)) continue;
|
||||
if (strings.size() & 1u) strings.push_back(0);
|
||||
if (strings.size() > 0xffffffffu - name.size() - 3u) throw std::runtime_error("PE import string table overflow");
|
||||
stringOffsets.emplace(name, static_cast<std::uint32_t>(strings.size()));
|
||||
@@ -372,7 +374,7 @@ void PeNidPatcher::PatchNids(std::vector<std::uint8_t>& pe, const std::string& l
|
||||
Write(pe, optionalHeaderOffset + 64u, std::uint32_t{0});
|
||||
for (const auto& item : imports) {
|
||||
const auto& name = importMap.at(item.Name);
|
||||
if (name == item.Name) continue;
|
||||
if (name == item.Name || fitsInPlace(item)) continue;
|
||||
const auto rva = section.VirtualAddress + stringOffsets.at(name);
|
||||
for (const auto offset : {item.LookupOffset, item.AddressOffset}) {
|
||||
if (magic == 0x20bu) Write(pe, offset, std::uint64_t{rva});
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from test_macho_nids import build_dylib, exports_of, nid, PATCHER
|
||||
|
||||
@@ -441,5 +442,68 @@ class ExecutableImportTests(unittest.TestCase):
|
||||
self.assertEqual(path.read_bytes(), image)
|
||||
|
||||
|
||||
@unittest.skipUnless(PATCHER is not None and PATCHER.is_file(), "nid_patcher executable required")
|
||||
class LibraryImportTests(unittest.TestCase):
|
||||
def library(self, names):
|
||||
image = ExecutableImportTests.pe(SimpleNamespace(names=names))
|
||||
struct.pack_into("<IIHHIIIIIII", image, 0x600, 0, 0, 0, 0, 0x1446, 1, 1, 1, 0x1428, 0x142c, 0x1430)
|
||||
struct.pack_into("<IIH", image, 0x628, 0x1000, 0x1432, 0)
|
||||
image[0x632:0x643] = b"puts_nid_postfix\0"
|
||||
image[0x646:0x64e] = b"lib.dll\0"
|
||||
struct.pack_into("<II", image, 0x58 + 112, 0x1400, 80)
|
||||
return image
|
||||
|
||||
def imports(self, image, count):
|
||||
sections = [struct.unpack_from("<8sIIIIIIHHI", image, 0x58 + 240 + index * 40)
|
||||
for index in range(struct.unpack_from("<H", image, 0x46)[0])]
|
||||
result = []
|
||||
for index in range(count):
|
||||
rva = struct.unpack_from("<Q", image, 576 + index * 8)[0]
|
||||
self.assertEqual(rva, struct.unpack_from("<Q", image, 704 + index * 8)[0])
|
||||
section = next(s for s in sections if s[2] <= rva < s[2] + s[3])
|
||||
offset = section[4] + rva - section[2] + 2
|
||||
result.append((rva, image[offset:image.index(0, offset)].decode()))
|
||||
return result, len(sections)
|
||||
|
||||
def patch(self, image):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "library.dll"
|
||||
path.write_bytes(image)
|
||||
result = subprocess.run([str(PATCHER), "libtest", str(path)], capture_output=True, text=True, timeout=20)
|
||||
return result, path.read_bytes()
|
||||
|
||||
def test_imports_longer_as_nids_move_to_a_new_section(self):
|
||||
names = ["sceA", "sceNetRecv", "sceKernelLongerName", "plain"]
|
||||
image = self.library(names)
|
||||
before, _ = self.imports(image, len(names))
|
||||
result, patched = self.patch(image)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
after, sections = self.imports(patched, len(names))
|
||||
self.assertEqual([name for _, name in after], [nid("sceA"), nid("sceNetRecv"), nid("sceKernelLongerName"), "plain"])
|
||||
self.assertEqual(sections, 2)
|
||||
self.assertEqual([rva == old for (rva, _), (old, _) in zip(after, before)], [False, False, True, True])
|
||||
|
||||
def test_imports_that_fit_keep_the_section_layout(self):
|
||||
names = ["sceKernelLongerName", "plain"]
|
||||
result, patched = self.patch(self.library(names))
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
after, sections = self.imports(patched, len(names))
|
||||
self.assertEqual([name for _, name in after], [nid("sceKernelLongerName"), "plain"])
|
||||
self.assertEqual(sections, 1)
|
||||
|
||||
def test_delay_loaded_imports_cannot_grow(self):
|
||||
for names, accepted in ((["sceNetRecv"], False), (["sceKernelLongerName"], True)):
|
||||
with self.subTest(names=names):
|
||||
image = self.library(names)
|
||||
struct.pack_into("<II", image, 0x58 + 112 + 13 * 8, 0x1500, 32)
|
||||
result, patched = self.patch(image)
|
||||
if accepted:
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
else:
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("delay-loaded", result.stdout + result.stderr)
|
||||
self.assertEqual(patched, image)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -8,15 +8,24 @@
|
||||
#include "prx/libSceAgcDriver/Resource/include/Registration.hpp"
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
std::uint32_t* RejectUnregisteredWorkloadStream(CommandBuffer* buf, std::uint32_t streamId, const char* function) {
|
||||
if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
|
||||
NotImplemented_nid_no_patch(function);
|
||||
return nullptr;
|
||||
constexpr std::uint32_t OpcodeNop = 0x10;
|
||||
constexpr std::uint32_t ActivePacketWords = 18;
|
||||
constexpr std::uint32_t CompletePacketWords = 12;
|
||||
constexpr std::uint32_t InactivePacketWords = 9;
|
||||
constexpr std::uint32_t MaxWorkloadId = 63;
|
||||
constexpr std::uint32_t MaxActiveCount = 63;
|
||||
|
||||
std::uint32_t* NewWorkloadPacket(CommandBuffer* buf, std::uint32_t words, const char* function) {
|
||||
auto* packet = Agc::Command::Allocate(buf, words, function);
|
||||
std::memset(packet, 0, words * sizeof(std::uint32_t));
|
||||
packet[0] = Agc::Command::Header(OpcodeNop, words, 0);
|
||||
return packet;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -83,18 +92,38 @@ std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t vide
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) {
|
||||
(void)workloadIds;
|
||||
(void)workloadCount;
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
if (buf == nullptr || workloadIds == nullptr || workloadCount == 0 || workloadCount > MaxActiveCount || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
|
||||
std::uint64_t mask = 0;
|
||||
for (std::uint32_t index = 0; index < workloadCount; ++index) {
|
||||
const auto id = workloadIds[index];
|
||||
if (id > MaxWorkloadId) return nullptr;
|
||||
const std::uint64_t bit = std::uint64_t{1} << id;
|
||||
if ((mask & bit) != 0) return nullptr;
|
||||
mask |= bit;
|
||||
}
|
||||
auto* packet = NewWorkloadPacket(buf, ActivePacketWords, __func__);
|
||||
packet[1] = streamId;
|
||||
packet[2] = static_cast<std::uint32_t>(mask);
|
||||
packet[3] = static_cast<std::uint32_t>(mask >> 32u);
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadComplete(CommandBuffer* buf, std::uint32_t streamId, std::uint32_t workloadId) {
|
||||
(void)workloadId;
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
if (buf == nullptr || workloadId > MaxWorkloadId || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
|
||||
const std::uint64_t clearMask = ~(std::uint64_t{1} << workloadId);
|
||||
auto* packet = NewWorkloadPacket(buf, CompletePacketWords, __func__);
|
||||
packet[1] = streamId;
|
||||
packet[2] = workloadId;
|
||||
packet[3] = static_cast<std::uint32_t>(clearMask);
|
||||
packet[4] = static_cast<std::uint32_t>(clearMask >> 32u);
|
||||
return packet;
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadStreamInactive(CommandBuffer* buf, std::uint32_t streamId) {
|
||||
return RejectUnregisteredWorkloadStream(buf, streamId, __func__);
|
||||
if (buf == nullptr || !AgcDriverWorkloadStreamRegistered_nid_postfix(streamId)) return nullptr;
|
||||
auto* packet = NewWorkloadPacket(buf, InactivePacketWords, __func__);
|
||||
packet[1] = streamId;
|
||||
return packet;
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, const char* str, uint32_t color) {
|
||||
|
||||
@@ -97,6 +97,19 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_rewind_patch_tests EXCLUDE_FROM_ALL tests/RewindPatch.cpp)
|
||||
target_include_directories(agc_rewind_patch_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_rewind_patch_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_rewind_patch_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_rewind_patch_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_rewind_patch COMMAND agc_rewind_patch_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_rewind_patch PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_draw_index_multi_instanced_tests EXCLUDE_FROM_ALL tests/DrawIndexMultiInstanced.cpp)
|
||||
target_include_directories(agc_draw_index_multi_instanced_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_draw_index_multi_instanced_tests PRIVATE libSceAgc libc)
|
||||
@@ -109,6 +122,19 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_atomic_mem_tests EXCLUDE_FROM_ALL tests/AtomicMem.cpp)
|
||||
target_include_directories(agc_atomic_mem_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_atomic_mem_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_atomic_mem_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_atomic_mem_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_atomic_mem COMMAND agc_atomic_mem_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_atomic_mem PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
configure_windows_unwind(libSceAgc)
|
||||
|
||||
add_test_executable(agc_static_buffer_tests EXCLUDE_FROM_ALL tests/StaticBuffer.cpp)
|
||||
@@ -227,6 +253,19 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_end_of_pipe_type_patch_tests EXCLUDE_FROM_ALL tests/EndOfPipeActionType.cpp)
|
||||
target_include_directories(agc_end_of_pipe_type_patch_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_end_of_pipe_type_patch_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_end_of_pipe_type_patch_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_end_of_pipe_type_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_end_of_pipe_type_patch COMMAND agc_end_of_pipe_type_patch_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_end_of_pipe_type_patch PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_branch_patch_tests EXCLUDE_FROM_ALL
|
||||
tests/BranchPatch.cpp
|
||||
Command/src/Packet.cpp
|
||||
@@ -320,6 +359,19 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_lod_stats_tests EXCLUDE_FROM_ALL tests/LodStats.cpp)
|
||||
target_include_directories(agc_lod_stats_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_lod_stats_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_lod_stats_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_lod_stats_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_lod_stats COMMAND agc_lod_stats_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_lod_stats PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_interpolant_mapping_tests EXCLUDE_FROM_ALL
|
||||
tests/InterpolantMapping.cpp
|
||||
Shader/src/InterpolantMapping.cpp
|
||||
@@ -395,6 +447,7 @@ add_test_executable(agc_dma_data_tests EXCLUDE_FROM_ALL
|
||||
Command/src/Memory.cpp
|
||||
Acb/src/Memory.cpp
|
||||
DcbFlow/src/Memory.cpp
|
||||
Patch/src/Dma.cpp
|
||||
)
|
||||
target_include_directories(agc_dma_data_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_dma_data_tests PRIVATE libSceAgcDriver libc)
|
||||
@@ -488,3 +541,21 @@ if(BUILD_TESTING)
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_create_shader_tests EXCLUDE_FROM_ALL
|
||||
tests/CreateShader.cpp
|
||||
Shader/src/CreateShader.cpp
|
||||
Shader/src/ShaderUtils.cpp
|
||||
)
|
||||
target_include_directories(agc_create_shader_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_create_shader_tests PRIVATE libc libSceAgcDriver)
|
||||
configure_windows_unwind(agc_create_shader_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_create_shader_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_create_shader COMMAND agc_create_shader_tests)
|
||||
set_tests_properties(agc_create_shader PROPERTIES TIMEOUT 30)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_create_shader PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "prx/libSceAgc/DcbFlow/include/Memory.hpp"
|
||||
#include "prx/libSceAgc/Acb/include/Memory.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t Sentinel = 0xabcdef01u;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct Storage {
|
||||
std::array<std::uint32_t, 16> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
|
||||
Storage() {
|
||||
words.fill(Sentinel);
|
||||
}
|
||||
};
|
||||
|
||||
using Writer = std::uint32_t* (APS5_VABI *)(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint8_t, const volatile void*, std::uint64_t, std::uint64_t, std::uint16_t);
|
||||
|
||||
void testPacket(Writer writer, std::uint32_t size, std::uint8_t atomicOp, std::uintptr_t address) {
|
||||
check(size == 36, "ATOMIC_MEM packet size");
|
||||
Storage storage;
|
||||
auto* packet = writer(&storage.buffer, atomicOp, 0x5, 2, reinterpret_cast<const volatile void*>(address), 0x1122334455667788ull, 0x99aabbccddeeff00ull, 0x1abc);
|
||||
const std::array<std::uint32_t, 9> expected{
|
||||
0xc0071e00u,
|
||||
atomicOp | (0x5u << 8u) | (2u << 25u),
|
||||
static_cast<std::uint32_t>(address),
|
||||
static_cast<std::uint32_t>(address >> 32u),
|
||||
0x55667788u,
|
||||
0x11223344u,
|
||||
0xddeeff00u,
|
||||
0x99aabbccu,
|
||||
0x1abcu,
|
||||
};
|
||||
check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), packet), "incorrect ATOMIC_MEM packet");
|
||||
check(storage.buffer.cursor_up == packet + size / sizeof(std::uint32_t), "the cursor did not advance by the packet size");
|
||||
check(std::all_of(storage.words.begin() + expected.size(), storage.words.end(), [](std::uint32_t word) { return word == Sentinel; }), "ATOMIC_MEM wrote past its nine words");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
testPacket(sceAgcDcbAtomicMem, sceAgcDcbAtomicMemGetSize(), 0x2f, 0x0000123456789ab4ull);
|
||||
testPacket(sceAgcDcbAtomicMem, sceAgcDcbAtomicMemGetSize(), 0x6f, 0x0000123456789ab8ull);
|
||||
testPacket(sceAgcAcbAtomicMem, sceAgcAcbAtomicMemGetSize(), 0x2f, 0x0000123456789ab4ull);
|
||||
testPacket(sceAgcAcbAtomicMem, sceAgcAcbAtomicMemGetSize(), 0x6f, 0x0000123456789ab8ull);
|
||||
std::puts("AGC atomic memory tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "prx/libSceAgcDriver/Resource/include/Registration.hpp"
|
||||
#include "prx/libSceAgc/Patch/include/WaitRegMem.hpp"
|
||||
#include "prx/libSceAgc/Patch/include/Nop.hpp"
|
||||
#include "prx/libSceAgc/Patch/include/QueueEndOfPipeAction.hpp"
|
||||
#include "prx/libSceAgc/Misc/include/Suspend.hpp"
|
||||
#include "prx/libSceAgc/Misc/include/RegisterDefaults.hpp"
|
||||
#include "prx/libSceAgc/Misc/include/PacketInfo.hpp"
|
||||
@@ -12,8 +13,13 @@
|
||||
#include "prx/libSceAgc/DcbState/include/Display.hpp"
|
||||
#include "prx/libSceAgc/DcbState/include/ContextState.hpp"
|
||||
#include "prx/libSceAgc/DcbFlow/include/Sync.hpp"
|
||||
#include "prx/libSceAgc/DcbFlow/include/Memory.hpp"
|
||||
#include "prx/libSceAgc/Acb/include/Memory.hpp"
|
||||
#include "prx/libSceAgc/DcbFlow/include/Control.hpp"
|
||||
#include "prx/libSceAgc/DcbDraw/include/Instancing.hpp"
|
||||
#include "prx/libSceAgc/Acb/include/Dispatch.hpp"
|
||||
#include "prx/libSceAgc/Cb/include/Dispatch.hpp"
|
||||
#include "prx/libSceAgc/DcbFlow/include/Dispatch.hpp"
|
||||
#include "prx/libSceAgc/DcbDraw/include/IndexBuffer.hpp"
|
||||
#include "prx/libSceAgc/DcbDraw/include/DrawIndexed.hpp"
|
||||
#include "prx/libSceAgc/DcbDraw/include/DrawNonIndexed.hpp"
|
||||
@@ -332,16 +338,33 @@ void testWorkloadStreams() {
|
||||
check(sceAgcDriverRegisterWorkloadStream(1, record) == 0, "workload stream registration failed");
|
||||
Storage registered;
|
||||
registered.words.fill(0xdeadbeefu);
|
||||
const auto untouched = registered.words;
|
||||
expectFailure([&] { sceAgcDcbSetWorkloadStreamInactive(®istered.buffer, 1); });
|
||||
expectFailure([&] { sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2); });
|
||||
expectFailure([&] { sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3); });
|
||||
expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1); });
|
||||
check(registered.words == untouched && registered.buffer.cursor_up == registered.words.data(), "a workload call that threw modified the buffer");
|
||||
const auto* acbActive = sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 2);
|
||||
check(acbActive == registered.words.data() && acbActive[0] == Agc::Command::Header(0x10, 18, 0) && acbActive[1] == 1 && acbActive[2] == 0x18 && acbActive[3] == 0, "ACB workloads active packet");
|
||||
for (std::uint32_t word = 4; word < 18; ++word) check(acbActive[word] == 0, "ACB workloads active packet tail is not zero");
|
||||
const auto* acbComplete = sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 33);
|
||||
check(acbComplete == registered.words.data() + 18 && acbComplete[0] == Agc::Command::Header(0x10, 12, 0) && acbComplete[1] == 1 && acbComplete[2] == 33 && acbComplete[3] == 0xffffffffu && acbComplete[4] == 0xfffffffdu, "ACB workload complete packet");
|
||||
for (std::uint32_t word = 5; word < 12; ++word) check(acbComplete[word] == 0, "ACB workload complete packet tail is not zero");
|
||||
const auto* acbInactive = sceAgcAcbSetWorkloadStreamInactive(®istered.buffer, 1);
|
||||
check(acbInactive == registered.words.data() + 30 && acbInactive[0] == Agc::Command::Header(0x10, 9, 0) && acbInactive[1] == 1, "ACB workload stream inactive packet");
|
||||
for (std::uint32_t word = 2; word < 9; ++word) check(acbInactive[word] == 0, "ACB workload stream inactive packet tail is not zero");
|
||||
check(registered.buffer.cursor_up == registered.words.data() + 39, "incorrect ACB workload cursor advance");
|
||||
const std::uint32_t duplicate[2] = {5, 5};
|
||||
const std::uint32_t outOfRange[1] = {64};
|
||||
const std::uint32_t top[1] = {63};
|
||||
check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, duplicate, 2) == nullptr, "ACB duplicate workload ids accepted");
|
||||
check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, outOfRange, 1) == nullptr, "ACB workload id 64 accepted");
|
||||
check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, top, 1) != nullptr, "ACB workload id 63 rejected");
|
||||
check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, ids, 0) == nullptr, "ACB workload count 0 accepted");
|
||||
check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 1, nullptr, 2) == nullptr, "ACB null workload ids accepted");
|
||||
check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 0, ids, 2) == nullptr, "ACB stream id 0 accepted");
|
||||
check(sceAgcAcbSetWorkloadsActive(®istered.buffer, 32, ids, 2) == nullptr, "ACB stream id 32 accepted");
|
||||
check(sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 64) == nullptr, "ACB workload complete id 64 accepted");
|
||||
check(sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr, "ACB null command buffer accepted for active");
|
||||
check(sceAgcDcbSetWorkloadStreamInactive(nullptr, 1) == nullptr && sceAgcAcbSetWorkloadsActive(nullptr, 1, ids, 2) == nullptr &&
|
||||
sceAgcAcbSetWorkloadComplete(nullptr, 1, 3) == nullptr && sceAgcAcbSetWorkloadStreamInactive(nullptr, 1) == nullptr,
|
||||
"a workload call with a null command buffer for a registered stream returned a packet");
|
||||
check(sceAgcDriverUnregisterWorkloadStream(1) == 0, "workload stream unregistration failed");
|
||||
check(sceAgcAcbSetWorkloadComplete(®istered.buffer, 1, 3) == nullptr, "ACB workload complete packet after unregistration");
|
||||
}
|
||||
|
||||
void testIndexBuffer() {
|
||||
@@ -593,6 +616,32 @@ void testMemory() {
|
||||
check(storage.words == beforeMask, "invalid mask patch modified packet memory");
|
||||
}
|
||||
|
||||
void testEndOfPipePatches() {
|
||||
std::array<std::uint32_t, 8> packet{};
|
||||
packet[0] = Agc::Command::Header(0x49u, 8);
|
||||
packet[1] = 0xf0000fffu;
|
||||
packet[2] = 2u << 29u;
|
||||
check(sceAgcQueueEndOfPipeActionPatchData(packet.data(), 0x1122334455667788ull) == 0, "end-of-pipe data patch failed");
|
||||
check(packet[5] == 0x55667788u && packet[6] == 0x11223344u, "64-bit end-of-pipe data patch did not write both halves");
|
||||
packet[2] = 1u << 29u;
|
||||
check(sceAgcQueueEndOfPipeActionPatchData(packet.data(), 0x0badf00du) == 0, "32-bit end-of-pipe data patch failed");
|
||||
check(packet[5] == 0x0badf00du && packet[6] == 0u, "32-bit end-of-pipe data patch wrote the wrong words");
|
||||
expectFailure([&] { sceAgcQueueEndOfPipeActionPatchData(packet.data(), 0x100000000ull); });
|
||||
check(packet[5] == 0x0badf00du && packet[6] == 0u, "a rejected 32-bit data patch changed the packet");
|
||||
check(sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 0x0a5u) == 0, "end-of-pipe GCR control patch failed");
|
||||
check(packet[1] == (0xf0000fffu | (0x0a5u << 12u)), "end-of-pipe GCR control patch did not keep the other control bits");
|
||||
check(sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 0x100u) == 0 && packet[1] == (0xf0000fffu | (0x300u << 12u)), "end-of-pipe GCR control 0x100 did not gain 0x200");
|
||||
check(sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 0x200u) == 0 && packet[1] == (0xf0000fffu | (0x200u << 12u)), "end-of-pipe GCR control 0x200 changed");
|
||||
const auto before = packet;
|
||||
expectFailure([&] { sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 0x1000u); });
|
||||
check(packet == before, "a rejected GCR control patch changed the packet");
|
||||
packet[0] = Agc::Command::Header(0x47u, 6);
|
||||
expectFailure([&] { sceAgcQueueEndOfPipeActionPatchData(packet.data(), 1); });
|
||||
expectFailure([&] { sceAgcQueueEndOfPipeActionPatchGcrCntl(packet.data(), 1); });
|
||||
packet[0] = before[0];
|
||||
check(packet == before, "a data or GCR control patch of another packet changed it");
|
||||
}
|
||||
|
||||
struct AtomicGdsAllocation {
|
||||
Storage storage;
|
||||
std::uint32_t capacity = 15;
|
||||
@@ -798,6 +847,32 @@ void testDefaultsTypes() {
|
||||
checkCompactTypes(sceAgcGetRegisterDefaults2Internal(0), 15, 0xf58fea31u, 0x00040400u, 0);
|
||||
}
|
||||
|
||||
void testDispatchPackets() {
|
||||
Storage graphics;
|
||||
const auto* indirect = sceAgcDcbDispatchIndirect(&graphics.buffer, 0x40, 0x8);
|
||||
check(indirect == graphics.words.data() && graphics.words[0] == 0xc0011600u && graphics.words[1] == 0x40u && graphics.words[2] == 0x49u, "graphics DISPATCH_INDIRECT packet mismatch");
|
||||
Storage compute;
|
||||
const auto* arguments = reinterpret_cast<const volatile void*>(0x123456789abcull);
|
||||
sceAgcAcbDispatchIndirect(&compute.buffer, arguments, 0x2000u);
|
||||
const std::array<std::uint32_t, 4> computeExpected{0xc0021600u, 0x56789abcu, 0x1234u, 0x2041u};
|
||||
check(std::equal(computeExpected.begin(), computeExpected.end(), compute.words.begin()), "compute DISPATCH_INDIRECT packet mismatch");
|
||||
Storage direct;
|
||||
sceAgcCbDispatch(&direct.buffer, 7, 3, 1, 0x8000u);
|
||||
const std::array<std::uint32_t, 5> directExpected{0xc0031500u, 7, 3, 1, 0x8041u};
|
||||
check(std::equal(directExpected.begin(), directExpected.end(), direct.words.begin()), "DISPATCH_DIRECT packet mismatch");
|
||||
Storage rejected;
|
||||
rejected.words.fill(0xa5a5a5a5u);
|
||||
const auto rejectedWords = rejected.words;
|
||||
const auto* rejectedCursor = rejected.buffer.cursor_up;
|
||||
expectFailure([&] { sceAgcCbDispatch(&rejected.buffer, 1, 1, 1, 0x100u); });
|
||||
check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer");
|
||||
expectFailure([&] { sceAgcAcbDispatchIndirect(&rejected.buffer, reinterpret_cast<const volatile void*>(0x1002ull), 0); });
|
||||
check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer");
|
||||
expectFailure([&] { sceAgcAcbDispatchIndirect(&rejected.buffer, nullptr, 0); });
|
||||
check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer");
|
||||
check(sceAgcDcbDispatchIndirectGetSize() == 12 && sceAgcAcbDispatchIndirectGetSize() == 16 && sceAgcCbDispatchGetSize() == 20, "dispatch packet sizes");
|
||||
}
|
||||
|
||||
void testEventWrite() {
|
||||
Storage storage;
|
||||
const auto* samplePacket = sceAgcDcbEventWrite(&storage.buffer, 0x38, reinterpret_cast<const void*>(0x1000));
|
||||
@@ -839,6 +914,35 @@ void testEventWrite() {
|
||||
check(cacheFlushWithAddress.words[0] == 0xc0004600u && cacheFlushWithAddress.words[1] == 0x16u, "incorrect event 0x16 with address packet");
|
||||
}
|
||||
|
||||
void testAcquireMem() {
|
||||
Storage storage;
|
||||
const auto* base = reinterpret_cast<const volatile void*>(0x1234567890ull);
|
||||
const auto* packet = sceAgcDcbAcquireMem(&storage.buffer, 0, 3, 0x1234u, base, 0x1000, 400);
|
||||
const std::array<std::uint32_t, 8> expected{0xc0065800u, 3, 0x11, 0, 0x12345678u, 0, 10, 0x1234u};
|
||||
check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), storage.words.begin()), "acquire packet does not round its range outward to 256 bytes");
|
||||
Storage whole;
|
||||
sceAgcDcbAcquireMem(&whole.buffer, 0, 0, 0, base, 0xffffffffffffffffull, 0);
|
||||
check(whole.words[0] == 0xc0065800u && whole.words[2] == 0 && whole.words[4] == 0 && whole.words[6] == 0, "an unbounded acquire did not cover the whole address space");
|
||||
Storage large;
|
||||
sceAgcDcbAcquireMem(&large.buffer, 0, 0, 0, base, (1ull << 40u) + 1u, 0);
|
||||
check(large.words[2] == 0 && large.words[4] == 0, "an acquire over 1 TiB did not cover the whole address space");
|
||||
Storage asynchronous;
|
||||
sceAgcAcbAcquireMem(&asynchronous.buffer, 0x55u, base, 0x100, 80);
|
||||
const std::array<std::uint32_t, 8> compute{0xc0065800u, 0x80000000u, 2, 0, 0x12345678u, 0, 2, 0x55u};
|
||||
check(std::equal(compute.begin(), compute.end(), asynchronous.words.begin()), "the asynchronous compute acquire did not select engine 1 with no CB/DB operation");
|
||||
Storage rejected;
|
||||
rejected.words.fill(0xa5a5a5a5u);
|
||||
const auto rejectedWords = rejected.words;
|
||||
const auto* rejectedCursor = rejected.buffer.cursor_up;
|
||||
expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 2, 0, 0, base, 0x100, 0); });
|
||||
check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer");
|
||||
expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 0, 0, 0x80000u, base, 0x100, 0); });
|
||||
check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer");
|
||||
expectFailure([&] { sceAgcDcbAcquireMem(&rejected.buffer, 0, 0, 0, base, 0x100, 40u * 0x10000u); });
|
||||
check(rejected.words == rejectedWords && rejected.buffer.cursor_up == rejectedCursor, "a refused packet changed the command buffer");
|
||||
check(sceAgcDcbAcquireMemGetSize() == 32 && sceAgcAcbAcquireMemGetSize() == 32, "acquire packet size");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
@@ -852,7 +956,9 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
testPackets();
|
||||
testEventWrite();
|
||||
testAcquireMem();
|
||||
testNop();
|
||||
testDispatchPackets();
|
||||
testClearState();
|
||||
testIndexedIndirectDraws();
|
||||
testTagHeaders();
|
||||
@@ -869,6 +975,7 @@ int main(int argc, char** argv) {
|
||||
testRegisterRange();
|
||||
testPacketPayloadAddress();
|
||||
testMemory();
|
||||
testEndOfPipePatches();
|
||||
testAtomicGds();
|
||||
testDebugRaiseException();
|
||||
testDefaults();
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "prx/libSceAgc/Shader/include/CreateShader.hpp"
|
||||
#include "prx/libSceAgc/Shader/include/ShaderConstants.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
const Shader* registered = nullptr;
|
||||
unsigned registrations = 0;
|
||||
|
||||
alignas(256) const std::array<std::uint32_t, 64> code{0xbf810000u};
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T* relative(T* target, const void* field) {
|
||||
return reinterpret_cast<T*>(reinterpret_cast<std::uintptr_t>(target) - reinterpret_cast<std::uintptr_t>(field));
|
||||
}
|
||||
|
||||
struct Binary {
|
||||
Shader shader{};
|
||||
std::array<ShaderRegister, 3> registers{};
|
||||
std::array<ShaderRegister, 1> context{};
|
||||
ShaderSpecialRegs specials{};
|
||||
ShaderUserData userData{};
|
||||
std::array<std::uint16_t, 2> direct{};
|
||||
std::array<ShaderSharp, 4> sharps{};
|
||||
std::array<ShaderSemantic, 1> inputs{};
|
||||
std::array<ShaderSemantic, 1> outputs{};
|
||||
};
|
||||
|
||||
void fill(Binary& binary, ShaderRegs::ShaderBinaryType type, std::uint32_t low) {
|
||||
binary.shader.file_header = ShaderRegs::SHADER_FILE_HEADER_MAGIC;
|
||||
binary.shader.version = ShaderRegs::SHADER_VERSION;
|
||||
binary.shader.header_size = sizeof(Shader);
|
||||
binary.shader.shader_size = sizeof(code);
|
||||
binary.shader.type = static_cast<std::uint8_t>(type);
|
||||
binary.registers = {{{low, 0x10u}, {low + 1u, 0xabcdef00u}, {0x20au, 0x1234u}}};
|
||||
binary.shader.num_sh_registers = static_cast<std::uint8_t>(binary.registers.size());
|
||||
binary.shader.sh_registers = relative(binary.registers.data(), &binary.shader.sh_registers);
|
||||
binary.shader.specials = relative(&binary.specials, &binary.shader.specials);
|
||||
}
|
||||
|
||||
Shader* create(Binary& binary) {
|
||||
registered = nullptr;
|
||||
registrations = 0;
|
||||
Shader* created = nullptr;
|
||||
check(sceAgcCreateShader(&created, &binary.shader, code.data()) == 0, "sceAgcCreateShader failed");
|
||||
check(created == &binary.shader && registered == &binary.shader && registrations == 1, "the created shader is not the header, registered once");
|
||||
check(binary.shader.code == code.data(), "the code address was not stored in the header");
|
||||
return created;
|
||||
}
|
||||
|
||||
void testCompute() {
|
||||
Binary binary;
|
||||
fill(binary, ShaderRegs::ShaderBinaryType::Cs, ShaderRegs::COMPUTE_PGM_LO);
|
||||
create(binary);
|
||||
check(binary.shader.sh_registers == binary.registers.data() && binary.shader.specials == &binary.specials, "the relative register and special pointers were not resolved");
|
||||
check(binary.shader.user_data == nullptr && binary.shader.cx_registers == nullptr && binary.shader.input_semantics == nullptr && binary.shader.output_semantics == nullptr, "a null relative pointer did not stay null");
|
||||
const auto program = reinterpret_cast<std::uint64_t>(code.data()) + 0x1000u;
|
||||
check(binary.registers[0].value == static_cast<std::uint32_t>(program >> 8u), "COMPUTE_PGM_LO does not hold the code address plus its offset");
|
||||
check(binary.registers[1].value == (0xabcdef00u | static_cast<std::uint32_t>((program >> 40u) & 0xffu)), "COMPUTE_PGM_HI did not keep its upper bits around the address byte");
|
||||
check(binary.registers[2].offset == 0x20au && binary.registers[2].value == 0x1234u, "a register other than the program address changed");
|
||||
}
|
||||
|
||||
void testPixelWithUserData() {
|
||||
Binary binary;
|
||||
fill(binary, ShaderRegs::ShaderBinaryType::Ps, ShaderRegs::SPI_SHADER_PGM_LO_PS);
|
||||
binary.shader.user_data = relative(&binary.userData, &binary.shader.user_data);
|
||||
binary.shader.cx_registers = relative(binary.context.data(), &binary.shader.cx_registers);
|
||||
binary.shader.num_cx_registers = 1;
|
||||
binary.shader.input_semantics = relative(binary.inputs.data(), &binary.shader.input_semantics);
|
||||
binary.shader.output_semantics = relative(binary.outputs.data(), &binary.shader.output_semantics);
|
||||
binary.userData.direct_resource_offset = relative(binary.direct.data(), &binary.userData.direct_resource_offset);
|
||||
for (std::size_t i = 0; i < binary.sharps.size(); ++i) binary.userData.sharp_resource_offset[i] = relative(&binary.sharps[i], &binary.userData.sharp_resource_offset[i]);
|
||||
create(binary);
|
||||
check(binary.shader.user_data == &binary.userData && binary.shader.cx_registers == binary.context.data(), "the relative user data and context register pointers were not resolved");
|
||||
check(binary.shader.input_semantics == binary.inputs.data() && binary.shader.output_semantics == binary.outputs.data(), "the relative semantic pointers were not resolved");
|
||||
check(binary.userData.direct_resource_offset == binary.direct.data(), "the user data's direct resource pointer was not resolved");
|
||||
for (std::size_t i = 0; i < binary.sharps.size(); ++i) check(binary.userData.sharp_resource_offset[i] == &binary.sharps[i], "a user data sharp resource pointer was not resolved");
|
||||
const auto program = reinterpret_cast<std::uint64_t>(code.data()) + 0x1000u;
|
||||
check(binary.registers[0].value == static_cast<std::uint32_t>(program >> 8u) && binary.registers[1].value == (0xabcdef00u | static_cast<std::uint32_t>((program >> 40u) & 0xffu)), "SPI_SHADER_PGM_LO_PS and _HI_PS do not hold the code address");
|
||||
}
|
||||
|
||||
void testWithoutProgramAddress() {
|
||||
Binary binary;
|
||||
fill(binary, ShaderRegs::ShaderBinaryType::Fs, 0x30u);
|
||||
const auto registers = binary.registers;
|
||||
create(binary);
|
||||
check(binary.registers[0].offset == registers[0].offset && binary.registers[0].value == registers[0].value && binary.registers[1].value == registers[1].value && binary.registers[2].value == registers[2].value, "a fetch shader's registers changed");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) {
|
||||
registered = shader;
|
||||
++registrations;
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
testCompute();
|
||||
testPixelWithUserData();
|
||||
testWithoutProgramAddress();
|
||||
std::puts("AGC create shader tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "prx/libSceAgc/DcbFlow/include/Memory.hpp"
|
||||
#include "prx/libSceAgc/Acb/include/Memory.hpp"
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
#include "prx/libSceAgc/Patch/include/Dma.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -73,6 +74,26 @@ void testCommand(TWriter writer, TSize size, std::uint32_t control) {
|
||||
check(shortBuffer.words == before && shortBuffer.buffer.cursor_up == shortBuffer.words.data(), "failed DMA write modified the buffer");
|
||||
}
|
||||
|
||||
void testPatches() {
|
||||
constexpr std::uint64_t PatchedDestination = 0x0000aabbccddeeffull;
|
||||
constexpr std::uint64_t PatchedSource = 0x0000102030405060ull;
|
||||
for (auto* writer : {writeCompute, writeDraw}) {
|
||||
Storage storage;
|
||||
auto* packet = writer(&storage.buffer);
|
||||
auto expected = storage.words;
|
||||
check(sceAgcDmaDataPatchSetDstAddressOrOffset(packet, PatchedDestination) == 0, "the destination patch failed");
|
||||
expected[4] = 0xccddeeffu;
|
||||
expected[5] = 0xaabbu;
|
||||
check(storage.words == expected, "the destination patch did not replace only words 4 and 5");
|
||||
check(sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, PatchedSource) == 0, "the source patch failed");
|
||||
expected[2] = 0x30405060u;
|
||||
expected[3] = 0x1020u;
|
||||
check(storage.words == expected, "the source patch did not replace only words 2 and 3");
|
||||
check(sceAgcDmaDataPatchSetSrcAddressOrOffsetOrImmediate(packet, 0xfedcba9876543210ull) == 0 && packet[2] == 0x76543210u && packet[3] == 0xfedcba98u, "the source patch did not keep a 64-bit immediate whole");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
@@ -80,6 +101,7 @@ int main() {
|
||||
testCommand(writeCompute, sceAgcAcbDmaDataGetSize, 0x44102000u);
|
||||
testCommand(writeDraw, sceAgcDcbDmaDataGetSize, 0xc4102001u);
|
||||
check(sceAgcAcbDmaDataGetSize() == sceAgcDcbDmaDataGetSize(), "compute and draw DMA sizes differ");
|
||||
testPatches();
|
||||
std::puts("AGC DMA data tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "prx/libSceAgc/Cb/include/Sync.hpp"
|
||||
#include "prx/libSceAgc/Patch/include/QueueEndOfPipeAction.hpp"
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Storage {
|
||||
std::array<std::uint32_t, 16> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 8> release(std::uint8_t action, const volatile Label* label) {
|
||||
Storage storage;
|
||||
const auto* packet = sceAgcCbReleaseMem(&storage.buffer, action, 0x0a5, 1, 2, label, 2, 0x1122334455667788ull, 0, 0, 0, 0);
|
||||
std::array<std::uint32_t, 8> words{};
|
||||
for (std::size_t index = 0; index < words.size(); ++index) words[index] = packet[index];
|
||||
return words;
|
||||
}
|
||||
|
||||
int fail(const char* message) {
|
||||
std::fprintf(stderr, "%s\n", message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
alignas(8) static Label label{};
|
||||
auto packet = release(0x28, &label);
|
||||
const auto original = packet;
|
||||
for (const std::uint8_t action : {0x00u, 0x14u, 0x2eu, 0x2fu, 0x30u, 0x3fu}) {
|
||||
if (sceAgcQueueEndOfPipeActionPatchType(packet.data(), action) != 0) return fail("the end-of-pipe type patch failed");
|
||||
if (packet != release(action, &label)) return fail("a patched end-of-pipe action differs from a release built with that action");
|
||||
}
|
||||
if (sceAgcQueueEndOfPipeActionPatchType(packet.data(), 0x28) != 0 || packet != original) return fail("patching the action back did not restore the packet");
|
||||
std::puts("AGC end-of-pipe type patch tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "prx/libSceAgc/DcbDraw/include/Stats.hpp"
|
||||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace {
|
||||
|
||||
struct Storage {
|
||||
std::array<std::uint32_t, 64> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
};
|
||||
|
||||
int fail(const char* message) {
|
||||
std::fprintf(stderr, "%s\n", message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
alignas(64) static std::array<std::uint8_t, 0x840> report{};
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(report.data());
|
||||
for (const std::uint8_t policy : {0u, 2u}) {
|
||||
Storage dcb;
|
||||
const auto* first = sceAgcDcbGetLodStats(&dcb.buffer, policy, nullptr, 0, 0, 0, 0, 0);
|
||||
const auto* second = sceAgcDcbGetLodStats(&dcb.buffer, policy, report.data(), 0x840, 0, 0, 1, 0);
|
||||
if (first != dcb.words.data() || second != dcb.words.data() + 5 || dcb.buffer.cursor_up != dcb.words.data() + 10) return fail("LOD stats packets are not five words each");
|
||||
const std::array<std::uint32_t, 5> expectedFirst{0xc0031000u, 0, 0, 0, static_cast<std::uint32_t>(policy) << 28u};
|
||||
const std::array<std::uint32_t, 5> expectedSecond{0xc0031000u, 0x840u, static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), (static_cast<std::uint32_t>(policy) << 28u) | (1u << 19u)};
|
||||
for (std::size_t index = 0; index < 5; ++index) {
|
||||
if (dcb.words[index] != expectedFirst[index]) return fail("the sizing LOD stats packet differs");
|
||||
if (dcb.words[5 + index] != expectedSecond[index]) return fail("the reporting LOD stats packet differs");
|
||||
}
|
||||
Storage fields;
|
||||
sceAgcDcbGetLodStats(&fields.buffer, 3, report.data(), 0x40, 0xff, 1, 0, 0xff);
|
||||
if (fields.words[4] != ((3u << 28u) | (1u << 18u) | (0xffu << 10u) | (0xffu << 2u))) return fail("the LOD stats control word lost a field");
|
||||
}
|
||||
std::puts("AGC LOD stats tests passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "prx/libSceAgc/DcbFlow/include/Control.hpp"
|
||||
#include "prx/libSceAgc/Patch/include/Rewind.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t Sentinel = 0xabcdef01u;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
struct Storage {
|
||||
std::array<std::uint32_t, 8> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
|
||||
Storage() {
|
||||
words.fill(Sentinel);
|
||||
}
|
||||
};
|
||||
|
||||
void testRewind() {
|
||||
check(sceAgcDcbRewindGetSize() == 8, "REWIND packet size");
|
||||
for (std::uint32_t state : {0u, 1u}) {
|
||||
Storage storage;
|
||||
auto* packet = sceAgcDcbRewind(&storage.buffer, state);
|
||||
check(packet == storage.words.data() && packet[0] == 0xc0005900u && packet[1] == state << 31u, "REWIND does not carry the initial state in bit 31");
|
||||
check(storage.buffer.cursor_up == packet + sceAgcDcbRewindGetSize() / sizeof(std::uint32_t), "REWIND did not advance the cursor by its size");
|
||||
check(storage.words[2] == Sentinel, "REWIND wrote past its two words");
|
||||
}
|
||||
}
|
||||
|
||||
void testRewindStatePatch() {
|
||||
Storage storage;
|
||||
auto* packet = sceAgcDcbRewind(&storage.buffer, 0);
|
||||
check(sceAgcRewindPatchSetRewindState(packet, 1) == 0 && packet[0] == 0xc0005900u && packet[1] == 0x80000000u, "the rewind state patch did not set bit 31");
|
||||
check(sceAgcRewindPatchSetRewindState(packet, 0) == 0 && packet[0] == 0xc0005900u && packet[1] == 0, "the rewind state patch did not clear bit 31");
|
||||
check(sceAgcAsyncRewindPatchSetRewindState(packet, 1) == 0 && packet[1] == 0x80000000u, "the asynchronous rewind state patch did not set bit 31");
|
||||
check(storage.words[2] == Sentinel, "a rewind state patch wrote past the packet");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
testRewind();
|
||||
testRewindStatePatch();
|
||||
std::puts("AGC rewind tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -1568,6 +1568,29 @@ add_custom_command(
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
add_test_executable(agc_pipeline_cache_allocation_tests EXCLUDE_FROM_ALL tests/PipelineCacheFailures.cpp
|
||||
Graphics/src/PipelineCache.cpp tests/AllocationFailure.cpp)
|
||||
target_include_directories(agc_pipeline_cache_allocation_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_pipeline_cache_allocation_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_pipeline_cache_allocation_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_pipeline_cache_allocation_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_pipeline_cache_allocation_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
foreach(mode startup now busy destructor worker)
|
||||
add_test(NAME agc_pipeline_cache_allocation_${mode} COMMAND agc_pipeline_cache_allocation_tests ${mode})
|
||||
set_tests_properties(agc_pipeline_cache_allocation_${mode} PROPERTIES TIMEOUT 25)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_pipeline_cache_allocation_${mode} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endforeach()
|
||||
if(WIN32)
|
||||
add_test(NAME agc_pipeline_cache_read_retry COMMAND agc_pipeline_cache_allocation_tests read-retry)
|
||||
set_tests_properties(agc_pipeline_cache_read_retry PROPERTIES TIMEOUT 25 ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_driver_recorder_tests EXCLUDE_FROM_ALL
|
||||
tests/Recorder.cpp
|
||||
tests/HostImportLifetime.cpp
|
||||
@@ -1846,6 +1869,8 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_draw_snapshot_epoch PROPERTIES SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_multisample_guest_access COMMAND agc_driver_recorder_tests --multisample-only)
|
||||
set_tests_properties(agc_multisample_guest_access PROPERTIES SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_repeat_storage_guard COMMAND agc_driver_recorder_tests --repeat-storage-guard-only)
|
||||
set_tests_properties(agc_repeat_storage_guard PROPERTIES SKIP_RETURN_CODE 77)
|
||||
add_test(NAME agc_draw_inputs_in_place COMMAND agc_driver_recorder_tests --draw-inputs-in-place-only)
|
||||
add_test(NAME agc_pending_write_snapshot COMMAND agc_driver_recorder_tests --pending-write-snapshot-only)
|
||||
set_tests_properties(agc_pending_write_snapshot PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
@@ -1866,7 +1891,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME agc_metadata_pass COMMAND agc_driver_recorder_tests --metadata-pass-only)
|
||||
set_tests_properties(agc_metadata_pass PROPERTIES SKIP_RETURN_CODE 77)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_recorder_read_list agc_host_import_unmap agc_parked_import_unmap agc_host_import_retirement agc_sparse_run_import agc_sparse_run_import_copied agc_unit_shadow agc_driver_recorder agc_idle_publish_wait agc_descriptor_pool_recycling agc_draw_snapshot_cap agc_draw_snapshot_epoch agc_multisample_guest_access agc_repeat_storage_guard agc_draw_inputs_in_place agc_draw_inputs_copied agc_resident_present agc_slice_dirty agc_queued_write_groups agc_metadata_pass agc_tiled_resolve PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
add_test(NAME agc_host_import_second_mapping COMMAND agc_driver_recorder_tests --host-import-second-mapping-only)
|
||||
set_tests_properties(agc_host_import_second_mapping PROPERTIES SKIP_RETURN_CODE 77 ENVIRONMENT_MODIFICATION
|
||||
@@ -2068,6 +2093,9 @@ if(BUILD_TESTING)
|
||||
add_test(NAME agc_performance_overlay_device_selection_cpu COMMAND agc_performance_overlay_tests device-selection-cpu)
|
||||
set_tests_properties(agc_performance_overlay agc_performance_overlay_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
set_tests_properties(agc_performance_overlay_device_selection_cpu PROPERTIES TIMEOUT 30)
|
||||
if(ANYPS5_ENABLE_ASAN)
|
||||
set_tests_properties(agc_performance_overlay PROPERTIES TIMEOUT 120)
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_performance_overlay_statistics agc_keyboard_overlay agc_performance_overlay agc_performance_overlay_device_selection agc_performance_overlay_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
@@ -2247,6 +2275,9 @@ file(GLOB agcExecutionTestSources CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/
|
||||
set(agcExecutionTests)
|
||||
foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
|
||||
get_filename_component(agcExecutionTestName ${agcExecutionTestSource} NAME_WE)
|
||||
if(NOT WIN32 AND agcExecutionTestName STREQUAL "CommittedReadMapping")
|
||||
continue()
|
||||
endif()
|
||||
if(ANYPS5_AGC_EXECUTION_TESTS AND NOT agcExecutionTestName IN_LIST ANYPS5_AGC_EXECUTION_TESTS)
|
||||
set(agcExecutionTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/ExecutionTestNotBuilt.cpp)
|
||||
endif()
|
||||
@@ -2427,6 +2458,9 @@ if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_two_sample_dithered_alpha_to_coverage COMMAND agc_driver_alpha_to_coverage_tests --dithered2)
|
||||
set(multisampledDccClearTests)
|
||||
foreach(samples IN ITEMS 2 4 8)
|
||||
set(testName "agc_driver_msaa_dcc_srgb_clear_${samples}")
|
||||
add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-srgb-clear ${samples})
|
||||
list(APPEND multisampledDccClearTests ${testName})
|
||||
set(testName "agc_driver_msaa_dcc_clear_${samples}")
|
||||
add_test(NAME ${testName} COMMAND agc_driver_alpha_to_coverage_tests --dcc-fast-clear ${samples})
|
||||
list(APPEND multisampledDccClearTests ${testName})
|
||||
@@ -2503,6 +2537,16 @@ target_include_directories(agc_driver_color565_target_tests PRIVATE $<TARGET_PRO
|
||||
|
||||
target_include_directories(agc_driver_depth_range_remap_draw_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_test(NAME agc_driver_tessellation_depth_range_remap COMMAND agc_driver_tessellation_tests --depth-remap)
|
||||
set_tests_properties(agc_driver_tessellation_depth_range_remap PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_tessellation_depth_range_remap PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_include_directories(agc_driver_depth_bounds_emulation_draw_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
foreach(stage IN ITEMS vert frag)
|
||||
@@ -2554,6 +2598,7 @@ target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $<TARGET_PR
|
||||
|
||||
target_include_directories(agc_driver_image_converted_unorm_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
target_include_directories(agc_driver_failed_capture_dump_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
target_include_directories(agc_driver_gcn_buffer_format_lookup_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>)
|
||||
|
||||
@@ -2666,6 +2711,10 @@ if(BUILD_TESTING)
|
||||
set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME ${agcTestName} COMMAND ${agcTest})
|
||||
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(agcTestName STREQUAL "agc_driver_image_table")
|
||||
set_tests_properties(${agcTestName} PROPERTIES TIMEOUT 30
|
||||
FAIL_REGULAR_EXPRESSION "SYNC-HAZARD-|VUID-|was not found but was requested by env var VK_INSTANCE_LAYERS")
|
||||
endif()
|
||||
if(WIN32)
|
||||
set_tests_properties(${agcTestName} PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
@@ -2715,12 +2764,15 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_driver_sparse_indirect_indices_unreadable agc_driver_sparse_indirect_indices_no_size agc_driver_sparse_indirect_indices_no_size_unreadable agc_driver_sparse_indirect_indices_no_size_unreadable_record PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_draw_cache_relocation_recipe_verify COMMAND agc_driver_draw_cache_relocation_tests)
|
||||
set_tests_properties(agc_driver_draw_cache_relocation_recipe_verify PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_VERIFY_DRAW_RECIPE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77 TIMEOUT 60 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
add_test(NAME agc_driver_draw_cache_relocation_evicted COMMAND agc_driver_draw_cache_relocation_tests --evicted)
|
||||
add_test(NAME agc_driver_draw_cache_relocation_kept COMMAND agc_driver_draw_cache_relocation_tests --kept)
|
||||
set_tests_properties(agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_DRAW_CACHE_ENTRIES=2" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_driver_draw_cache_relocation_recipe_verify agc_driver_draw_cache_relocation_evicted agc_driver_draw_cache_relocation_kept PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
set_tests_properties(agc_driver_pixel_implicit_derivatives PROPERTIES TIMEOUT 30)
|
||||
@@ -2780,6 +2832,11 @@ 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_pack_ownership COMMAND agc_shader_disk_cache_tests --prebuilt-pack-ownership)
|
||||
set_tests_properties(agc_shader_disk_cache_pack_ownership PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_shader_disk_cache_pack_ownership 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)
|
||||
if(Python3_Interpreter_FOUND)
|
||||
set(housekeepingModes plain)
|
||||
@@ -2894,6 +2951,7 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_pipeline_journal_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
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)
|
||||
add_test(NAME agc_pipeline_store_touch COMMAND agc_driver_graphics_tests --pipeline-store-touch)
|
||||
add_test(NAME agc_pipeline_store_growth COMMAND agc_driver_graphics_tests --pipeline-store-growth)
|
||||
add_test(NAME agc_pipeline_store_fixed_bound COMMAND agc_driver_graphics_tests --pipeline-store-growth)
|
||||
add_test(NAME agc_pipeline_store_replay_growth COMMAND agc_driver_graphics_tests --pipeline-store-replay-growth)
|
||||
@@ -2901,12 +2959,12 @@ if(BUILD_TESTING)
|
||||
set_tests_properties(agc_pipeline_store_fixed_bound PROPERTIES ENVIRONMENT "APS5_PIPELINE_STORE_ENTRIES=1024")
|
||||
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}")
|
||||
set_tests_properties(agc_pipeline_store_lazy_buffer_pool agc_pipeline_store_touch PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
add_test(NAME agc_pipeline_journal_extended_paths COMMAND agc_driver_graphics_tests --pipeline-journal-extended-paths)
|
||||
set_tests_properties(agc_pipeline_journal_extended_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
set_tests_properties(agc_pipeline_journal_long_paths agc_pipeline_journal_extended_paths 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 agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth 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 agc_pipeline_store_touch agc_pipeline_store_growth agc_pipeline_store_fixed_bound agc_pipeline_store_replay_growth 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)
|
||||
@@ -3005,6 +3063,13 @@ if(BUILD_TESTING)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_f64_denorm_preserve_spirv PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_volume_direct_readback COMMAND agc_driver_layered_volume_draw_tests)
|
||||
set_tests_properties(agc_driver_volume_direct_readback PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_TEST_DIRECT_READBACK=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_driver_volume_direct_readback PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_f16_to_f32_unpack_spirv COMMAND agc_driver_f16_to_f32_unpack_tests --verify-spirv)
|
||||
set_tests_properties(agc_driver_f16_to_f32_unpack_spirv PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
|
||||
if(WIN32)
|
||||
@@ -3111,6 +3176,7 @@ if(BUILD_TESTING)
|
||||
$<TARGET_FILE:libSceAgcDriver>)
|
||||
set_tests_properties(agc_driver_guest_memory_lifetime_converted PROPERTIES
|
||||
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIN_WAIT_MS=1000" SKIP_RETURN_CODE 77 TIMEOUT 30)
|
||||
add_asan_test_options(agc_driver_guest_memory_lifetime_converted verify_asan_link_order=0)
|
||||
endif()
|
||||
set_tests_properties(agc_driver_flat_store PROPERTIES RUN_SERIAL TRUE)
|
||||
set_target_properties(agc_driver_flat_apertures_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
@@ -3292,6 +3358,13 @@ if(BUILD_TESTING)
|
||||
endif()
|
||||
endforeach()
|
||||
endforeach()
|
||||
add_test(NAME agc_driver_depth_plane_cpu_write_after_memo COMMAND agc_driver_depth_plane_partial_store_tests --cpu-write-after-memo)
|
||||
set_tests_properties(agc_driver_depth_plane_cpu_write_after_memo 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_depth_plane_cpu_write_after_memo PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test(NAME agc_driver_depth_plane_partial_store_unimported COMMAND agc_driver_depth_plane_partial_store_tests)
|
||||
add_test(NAME agc_driver_depth_plane_partial_store_buffer_copy COMMAND agc_driver_depth_plane_partial_store_tests --buffer-copy)
|
||||
foreach(partialStoreTest agc_driver_depth_plane_partial_store_unimported agc_driver_depth_plane_partial_store_buffer_copy)
|
||||
@@ -3715,6 +3788,12 @@ if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS)
|
||||
add_test(NAME agc_compute_pipeline_replay COMMAND agc_compute_pipeline_replay_tests)
|
||||
set_tests_properties(agc_compute_pipeline_replay PROPERTIES TIMEOUT 30 SKIP_RETURN_CODE 77)
|
||||
add_dependencies(agc_compute_pipeline_replay_tests agc_compute_pipeline_replay agc_driver_branch_past_endpgm_tests)
|
||||
add_test(NAME agc_compute_pipeline_inspection COMMAND ${CMAKE_COMMAND}
|
||||
-DREPLAY=$<TARGET_FILE:agc_compute_pipeline_replay>
|
||||
-DVULKAN_PROBE=$<TARGET_FILE:agc_compute_pipeline_replay_tests>
|
||||
-DOUTPUT_ROOT=${CMAKE_CURRENT_BINARY_DIR}/capture-tests
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineInspectRun.cmake)
|
||||
set_tests_properties(agc_compute_pipeline_inspection PROPERTIES TIMEOUT 60 SKIP_REGULAR_EXPRESSION "compute pipeline inspection: skipped")
|
||||
add_test(NAME agc_compute_pipeline_capture_run COMMAND ${CMAKE_COMMAND}
|
||||
-DFIXTURE=$<TARGET_FILE:agc_driver_branch_past_endpgm_tests>
|
||||
-DREPLAY=$<TARGET_FILE:agc_compute_pipeline_replay>
|
||||
@@ -3722,7 +3801,7 @@ if(BUILD_TESTING AND ANYPS5_ENABLE_SPIRV_TOOLS)
|
||||
-P ${CMAKE_CURRENT_SOURCE_DIR}/tests/ComputePipelineCaptureRun.cmake)
|
||||
set_tests_properties(agc_compute_pipeline_capture_run PROPERTIES TIMEOUT 30 SKIP_REGULAR_EXPRESSION "compute pipeline capture run: skipped, no usable Vulkan device")
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
set_tests_properties(agc_compute_pipeline_replay agc_compute_pipeline_capture_run agc_compute_pipeline_inspection PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ struct DrawEntryCounters {
|
||||
|
||||
std::uint64_t registerKeyLookups = 0, registerKeyHits = 0, decodeSkipped = 0, decodePartial = 0, facadeMismatches = 0, verifyDecodes = 0, verifyDecodeMismatches = 0;
|
||||
double keyUs = 0;
|
||||
std::uint64_t absentNewRegisters = 0, absentUserWords = 0, absentEvicted = 0, absentNeverInserted = 0, evictions = 0;
|
||||
std::uint64_t absentNewRegisters = 0, absentUserWords = 0, absentEvicted = 0, absentNeverInserted = 0, evictions = 0, byteEvictions = 0;
|
||||
std::array<std::uint64_t, 6> registerKeyMissChanges{};
|
||||
std::uint64_t differingSameRuns = 0, differingWords = 0, differingRunsChanged = 0;
|
||||
std::uint64_t dataHits = 0, dataStagesReused = 0, dataStagesCompiled = 0, dataVerified = 0;
|
||||
|
||||
@@ -107,7 +107,9 @@ public:
|
||||
std::uint64_t variantBytes = 0;
|
||||
};
|
||||
DispatchCacheFootprint DispatchCacheSize();
|
||||
DispatchCacheFootprint DrawCacheSize();
|
||||
static std::uint64_t DispatchCacheBytes();
|
||||
static std::uint64_t DrawCacheBytes();
|
||||
static ShaderMemory::PendingWrite QueryPendingSpan(std::uint64_t address, std::size_t bytes, std::span<std::byte> known);
|
||||
std::shared_ptr<const ShaderRegistry> RegisteredShaders();
|
||||
|
||||
@@ -143,6 +145,7 @@ private:
|
||||
void completeSubmission(std::uint64_t serial, std::uint64_t received, std::span<const std::uint64_t> labelWrites, const std::shared_ptr<SubmissionProgress>& progress);
|
||||
static bool waitFree(const Submission& submission);
|
||||
bool queue0Before(std::uint64_t received) const;
|
||||
bool waitFreeBefore(std::uint64_t received) const;
|
||||
bool orderReleased(std::uint32_t queue, std::uint64_t received) const;
|
||||
void noteWaitBlocked(std::uint32_t queue, std::uint64_t awaited, bool blocked);
|
||||
void reportPresents(double waitedMs, std::size_t inFlight);
|
||||
@@ -274,7 +277,7 @@ private:
|
||||
void noteForeignWriter(std::uint64_t begin, std::uint64_t end, std::uint32_t queue);
|
||||
void noteDrawWriters(std::span<const Graphics::CompiledShader> stages, std::uint32_t queue);
|
||||
static std::vector<std::pair<std::uint64_t, std::uint64_t>> drawWriteRanges(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> stages);
|
||||
void commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace = {});
|
||||
void commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace = {}, std::uint64_t shape = 0);
|
||||
bool labelPacketQueues(std::span<const std::uint32_t> packet, std::uint32_t opcode);
|
||||
bool enqueueLabelPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue);
|
||||
bool enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32_t opcode, std::uint32_t queue, const QueueState& state);
|
||||
@@ -428,6 +431,7 @@ private:
|
||||
std::uint64_t drawCacheHits = 0, drawCacheEvictions = 0, drawCacheVariants = 0, drawCacheVariantBytes = 0;
|
||||
std::optional<DrawKeySightings> drawKeySightings;
|
||||
std::optional<DrawKeySightings> drawShapeSightings;
|
||||
std::uint64_t drawCacheByteEvictions = 0;
|
||||
|
||||
DrawEntryCounters drawEntryCounters;
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ struct QueueWorker {
|
||||
|
||||
std::atomic<std::uint64_t> queued{0};
|
||||
std::unordered_map<std::uint64_t, std::uint32_t> unfinishedWrites;
|
||||
std::uint64_t executingWaitFree = 0;
|
||||
bool executingWaits = false;
|
||||
std::thread thread;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
namespace AgcDriver::DriverDetail {
|
||||
|
||||
inline constexpr std::uint32_t SubmissionQueueEnd = 0x58;
|
||||
inline constexpr std::uint32_t SubmissionQueueEnd = 0x59;
|
||||
inline constexpr std::uint32_t UnmeasuredSubmissionQueueEnd = 0x58;
|
||||
|
||||
std::size_t SubmissionRingCapacity();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ struct Float32FlushControl {
|
||||
|
||||
inline Float32FlushControl Float32FlushControlFor(VkDriverId driver, VkBool32 flushToZero, VkShaderFloatControlsIndependence independence) {
|
||||
const bool declared = flushToZero == VK_TRUE && independence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE;
|
||||
return {declared || driver == VK_DRIVER_ID_MOLTENVK, declared};
|
||||
return {declared || driver == VK_DRIVER_ID_MOLTENVK || driver == VK_DRIVER_ID_NVIDIA_PROPRIETARY, declared};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -251,11 +251,24 @@ inline Registers BuildInitialContextRegisters() {
|
||||
return result;
|
||||
}
|
||||
|
||||
inline Registers InitialContextRegisters() {
|
||||
inline const Registers& InitialContextRegisterSet() {
|
||||
static const Registers& initial = *new Registers(BuildInitialContextRegisters());
|
||||
return initial;
|
||||
}
|
||||
|
||||
inline Registers InitialContextRegisters() {
|
||||
return InitialContextRegisterSet();
|
||||
}
|
||||
|
||||
struct SpareRegisters {
|
||||
Registers registers;
|
||||
SpareRegisters() = default;
|
||||
SpareRegisters(const SpareRegisters&) {}
|
||||
SpareRegisters& operator=(const SpareRegisters&) { return *this; }
|
||||
SpareRegisters(SpareRegisters&&) noexcept = default;
|
||||
SpareRegisters& operator=(SpareRegisters&&) noexcept = default;
|
||||
};
|
||||
|
||||
struct Predication {
|
||||
std::uint64_t address = 0;
|
||||
std::uint32_t operation = 0;
|
||||
@@ -268,6 +281,7 @@ struct QueueState {
|
||||
Registers context = InitialContextRegisters();
|
||||
Registers userConfig{{0x24a, 0}, {0x24b, 0}};
|
||||
std::optional<Registers> savedContext;
|
||||
SpareRegisters spareContext;
|
||||
std::vector<std::uint32_t> constantRam;
|
||||
std::uint64_t indexBase = 0;
|
||||
std::uint64_t drawIndirectBase = 0;
|
||||
@@ -279,7 +293,18 @@ struct QueueState {
|
||||
Predication predication;
|
||||
|
||||
void ClearContext() {
|
||||
context = InitialContextRegisters();
|
||||
context = InitialContextRegisterSet();
|
||||
}
|
||||
|
||||
void PushContext() {
|
||||
spareContext.registers = context;
|
||||
savedContext.emplace(std::move(spareContext.registers));
|
||||
}
|
||||
|
||||
void PopContext() {
|
||||
std::swap(context, *savedContext);
|
||||
spareContext.registers = std::move(*savedContext);
|
||||
savedContext.reset();
|
||||
}
|
||||
|
||||
std::span<const std::uint32_t> ConstantRam() const {
|
||||
|
||||
@@ -81,6 +81,7 @@ public:
|
||||
};
|
||||
static SupplyCounts Supplies();
|
||||
[[nodiscard]] std::vector<std::shared_ptr<const ShaderRecompiler::SrtGuestWords>> SuppliedBlocks() const;
|
||||
[[nodiscard]] std::size_t Bytes() const;
|
||||
struct VouchCounts {
|
||||
std::uint64_t calls, blocks, refused, words;
|
||||
};
|
||||
@@ -100,6 +101,10 @@ private:
|
||||
return (bits[index / 64u] & block) == block;
|
||||
}
|
||||
void Set(std::size_t index) { bits[index / 64u] |= std::uint64_t{1} << (index % 64u); }
|
||||
void Clear(std::size_t index) { bits[index / 64u] &= ~(std::uint64_t{1} << (index % 64u)); }
|
||||
void ClearMask(const WordMask& other) {
|
||||
for (std::size_t i = 0; i < bits.size(); ++i) bits[i] &= ~other.bits[i];
|
||||
}
|
||||
void SetAll() { bits.fill(~std::uint64_t{0}); }
|
||||
void SetBlock(std::size_t index) { bits[index / 64u] |= ((std::uint64_t{1} << BlockWords) - 1u) << (index % 64u / BlockWords * BlockWords); }
|
||||
void Reset() { bits.fill(0u); }
|
||||
@@ -117,6 +122,7 @@ private:
|
||||
struct Page {
|
||||
std::array<std::uint32_t, PageWords> words;
|
||||
WordMask valid;
|
||||
WordMask raw;
|
||||
WordMask read;
|
||||
WordMask recent;
|
||||
bool wordwise = false;
|
||||
@@ -137,6 +143,8 @@ private:
|
||||
static ShaderRecompiler::SrtWriteStamp stampWrites(void* context, std::uint64_t address, std::uint64_t bytes);
|
||||
static bool unchangedSince(void* context, std::uint64_t address, std::uint64_t bytes, const ShaderRecompiler::SrtWriteStamp& stamp);
|
||||
static bool supplyWords(void* context, const std::shared_ptr<const ShaderRecompiler::SrtGuestWords>& words);
|
||||
static std::uint64_t descriptorScope(void* context, std::size_t bytes);
|
||||
bool invalidateRawPages();
|
||||
Page& page(std::uint64_t base);
|
||||
[[nodiscard]] std::size_t pageAtOrAfter(std::uint64_t base) const;
|
||||
Page& addPage(std::size_t position, std::uint64_t base);
|
||||
@@ -158,6 +166,9 @@ private:
|
||||
PendingWriteObserver observe = nullptr;
|
||||
HookWaitCounter hookWaits = nullptr;
|
||||
PendingWriteQuery pendingSpan = nullptr;
|
||||
std::size_t descriptorSpanBytes = 0;
|
||||
std::uint64_t rawInvalidations = 0;
|
||||
std::uint32_t forceSync = 0;
|
||||
};
|
||||
|
||||
// The positions, among a dispatch-cache variant's stored words, of the pure flat-SRT leaves a
|
||||
|
||||
@@ -13,6 +13,10 @@ inline bool WorkgroupScopeFenceUsable(VkDriverId driver) {
|
||||
return driver != VK_DRIVER_ID_AMD_PROPRIETARY;
|
||||
}
|
||||
|
||||
inline bool LdsLockFencesRequired(VkDriverId driver) {
|
||||
return driver == VK_DRIVER_ID_MOLTENVK;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -344,7 +344,7 @@ public:
|
||||
using DrawTraceContext = Graphics::DrawTraceContext;
|
||||
// `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, direct draw
|
||||
// built for its draw-cache entry (design_cpu_final M8); null otherwise.
|
||||
void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr);
|
||||
void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr, std::uint64_t shape = 0);
|
||||
bool DrawInputsInPlace() const;
|
||||
std::optional<std::string> KnownDrawRejection(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> shaders) const;
|
||||
void ColorMetadataPass(const Graphics::ColorMetadataPass& pass);
|
||||
|
||||
@@ -65,7 +65,8 @@ Driver::DispatchCacheFootprint Driver::DispatchCacheSize() {
|
||||
}
|
||||
|
||||
std::uint64_t Driver::variantBytes(const DispatchVariant& variant) {
|
||||
return sizeof(DispatchVariant) + variant.words.size() * sizeof(std::uint32_t) + variant.shared.size() * sizeof(SharedWords) + variant.runs.size() * sizeof(std::pair<std::uint64_t, std::uint64_t>) + variant.captured.size() * sizeof(ShaderRecompiler::MemoryRegion) + (variant.vertexInfo != nullptr ? sizeof(ShaderRecompiler::ShaderVertexStageInfo) : 0);
|
||||
const auto held = [](const auto& values) { return static_cast<std::uint64_t>(values.capacity() * sizeof(values[0])); };
|
||||
return sizeof(DispatchVariant) + held(variant.runs) + held(variant.words) + held(variant.shared) + held(variant.dataPositions) + held(variant.dataSlots) + held(variant.runBases) + held(variant.shaderDataPatches) + held(variant.captured) + held(variant.spans) + (variant.vertexInfo != nullptr ? sizeof(ShaderRecompiler::ShaderVertexStageInfo) : 0) + (variant.memory != nullptr ? variant.memory->Bytes() : 0);
|
||||
}
|
||||
|
||||
void Driver::accountVariant(const DispatchVariant& variant, bool added) {
|
||||
|
||||
@@ -503,6 +503,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
|
||||
}
|
||||
decode = std::move(refreshed);
|
||||
}
|
||||
if (decode != nullptr && !userPatches.empty() && verifyDrawRecipe()) {
|
||||
auto patched = std::make_shared<DrawDecode>(*decode);
|
||||
for (const auto& patch : userPatches) patched->programs.at(patch.program).userData.at(patch.index) = patch.value;
|
||||
decode = std::move(patched);
|
||||
}
|
||||
try {
|
||||
resolveDrawDecode(queue, submission, localDevice->graphicsContext(), decode, registerKey, drawKey, profile);
|
||||
} catch (...) {
|
||||
@@ -1240,10 +1245,10 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
|
||||
const auto commitPacket = tail.packetOpcode;
|
||||
const auto epoch = DrawFinisher::OnFinisherThread() ? tail.epoch : DrawPipeline::EpochToken().load(std::memory_order_relaxed);
|
||||
auto reads = QueuedDrawReadRanges(drawParameters, stages);
|
||||
auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, trace = trace, writablePrograms = std::move(writablePrograms), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable {
|
||||
auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, shape = tail.shapeKey, commitQueue, commitPacket, epoch, trace = trace, writablePrograms = std::move(writablePrograms), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)]() mutable {
|
||||
DrawPipeline::FollowEpoch(epoch);
|
||||
GuestMemory::SetCurrentPacket(commitPacket, commitQueue);
|
||||
commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey, trace);
|
||||
commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey, trace, shape);
|
||||
};
|
||||
auto held = std::make_shared<decltype(commit)>(std::move(commit));
|
||||
DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads), true);
|
||||
@@ -1265,14 +1270,14 @@ DrawVerdict Driver::finishDraw(DrawTail& tail, PerformanceTimer& timing) {
|
||||
VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, VulkanDevice::RecipeKind::Draw);
|
||||
}
|
||||
std::shared_ptr<const DrawRecipe> built;
|
||||
localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr);
|
||||
localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr, tail.shapeKey);
|
||||
phaseTiming.Phase(DrawRowGraphics);
|
||||
if (built != nullptr) attachDrawRecipe(drawKey, recipeStages, std::move(built));
|
||||
timing.Mark("draw_and_resource_release");
|
||||
return drawn();
|
||||
}
|
||||
|
||||
void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace) {
|
||||
void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t queue, const Graphics::State& graphics, const Pm4::DrawParameters& drawParameters, std::span<const Graphics::CompiledShader> stages, std::span<const Graphics::GuestMemorySnapshot> snapshots, const std::shared_ptr<const DrawRecipe>& recipe, const std::vector<std::shared_ptr<DispatchVariant>>& recipeStages, std::uint64_t drawKey, VulkanDevice::DrawTraceContext trace, std::uint64_t shape) {
|
||||
const auto* tracePtr = trace.token != 0 ? &trace : nullptr;
|
||||
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Draw);
|
||||
std::lock_guard gpuLock(GuestMemory::GpuMutex());
|
||||
@@ -1291,7 +1296,7 @@ void Driver::commitDraw(std::shared_ptr<VulkanDevice> localDevice, std::uint32_t
|
||||
VulkanDevice::NoteRecipe(VulkanDevice::RecipeEvent::Restart, VulkanDevice::RecipeKind::Draw);
|
||||
}
|
||||
std::shared_ptr<const DrawRecipe> built;
|
||||
localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr);
|
||||
localDevice->Draw(graphics, drawParameters, stages, snapshots, recipeStages.empty() ? nullptr : &built, tracePtr, shape);
|
||||
if (built != nullptr) attachDrawRecipe(drawKey, recipeStages, std::move(built));
|
||||
}
|
||||
|
||||
|
||||
@@ -65,6 +65,15 @@ std::size_t Driver::drawCacheEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
std::uint64_t Driver::DrawCacheBytes() {
|
||||
static const std::uint64_t bytes = [] {
|
||||
const char* text = std::getenv("APS5_DRAW_CACHE_BYTES");
|
||||
const auto parsed = text != nullptr ? std::strtoull(text, nullptr, 10) : 0ull;
|
||||
return parsed != 0 ? static_cast<std::uint64_t>(parsed) : std::uint64_t{256} << 20u;
|
||||
}();
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void Driver::accountDrawVariant(const DispatchVariant& variant, bool added) {
|
||||
if (added) {
|
||||
++drawCacheVariants;
|
||||
@@ -180,7 +189,8 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<Disp
|
||||
drawOrder.splice(drawOrder.begin(), drawOrder, replacement->order);
|
||||
found->second = std::move(replacement);
|
||||
}
|
||||
while (drawCache.size() > drawCacheEntries()) {
|
||||
while (drawCache.size() > drawCacheEntries() || (drawCache.size() > 1 && drawCacheVariantBytes > DrawCacheBytes())) {
|
||||
const bool overBytes = drawCache.size() <= drawCacheEntries();
|
||||
const auto last = drawCache.find(drawOrder.back());
|
||||
for (const auto& variants : last->second->stages) {
|
||||
for (const auto& variant : variants) accountDrawVariant(*variant, false);
|
||||
@@ -191,6 +201,10 @@ void Driver::insertDrawEntry(std::uint64_t key, std::vector<std::shared_ptr<Disp
|
||||
drawCache.erase(last);
|
||||
++drawCacheEvictions;
|
||||
++counters.evictions;
|
||||
if (overBytes) {
|
||||
++drawCacheByteEvictions;
|
||||
++counters.byteEvictions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ void Driver::reportDrawCache(DrawEntryCounters& counters, bool profile) {
|
||||
char line[1536];
|
||||
std::uint64_t misses = 0;
|
||||
for (const auto value : counters.misses) misses += value;
|
||||
std::snprintf(line, sizeof(line), "[draw-cache] 10 s: %llu lookups, %llu hits (%.1f%%), %llu stage misses, %llu without an entry, %llu data-only hits (stages: %llu reused, %llu compiled; %llu verified); no entry: new registers %llu, user words/pointers only %llu, evicted %llu, seen but never inserted %llu; stage misses: front differing %llu, fragment differing %llu, other differing %llu, layout %llu, gate %llu, stage count %llu; differing captures: same runs %llu (%.1f words changed each), other runs %llu; %zu entries, %llu evicted\n", count(counters.lookups), count(counters.hits), counters.lookups != 0 ? 100.0 * static_cast<double>(counters.hits) / static_cast<double>(counters.lookups) : 0.0, count(misses), count(counters.absent), count(counters.dataHits), count(counters.dataStagesReused), count(counters.dataStagesCompiled), count(counters.dataVerified), count(counters.absentNewRegisters), count(counters.absentUserWords), count(counters.absentEvicted), count(counters.absentNeverInserted), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.differingSameRuns), counters.differingSameRuns != 0 ? static_cast<double>(counters.differingWords) / static_cast<double>(counters.differingSameRuns) : 0.0, count(counters.differingRunsChanged), drawCache.size(), count(counters.evictions));
|
||||
std::snprintf(line, sizeof(line), "[draw-cache] 10 s: %llu lookups, %llu hits (%.1f%%), %llu stage misses, %llu without an entry, %llu data-only hits (stages: %llu reused, %llu compiled; %llu verified); no entry: new registers %llu, user words/pointers only %llu, evicted %llu, seen but never inserted %llu; stage misses: front differing %llu, fragment differing %llu, other differing %llu, layout %llu, gate %llu, stage count %llu; differing captures: same runs %llu (%.1f words changed each), other runs %llu; %zu entries, %llu evicted (%llu over the byte budget)\n", count(counters.lookups), count(counters.hits), counters.lookups != 0 ? 100.0 * static_cast<double>(counters.hits) / static_cast<double>(counters.lookups) : 0.0, count(misses), count(counters.absent), count(counters.dataHits), count(counters.dataStagesReused), count(counters.dataStagesCompiled), count(counters.dataVerified), count(counters.absentNewRegisters), count(counters.absentUserWords), count(counters.absentEvicted), count(counters.absentNeverInserted), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.differingSameRuns), counters.differingSameRuns != 0 ? static_cast<double>(counters.differingWords) / static_cast<double>(counters.differingSameRuns) : 0.0, count(counters.differingRunsChanged), drawCache.size(), count(counters.evictions), count(counters.byteEvictions));
|
||||
text += line;
|
||||
std::snprintf(line, sizeof(line), "[draw-cache] register-key miss component changes (10 s): stable registers %llu, user data %llu, auxiliary registers %llu, device %llu, first shader set %llu, unchanged components %llu\n", count(counters.registerKeyMissChanges[0]), count(counters.registerKeyMissChanges[1]), count(counters.registerKeyMissChanges[2]), count(counters.registerKeyMissChanges[3]), count(counters.registerKeyMissChanges[4]), count(counters.registerKeyMissChanges[5]));
|
||||
text += line;
|
||||
@@ -47,6 +47,11 @@ DrawEntryCounters Driver::DrawCacheCounters() {
|
||||
return drawEntryCounters;
|
||||
}
|
||||
|
||||
Driver::DispatchCacheFootprint Driver::DrawCacheSize() {
|
||||
std::lock_guard cacheLock(drawCacheMutex);
|
||||
return {drawCache.size(), drawCacheVariantBytes};
|
||||
}
|
||||
|
||||
OverlayDriverCounters Driver::OverlayCounters() {
|
||||
OverlayDriverCounters counters{flipsCounted.load(std::memory_order_relaxed), std::nullopt};
|
||||
std::unique_lock cacheLock(drawCacheMutex, std::try_to_lock);
|
||||
@@ -70,7 +75,7 @@ std::string Driver::profileDrawCacheLines(const DrawEntryCounters& counters) {
|
||||
std::snprintf(item, sizeof(item), "%s%llu", rank == 0 ? "" : " / ", count(counters.variantHitsByRank[rank]));
|
||||
ranks += item;
|
||||
}
|
||||
append("[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu shape updates, %llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB), %llu evictions in total, %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast<double>(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast<double>(counters.variantsCompared) / static_cast<double>(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.shapeUpdates), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast<double>(drawCacheVariantBytes) / (1024.0 * 1024.0), count(drawCacheEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches));
|
||||
append("[draw-cache] %llu lookups (10 s): %llu no entry, %llu validated in %.1f us each (key, lookup and every stage): %llu hits (every stage equal; stage hits by variant rank 1..k %s), misses by reason: front stage differing %llu, fragment differing %llu, other stage differing %llu, layout (push offset) %llu, gate %llu, stage count %llu; %llu stage validations (%llu equal, %.2f variants compared each); %llu inserts (%llu shape updates, %llu variants inserted, %llu evicted beyond k, %llu already present, %llu unstable), %zu entries (%llu variants, ~%.1f MiB of a %.1f MiB budget), %llu evictions in total (%llu over the byte budget), %llu LRU moves; verify: %llu hits captured again, %llu stages differed\n", count(counters.lookups), count(counters.absent), count(validated), validated != 0 ? counters.validateUs / static_cast<double>(validated) : 0.0, count(counters.hits), ranks.c_str(), miss(DrawMiss::FrontDiffering), miss(DrawMiss::FragmentDiffering), miss(DrawMiss::OtherDiffering), miss(DrawMiss::Layout), miss(DrawMiss::Gate), miss(DrawMiss::Stages), count(counters.stageValidations), count(counters.stageEqual), counters.stageValidations != 0 ? static_cast<double>(counters.variantsCompared) / static_cast<double>(counters.stageValidations) : 0.0, count(counters.inserts), count(counters.shapeUpdates), count(counters.variantsInserted), count(counters.variantsEvicted), count(counters.present), count(counters.unstable), drawCache.size(), count(drawCacheVariants), static_cast<double>(drawCacheVariantBytes) / (1024.0 * 1024.0), static_cast<double>(DrawCacheBytes()) / (1024.0 * 1024.0), count(drawCacheEvictions), count(drawCacheByteEvictions), count(counters.touches), count(counters.verifyHits), count(counters.verifyMismatches));
|
||||
append("[draw-cache] register key (10 s): %llu lookups, %llu hits, key build %.1f us each; decode skipped %llu, partial (state/pixel/programs from the entry) %llu; facade log != table %llu; verify: %llu decodes compared, %llu differed\n", count(counters.registerKeyLookups), count(counters.registerKeyHits), counters.registerKeyLookups != 0 ? counters.keyUs / static_cast<double>(counters.registerKeyLookups) : 0.0, count(counters.decodeSkipped), count(counters.decodePartial), count(counters.facadeMismatches), count(counters.verifyDecodes), count(counters.verifyDecodeMismatches));
|
||||
append("[draw-cache] stage data hits (10 s): %llu stages (%llu words refreshed), verified %llu; stage inserts with data positions %llu (%.1f positions each)\n", count(counters.stageDataHits), count(counters.stageDataWords), count(counters.stageDataVerified), count(counters.stageDataInserts), counters.stageDataInserts != 0 ? static_cast<double>(counters.stageDataPositions) / static_cast<double>(counters.stageDataInserts) : 0.0);
|
||||
return text;
|
||||
|
||||
@@ -73,6 +73,9 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
|
||||
{0, 0, pushOffset, Graphics::StagePushLimit(graphics.stages, pushOffset, graphics.depthRemap.mode != 0u) - pushOffset % Graphics::PipelinePushSlotBytes},
|
||||
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, Graphics::StageMeshConfiguration(graphics.stages, program.binary.stage), graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt}
|
||||
};
|
||||
if (graphics.depthRemap.mode != 0u && pushOffset < Graphics::PipelinePushSlotBytes) {
|
||||
request.layout.pushConstantSizeBytes = std::min(request.layout.pushConstantSizeBytes, ShaderRecompiler::DepthRemapPushOffsetBytes - pushOffset);
|
||||
}
|
||||
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
|
||||
auto invocation = [&] {
|
||||
try {
|
||||
|
||||
@@ -74,6 +74,8 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
|
||||
mix(pixel.orderedPixelShader);
|
||||
mix(pixel.stencilRefExportEnable);
|
||||
mix(pixel.stencilOperationValueExport);
|
||||
mix(std::bit_cast<std::uint32_t>(pixel.depthExportMin));
|
||||
mix(std::bit_cast<std::uint32_t>(pixel.depthExportMax));
|
||||
for (const auto value : pixel.targetOutputMode) mix(value);
|
||||
for (const auto value : pixel.targetExportMapping) mix(value);
|
||||
for (const auto value : pixel.targetExportPacking) mix(static_cast<std::uint64_t>(value));
|
||||
|
||||
@@ -255,7 +255,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
|
||||
if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false;
|
||||
const auto& p = a.pixel;
|
||||
const auto& q = b.pixel;
|
||||
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.stencilOperationValueExport != q.stencilOperationValueExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
|
||||
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.depthExportMin != q.depthExportMin || p.depthExportMax != q.depthExportMax || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.stencilOperationValueExport != q.stencilOperationValueExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
|
||||
if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false;
|
||||
for (std::size_t i = 0; i < a.programs.size(); ++i) {
|
||||
const auto& x = a.programs[i];
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include <atomic>
|
||||
@@ -407,6 +408,7 @@ void Driver::execute(const Submission& submission) {
|
||||
}
|
||||
if (!flipInterruptDeferred) deliverFlipEopInterrupt(submission.queue);
|
||||
++flipsCounted;
|
||||
Graphics::NoteHostImportFrame();
|
||||
if (batchesAtFlip != 0) flipSerial = batchesAtFlip;
|
||||
flipBatchesUnsignaled += unsignaledAtFlip;
|
||||
if (Graphics::AbConfigured()) {
|
||||
|
||||
@@ -243,15 +243,29 @@ void Driver::run(std::uint32_t id) noexcept {
|
||||
pending.pop_front();
|
||||
if (APS5_ENABLE_TIMING_LOG) submission.dequeuedAt = std::chrono::steady_clock::now();
|
||||
if (id == 0) queue0Executing = submission.suspend ? 0 : submission.received;
|
||||
struct OrderHold {
|
||||
std::atomic<std::uint32_t>& holders;
|
||||
explicit OrderHold(std::atomic<std::uint32_t>& holders) : holders(holders) { holders.fetch_add(1, std::memory_order_acq_rel); }
|
||||
~OrderHold() { holders.fetch_sub(1, std::memory_order_acq_rel); }
|
||||
OrderHold(const OrderHold&) = delete;
|
||||
OrderHold& operator=(const OrderHold&) = delete;
|
||||
};
|
||||
if (id == 0 && !submission.suspend) {
|
||||
{
|
||||
OrderHold hold{orderHolders};
|
||||
const auto released = [&] { return failure || stopping || !waitFreeBefore(submission.received); };
|
||||
StallWatch stall(id);
|
||||
while (!released() && !poll(released)) stall.Check("order behind other queues for event", submission.received);
|
||||
}
|
||||
rethrowFailure();
|
||||
}
|
||||
if (id != 0) {
|
||||
worker.executingWaitFree = submission.waitFree ? submission.received : 0;
|
||||
worker.executingWaits = !submission.waitFree && !submission.suspend;
|
||||
}
|
||||
if (submission.waitFree) {
|
||||
{
|
||||
struct OrderHold {
|
||||
std::atomic<std::uint32_t>& holders;
|
||||
explicit OrderHold(std::atomic<std::uint32_t>& holders) : holders(holders) { holders.fetch_add(1, std::memory_order_acq_rel); }
|
||||
~OrderHold() { holders.fetch_sub(1, std::memory_order_acq_rel); }
|
||||
OrderHold(const OrderHold&) = delete;
|
||||
OrderHold& operator=(const OrderHold&) = delete;
|
||||
} hold{orderHolders};
|
||||
OrderHold hold{orderHolders};
|
||||
const auto released = [&] { return failure || stopping || orderReleased(id, submission.received); };
|
||||
StallWatch stall(id);
|
||||
while (!released() && !poll(released)) stall.Check("order behind queue 0 for event", submission.received);
|
||||
@@ -279,6 +293,10 @@ void Driver::run(std::uint32_t id) noexcept {
|
||||
forgetUnfinishedWrites(workers.at(id), submission);
|
||||
}
|
||||
if (id == 0) queue0Executing = 0;
|
||||
if (id != 0) {
|
||||
workers.at(id).executingWaitFree = 0;
|
||||
workers.at(id).executingWaits = false;
|
||||
}
|
||||
|
||||
notify = idleWaiters != 0 || orderHolders.load(std::memory_order_acquire) != 0;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp"
|
||||
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
|
||||
@@ -440,6 +441,7 @@ void Driver::Submit(const Packet* packet, std::uint32_t queue, bool cpuFlip) {
|
||||
++flipsCounted;
|
||||
submission.flips.begin()->second->GpuReady(frame);
|
||||
lock.unlock();
|
||||
Graphics::NoteHostImportFrame();
|
||||
AgcDriverDeliverEopInterrupt(0);
|
||||
return;
|
||||
}
|
||||
@@ -537,6 +539,18 @@ bool Driver::queue0Before(std::uint64_t received) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Driver::waitFreeBefore(std::uint64_t received) const {
|
||||
for (const auto& [queue, worker] : workers) {
|
||||
if (queue == 0 || worker.executingWaits) continue;
|
||||
if (worker.executingWaitFree != 0 && worker.executingWaitFree < received) return true;
|
||||
for (const auto& pending : worker.pending) {
|
||||
if (!pending.waitFree || pending.received >= received) break;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Driver::orderReleased(std::uint32_t queue, std::uint64_t received) const {
|
||||
if (!queue0Before(received)) return true;
|
||||
const auto awaited = queue0Awaited.load(std::memory_order_acquire);
|
||||
|
||||
@@ -1802,7 +1802,10 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
|
||||
const auto wave = Graphics::StageWaveSize(decoded.state.stages, program.binary.stage);
|
||||
pushOffset = Graphics::StagePushOffset(pushOffset, program.binary.stage, fixedSlots);
|
||||
const auto offset = pushOffset;
|
||||
const auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes;
|
||||
auto room = (fixedSlots && fragment ? Graphics::PipelinePushSlotBytes : capacity) - pushOffset % Graphics::PipelinePushSlotBytes;
|
||||
if (decoded.state.depthRemap.mode != 0u && offset < Graphics::PipelinePushSlotBytes) {
|
||||
room = std::min(room, ShaderRecompiler::DepthRemapPushOffsetBytes - offset);
|
||||
}
|
||||
if (deferUnprepared && DeferFailedRegistration(*program.snapshot)) {
|
||||
prepared.push_back({program.snapshot, {program.codeOffset, nullptr}, program.binary.stage, offset, nullptr});
|
||||
continue;
|
||||
@@ -1818,6 +1821,7 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
|
||||
}
|
||||
if (vertex) {
|
||||
vertex->paClVsOutCntl = PositionExportLayout(program.snapshot.get(), program.codeOffset, program.binary.code, decoded.state.paClVsOutCntl, program.tailContinuation.get()) | decoded.state.layerExports;
|
||||
vertex->depthRemapMode = decoded.state.depthRemap.mode;
|
||||
Graphics::ApplyPointSize(decoded.state, *vertex);
|
||||
}
|
||||
ShaderRecompiler::RecompileRequest request{program.binary, {wave, program.firstUserSgpr, program.userData, {}, fragment ? std::optional(decoded.pixel) : std::nullopt, vertex, memory, RegisteredFloatMode(*program.snapshot)}, target, {0, 0, offset, room}, ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, Graphics::StageMeshConfiguration(decoded.state.stages, program.binary.stage), decoded.state.stages.tessellation, {}}};
|
||||
|
||||
@@ -135,9 +135,14 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
|
||||
if (bump) bumpEpoch(&EpochBumps::waits);
|
||||
}
|
||||
} epochPoint;
|
||||
const auto storedByOwnLabel = [&] {
|
||||
std::uint32_t writer = queue;
|
||||
return !requireMemory && storedSince(packet, awaited, awaitedBytes, received, &writer) && writer == queue;
|
||||
};
|
||||
GuestMemory::CheckRange(reinterpret_cast<const void*>(awaited), awaitedBytes, awaitedBytes);
|
||||
if (Pm4::WaitSatisfiedUnchecked(packet)) {
|
||||
++outcomes.atEntry;
|
||||
if (storedByOwnLabel()) epochPoint.bump = false;
|
||||
return;
|
||||
}
|
||||
if (heldAtSubmit) return;
|
||||
@@ -406,6 +411,7 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
|
||||
}
|
||||
}
|
||||
++outcomes.polled;
|
||||
if (storedByOwnLabel()) epochPoint.bump = false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/GuestArena.hpp"
|
||||
#include "prx/libc/include/GuestWriteWatch.hpp"
|
||||
#include "prx/libc/include/HostThreadLocal.hpp"
|
||||
#include "prx/libc/include/HostThreadSlot.hpp"
|
||||
#include <mutex>
|
||||
#include <array>
|
||||
@@ -2677,13 +2678,74 @@ void CopyGuestBytes(void* destination, const void* source, std::size_t bytes) {
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef _WIN32
|
||||
bool readPages(std::byte* destination, std::uint64_t source, std::size_t bytes) {
|
||||
SIZE_T copied = 0;
|
||||
if (ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<const void*>(source), destination, bytes, &copied) != 0) {
|
||||
require(copied == bytes, "short committed guest memory read");
|
||||
return true;
|
||||
}
|
||||
const auto error = GetLastError();
|
||||
require(error == ERROR_PARTIAL_COPY || error == ERROR_NOACCESS || error == ERROR_INVALID_ADDRESS, "cannot read committed guest memory");
|
||||
return false;
|
||||
}
|
||||
|
||||
template <class Visit>
|
||||
void visitReadablePages(std::byte* destination, std::uint64_t source, std::size_t bytes, Visit&& visit) {
|
||||
if (readPages(destination, source, bytes)) {
|
||||
visit(std::size_t{0}, bytes, true);
|
||||
return;
|
||||
}
|
||||
ForgetPages(static_cast<std::uintptr_t>(source), bytes);
|
||||
for (std::size_t offset = 0; offset < bytes;) {
|
||||
const auto cursor = source + offset;
|
||||
const auto chunk = std::min<std::size_t>(bytes - offset, PageBytes - static_cast<std::size_t>(cursor % PageBytes));
|
||||
MEMORY_BASIC_INFORMATION memory{};
|
||||
require(VirtualQuery(reinterpret_cast<const void*>(cursor), &memory, sizeof(memory)) == sizeof(memory), "cannot query guest memory after a failed committed read");
|
||||
const bool readable = memory.State == MEM_COMMIT && (memory.Protect & (PAGE_GUARD | PAGE_NOACCESS)) == 0 && readableProtection(memory.Protect & 0xffu);
|
||||
visit(offset, chunk, readable && readPages(destination + offset, cursor, chunk));
|
||||
offset += chunk;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void copyCommittedBytes(std::byte* destination, std::uint64_t source, std::size_t bytes) {
|
||||
#ifdef _WIN32
|
||||
visitReadablePages(destination, source, bytes, [&](std::size_t offset, std::size_t count, bool read) {
|
||||
if (!read) std::memset(destination + offset, 0, count);
|
||||
});
|
||||
#else
|
||||
std::memcpy(destination, reinterpret_cast<const void*>(source), bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool equalsCommittedBytes(std::uint64_t address, const std::byte* expected, std::size_t bytes) {
|
||||
#ifdef _WIN32
|
||||
constexpr std::size_t ChunkBytes = 64 * PageBytes;
|
||||
struct CompareChunkStorage {};
|
||||
auto& current = HostThreadLocal<std::vector<std::byte>, CompareChunkStorage>();
|
||||
if (current.size() < ChunkBytes) current.resize(ChunkBytes);
|
||||
for (std::size_t done = 0; done < bytes; done += ChunkBytes) {
|
||||
const auto count = std::min(ChunkBytes, bytes - done);
|
||||
bool equal = true;
|
||||
visitReadablePages(current.data(), address + done, count, [&](std::size_t offset, std::size_t length, bool read) {
|
||||
if (read && equal && std::memcmp(current.data() + offset, expected + done + offset, length) != 0) equal = false;
|
||||
});
|
||||
if (!equal) return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return std::memcmp(reinterpret_cast<const void*>(address), expected, bytes) == 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void copyCommitted(std::uint64_t address, std::span<std::byte> destination) {
|
||||
if (Accessible(reinterpret_cast<const void*>(address), destination.size())) {
|
||||
std::memcpy(destination.data(), reinterpret_cast<const void*>(address), destination.size());
|
||||
copyCommittedBytes(destination.data(), address, destination.size());
|
||||
return;
|
||||
}
|
||||
std::memset(destination.data(), 0, destination.size());
|
||||
for (const auto& [begin, end] : CommittedRanges(address, destination.size())) std::memcpy(destination.data() + (begin - address), reinterpret_cast<const void*>(begin), static_cast<std::size_t>(end - begin));
|
||||
for (const auto& [begin, end] : CommittedRanges(address, destination.size())) copyCommittedBytes(destination.data() + (begin - address), begin, static_cast<std::size_t>(end - begin));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2714,9 +2776,9 @@ bool EqualsCommitted(std::uint64_t address, std::span<const std::byte> bytes) {
|
||||
bool EqualsCommittedUnsynced(std::uint64_t address, std::span<const std::byte> bytes) {
|
||||
if (bytes.empty()) return true;
|
||||
const TimedAccess timed(CounterCompare, bytes.size());
|
||||
if (Accessible(reinterpret_cast<const void*>(address), bytes.size())) return std::memcmp(reinterpret_cast<const void*>(address), bytes.data(), bytes.size()) == 0;
|
||||
if (Accessible(reinterpret_cast<const void*>(address), bytes.size())) return equalsCommittedBytes(address, bytes.data(), bytes.size());
|
||||
for (const auto& [begin, end] : CommittedRanges(address, bytes.size())) {
|
||||
if (std::memcmp(reinterpret_cast<const void*>(begin), bytes.data() + (begin - address), static_cast<std::size_t>(end - begin)) != 0) return false;
|
||||
if (!equalsCommittedBytes(begin, bytes.data() + (begin - address), static_cast<std::size_t>(end - begin))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
#include <psapi.h>
|
||||
#else
|
||||
#include <sys/resource.h>
|
||||
#ifdef __APPLE__
|
||||
#include <mach/mach.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace AgcDriver {
|
||||
@@ -616,6 +619,11 @@ std::optional<std::uint64_t> ReadProcessPrivateBytes() {
|
||||
counters.cb = sizeof(counters);
|
||||
if (K32GetProcessMemoryInfo(GetCurrentProcess(), reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&counters), sizeof(counters)) == 0) return std::nullopt;
|
||||
return static_cast<std::uint64_t>(counters.PrivateUsage);
|
||||
#elif defined(__APPLE__)
|
||||
task_vm_info_data_t info{};
|
||||
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
|
||||
if (task_info(mach_task_self(), TASK_VM_INFO, reinterpret_cast<task_info_t>(&info), &count) != KERN_SUCCESS || info.phys_footprint == 0) return std::nullopt;
|
||||
return static_cast<std::uint64_t>(info.phys_footprint);
|
||||
#else
|
||||
std::FILE* status = std::fopen("/proc/self/status", "r");
|
||||
if (status == nullptr) return std::nullopt;
|
||||
|
||||
@@ -1034,13 +1034,12 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
|
||||
case 0: queue.ClearContext(); break;
|
||||
case 1: case 3:
|
||||
require(!queue.savedContext.has_value(), "context state is already pushed");
|
||||
queue.savedContext = queue.context;
|
||||
queue.PushContext();
|
||||
if (packet[1] == 3) queue.ClearContext();
|
||||
break;
|
||||
case 2:
|
||||
require(queue.savedContext.has_value(), "context state has not been pushed");
|
||||
queue.context = std::move(*queue.savedContext);
|
||||
queue.savedContext.reset();
|
||||
queue.PopContext();
|
||||
break;
|
||||
}
|
||||
APS5_LOG_OUT_DEBUG("CONTEXT_STATE done operation=%u targetMaskAfter=0x%x shaderMaskAfter=0x%x", packet[1], queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u);
|
||||
|
||||
@@ -71,6 +71,7 @@ ShaderDeviceProfile::ShaderDeviceProfile(const ShaderRecompiler::SpirvTarget& ta
|
||||
checkFeature(spv::CapabilityDrawParameters, drawParameters != nullptr && drawParameters->shaderDrawParameters == VK_TRUE, "shaderDrawParameters");
|
||||
checkFeature(spv::CapabilityShaderClockKHR, clock != nullptr && clock->shaderSubgroupClock == VK_TRUE && clock->shaderDeviceClock == VK_TRUE, "shader clock");
|
||||
Require(!target.fragmentShaderBarycentricEnabled || barycentricEnabled, "shader device profile exposes disabled barycentrics");
|
||||
Require(!target.fragmentBarycentricBuiltins || barycentricEnabled, "shader device profile exposes disabled barycentric built-ins");
|
||||
Require(!target.mesh || meshEnabled, "shader device profile exposes disabled mesh shaders");
|
||||
Require(!target.nonConstantImageOffsets || (maintenance8 != nullptr && maintenance8->maintenance8 == VK_TRUE), "shader device profile exposes disabled maintenance8");
|
||||
Require(!target.tessellation || core.tessellationShader == VK_TRUE, "shader device profile exposes disabled tessellation");
|
||||
|
||||
@@ -179,6 +179,10 @@ ShaderMemory::~ShaderMemory() {
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t ShaderMemory::Bytes() const {
|
||||
return sizeof(ShaderMemory) + initial.capacity() * sizeof(initial[0]) + pages.capacity() * sizeof(pages[0]) + pages.size() * sizeof(Page) + supplied.capacity() * sizeof(Supplied) + spilled.capacity() * sizeof(spilled[0]);
|
||||
}
|
||||
|
||||
void ShaderMemory::WordMask::SetRun(std::size_t first, std::size_t count) {
|
||||
while (count != 0) {
|
||||
const auto shift = first % 64u;
|
||||
@@ -235,6 +239,7 @@ ShaderMemory::Page& ShaderMemory::addPage(std::size_t position, std::uint64_t ba
|
||||
pool.pop_back();
|
||||
}
|
||||
owned->valid.Reset();
|
||||
owned->raw.Reset();
|
||||
owned->read.Reset();
|
||||
owned->recent.Reset();
|
||||
owned->wordwise = false;
|
||||
@@ -299,12 +304,38 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
|
||||
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);
|
||||
}
|
||||
const auto spanNeedsSync = [](PendingWrite spanPolicy) {
|
||||
return spanPolicy == PendingWrite::Sync || spanPolicy == PendingWrite::VerifyRaw || spanPolicy == PendingWrite::VerifyKnownValue;
|
||||
};
|
||||
const auto spanInvalidatesCache = [&](PendingWrite spanPolicy) {
|
||||
return spanNeedsSync(spanPolicy) || spanPolicy == PendingWrite::KnownValue;
|
||||
};
|
||||
const auto checkSpanBytes = std::max<std::size_t>(self.descriptorSpanBytes, sizeof(*value));
|
||||
if (page.wordwise && page.valid.Test(index) &&
|
||||
(self.forceSync != 0u ? self.pendingWrite(address, checkSpanBytes, {}) != PendingWrite::None
|
||||
: (spanInvalidatesCache(self.pendingWrite(address, checkSpanBytes, {})) ||
|
||||
(checkSpanBytes > sizeof(*value) && spanInvalidatesCache(self.pendingWrite(address, sizeof(*value), {})))))) {
|
||||
page.valid.Clear(index);
|
||||
page.raw.Clear(index);
|
||||
}
|
||||
if (!page.valid.Test(index)) {
|
||||
std::uint32_t word = 0;
|
||||
auto policy = PendingWrite::Sync;
|
||||
bool forcedRaw = false;
|
||||
if (page.wordwise) {
|
||||
policy = self.pendingWrite(address, sizeof(word), std::as_writable_bytes(std::span(&word, 1)));
|
||||
if (policy != PendingWrite::None && policy != PendingWrite::Sync && !GuestMemory::Accessible(reinterpret_cast<const void*>(address), sizeof(word))) policy = PendingWrite::Sync;
|
||||
if (self.descriptorSpanBytes > sizeof(word) &&
|
||||
(self.forceSync != 0u ? self.pendingWrite(address, self.descriptorSpanBytes, {}) != PendingWrite::None
|
||||
: spanNeedsSync(self.pendingWrite(address, self.descriptorSpanBytes, {})))) {
|
||||
policy = PendingWrite::Sync;
|
||||
forcedRaw = self.forceSync != 0u && !spanNeedsSync(self.pendingWrite(address, self.descriptorSpanBytes, {}));
|
||||
} else {
|
||||
policy = self.pendingWrite(address, sizeof(word), std::as_writable_bytes(std::span(&word, 1)));
|
||||
if (self.forceSync != 0u && policy != PendingWrite::None) {
|
||||
forcedRaw = !spanNeedsSync(policy);
|
||||
policy = PendingWrite::Sync;
|
||||
}
|
||||
if (policy != PendingWrite::None && policy != PendingWrite::Sync && !GuestMemory::Accessible(reinterpret_cast<const void*>(address), sizeof(word))) policy = PendingWrite::Sync;
|
||||
}
|
||||
}
|
||||
// A read is evidence only when the hook waited for unfinished GPU work across it.
|
||||
const auto hookWaits = [&] { return self.hookWaits != nullptr ? self.hookWaits() : 0; };
|
||||
@@ -313,38 +344,44 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
|
||||
else ++CaptureTotals().observationsSkipped;
|
||||
};
|
||||
const auto fetchStarted = CaptureProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
bool waited = false;
|
||||
if (policy == PendingWrite::KnownValue || policy == PendingWrite::VerifyKnownValue) {
|
||||
// The query stored the known dword in `word`.
|
||||
++CaptureTotals().wordsKnown;
|
||||
if (policy == PendingWrite::VerifyKnownValue) {
|
||||
std::uint32_t waited = 0;
|
||||
std::uint32_t verifiedWord = 0;
|
||||
const auto waitsBefore = hookWaits();
|
||||
GuestMemory::Read(address, std::as_writable_bytes(std::span(&waited, 1)), alignof(std::uint32_t));
|
||||
GuestMemory::Read(address, std::as_writable_bytes(std::span(&verifiedWord, 1)), alignof(std::uint32_t));
|
||||
++CaptureTotals().wordsKnownVerified;
|
||||
if (waited != word) ++CaptureTotals().wordsKnownMismatches;
|
||||
if (self.observe != nullptr) report(waitsBefore, waited == word);
|
||||
word = waited;
|
||||
if (verifiedWord != word) ++CaptureTotals().wordsKnownMismatches;
|
||||
if (self.observe != nullptr) report(waitsBefore, verifiedWord == word);
|
||||
waited = page.wordwise && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true);
|
||||
word = verifiedWord;
|
||||
}
|
||||
} else if (policy == PendingWrite::Raw || policy == PendingWrite::VerifyRaw) {
|
||||
GuestMemory::GuardRead(address, sizeof(word));
|
||||
std::memcpy(&word, reinterpret_cast<const void*>(address), sizeof(word));
|
||||
++CaptureTotals().wordsRaw;
|
||||
if (policy == PendingWrite::VerifyRaw) {
|
||||
std::uint32_t waited = 0;
|
||||
std::uint32_t verifiedWord = 0;
|
||||
const auto waitsBefore = hookWaits();
|
||||
GuestMemory::Read(address, std::as_writable_bytes(std::span(&waited, 1)), alignof(std::uint32_t));
|
||||
GuestMemory::Read(address, std::as_writable_bytes(std::span(&verifiedWord, 1)), alignof(std::uint32_t));
|
||||
++CaptureTotals().wordsVerified;
|
||||
if (waited != word) ++CaptureTotals().wordMismatches;
|
||||
if (self.observe != nullptr) report(waitsBefore, waited == word);
|
||||
word = waited;
|
||||
if (verifiedWord != word) ++CaptureTotals().wordMismatches;
|
||||
if (self.observe != nullptr) report(waitsBefore, verifiedWord == word);
|
||||
waited = page.wordwise && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true);
|
||||
word = verifiedWord;
|
||||
}
|
||||
} else {
|
||||
// On a word-wise page the bytes before the hook's wait are kept for the observer.
|
||||
const bool observed = page.wordwise && policy == PendingWrite::Sync && self.observe != nullptr && GuestMemory::Accessible(reinterpret_cast<const void*>(address), sizeof(word));
|
||||
std::uint32_t before = 0;
|
||||
if (observed) std::memcpy(&before, reinterpret_cast<const void*>(address), sizeof(before));
|
||||
const auto waitsBefore = observed ? hookWaits() : 0;
|
||||
const auto waitsBefore = page.wordwise ? hookWaits() : 0;
|
||||
const auto started = page.wordwise ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
if (page.wordwise && policy == PendingWrite::Sync && self.descriptorSpanBytes > sizeof(word)) {
|
||||
GuestMemory::FlushGpuWrites(address, self.descriptorSpanBytes);
|
||||
}
|
||||
GuestMemory::Read(address, std::as_writable_bytes(std::span(&word, 1)), alignof(std::uint32_t));
|
||||
if (page.wordwise) {
|
||||
const auto nanoseconds = static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - started).count());
|
||||
@@ -352,12 +389,16 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
|
||||
++CaptureTotals().wordWaits;
|
||||
CaptureTotals().wordWaitNanoseconds += nanoseconds;
|
||||
}
|
||||
waited = policy != PendingWrite::None && (self.hookWaits != nullptr ? self.hookWaits() != waitsBefore : true);
|
||||
}
|
||||
if (observed) report(waitsBefore, before == word);
|
||||
}
|
||||
if ((waited || (page.wordwise && policy == PendingWrite::Sync && !forcedRaw)) && self.invalidateRawPages()) ++self.rawInvalidations;
|
||||
if (CaptureProfiled()) CaptureTotals().wordReadNanoseconds += NanosecondsSince(fetchStarted);
|
||||
page.words[index] = word;
|
||||
page.valid.Set(index);
|
||||
if (page.wordwise && (policy == PendingWrite::Raw || policy == PendingWrite::KnownValue || (forcedRaw && !waited))) page.raw.Set(index);
|
||||
else page.raw.Clear(index);
|
||||
}
|
||||
page.read.Set(index);
|
||||
page.recent.Set(index);
|
||||
@@ -365,6 +406,17 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ShaderMemory::invalidateRawPages() {
|
||||
bool invalidated = false;
|
||||
for (auto& [pageBase, existing] : pages) {
|
||||
if (!existing->wordwise || existing->raw.None()) continue;
|
||||
existing->valid.ClearMask(existing->raw);
|
||||
existing->raw.Reset();
|
||||
invalidated = true;
|
||||
}
|
||||
return invalidated;
|
||||
}
|
||||
|
||||
bool ShaderMemory::overlapsInitial(std::uint64_t begin, std::uint64_t end) const {
|
||||
if (initial.empty()) return false;
|
||||
const auto after = std::lower_bound(initial.begin(), initial.end(), begin, [](const auto& entry, std::uint64_t value) { return entry.first < value; });
|
||||
@@ -375,43 +427,96 @@ bool ShaderMemory::overlapsInitial(std::uint64_t begin, std::uint64_t end) const
|
||||
bool ShaderMemory::readWords(void* context, std::uint64_t address, std::uint32_t* values, std::uint64_t count) {
|
||||
auto& self = *static_cast<ShaderMemory*>(context);
|
||||
if (!self.supplied.empty() && count != 0) self.spillTouching(address, count > (std::numeric_limits<std::uint64_t>::max() - address) / sizeof(std::uint32_t) ? std::numeric_limits<std::uint64_t>::max() : address + count * sizeof(std::uint32_t));
|
||||
while (count != 0) {
|
||||
const auto base = address & ~static_cast<std::uint64_t>(PageBytes - 1);
|
||||
const auto index = static_cast<std::size_t>((address % PageBytes) / sizeof(std::uint32_t));
|
||||
const auto words = static_cast<std::size_t>(std::min<std::uint64_t>(count, PageWords - index));
|
||||
const auto bytes = static_cast<std::uint64_t>(words) * sizeof(std::uint32_t);
|
||||
Page* page = nullptr;
|
||||
if (address % sizeof(std::uint32_t) == 0 && address <= std::numeric_limits<std::uint64_t>::max() - bytes && !self.overlapsInitial(address, address + bytes)) page = &self.page(base);
|
||||
if (page != nullptr && page->lazy) {
|
||||
const auto end = (index + words + BlockWords - 1u) / BlockWords * BlockWords;
|
||||
auto first = index / BlockWords * BlockWords;
|
||||
while (first < end) {
|
||||
if (page->valid.TestBlock(first)) {
|
||||
first += BlockWords;
|
||||
continue;
|
||||
const auto startAddress = address;
|
||||
auto* const startValues = values;
|
||||
const auto startCount = count;
|
||||
const auto savedSpanBytes = self.descriptorSpanBytes;
|
||||
const auto savedForceSync = self.forceSync;
|
||||
struct ScopeReset {
|
||||
ShaderMemory& self;
|
||||
std::size_t spanBytes;
|
||||
std::uint32_t forceSync;
|
||||
~ScopeReset() {
|
||||
self.descriptorSpanBytes = spanBytes;
|
||||
self.forceSync = forceSync;
|
||||
}
|
||||
} scopeReset{self, savedSpanBytes, savedForceSync};
|
||||
const auto strideWords = savedSpanBytes / sizeof(std::uint32_t);
|
||||
for (std::uint32_t attempt = 0; attempt < 3u; ++attempt) {
|
||||
if (attempt == 2u) ++self.forceSync;
|
||||
address = startAddress;
|
||||
values = startValues;
|
||||
count = startCount;
|
||||
const auto invalidationsBefore = self.rawInvalidations;
|
||||
while (count != 0) {
|
||||
const auto base = address & ~static_cast<std::uint64_t>(PageBytes - 1);
|
||||
const auto index = static_cast<std::size_t>((address % PageBytes) / sizeof(std::uint32_t));
|
||||
const auto words = static_cast<std::size_t>(std::min<std::uint64_t>(count, PageWords - index));
|
||||
const auto bytes = static_cast<std::uint64_t>(words) * sizeof(std::uint32_t);
|
||||
Page* page = nullptr;
|
||||
if (address % sizeof(std::uint32_t) == 0 && address <= std::numeric_limits<std::uint64_t>::max() - bytes && !self.overlapsInitial(address, address + bytes)) page = &self.page(base);
|
||||
if (page != nullptr && page->lazy) {
|
||||
const auto end = (index + words + BlockWords - 1u) / BlockWords * BlockWords;
|
||||
auto first = index / BlockWords * BlockWords;
|
||||
while (first < end) {
|
||||
if (page->valid.TestBlock(first)) {
|
||||
first += BlockWords;
|
||||
continue;
|
||||
}
|
||||
auto last = first + BlockWords;
|
||||
while (last < end && !page->valid.TestBlock(last)) last += BlockWords;
|
||||
GuestMemory::CopyGuestBytes(&page->words[first], reinterpret_cast<const void*>(base + first * sizeof(std::uint32_t)), (last - first) * sizeof(std::uint32_t));
|
||||
page->valid.SetRun(first, last - first);
|
||||
first = last;
|
||||
}
|
||||
auto last = first + BlockWords;
|
||||
while (last < end && !page->valid.TestBlock(last)) last += BlockWords;
|
||||
GuestMemory::CopyGuestBytes(&page->words[first], reinterpret_cast<const void*>(base + first * sizeof(std::uint32_t)), (last - first) * sizeof(std::uint32_t));
|
||||
page->valid.SetRun(first, last - first);
|
||||
first = last;
|
||||
}
|
||||
}
|
||||
if (page != nullptr && (page->lazy || page->valid.All())) {
|
||||
std::memcpy(values, page->words.data() + index, bytes);
|
||||
page->read.SetRun(index, words);
|
||||
page->recent.SetRun(index, words);
|
||||
CaptureTotals().reads += words;
|
||||
} else {
|
||||
for (std::size_t word = 0; word < words; ++word) {
|
||||
if (!read(context, address + word * sizeof(std::uint32_t), values + word)) return false;
|
||||
if (page != nullptr && (page->lazy || (!page->wordwise && page->valid.All()))) {
|
||||
std::memcpy(values, page->words.data() + index, bytes);
|
||||
page->read.SetRun(index, words);
|
||||
page->recent.SetRun(index, words);
|
||||
CaptureTotals().reads += words;
|
||||
} else {
|
||||
const auto offsetWords = static_cast<std::uint64_t>(values - startValues);
|
||||
for (std::size_t word = 0; word < words; ++word) {
|
||||
if (strideWords > 1u) {
|
||||
const auto inRecord = (offsetWords + word) % strideWords;
|
||||
const auto remainingWords = std::min<std::uint64_t>(strideWords - inRecord, startCount - (offsetWords + word));
|
||||
self.descriptorSpanBytes = static_cast<std::size_t>(remainingWords * sizeof(std::uint32_t));
|
||||
}
|
||||
const bool ok = read(context, address + word * sizeof(std::uint32_t), values + word);
|
||||
self.descriptorSpanBytes = savedSpanBytes;
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
if (self.rawInvalidations != invalidationsBefore) break;
|
||||
}
|
||||
if (self.rawInvalidations != invalidationsBefore) break;
|
||||
}
|
||||
address += bytes;
|
||||
values += words;
|
||||
count -= words;
|
||||
}
|
||||
if (self.rawInvalidations == invalidationsBefore) {
|
||||
return true;
|
||||
}
|
||||
address += bytes;
|
||||
values += words;
|
||||
count -= words;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
std::uint64_t ShaderMemory::descriptorScope(void* context, std::size_t bytes) {
|
||||
auto& self = *static_cast<ShaderMemory*>(context);
|
||||
if (bytes == ShaderRecompiler::SrtDescriptorForceSync) {
|
||||
++self.forceSync;
|
||||
self.descriptorSpanBytes = 0;
|
||||
return self.rawInvalidations;
|
||||
}
|
||||
if (bytes == ShaderRecompiler::SrtDescriptorEndForceSync) {
|
||||
if (self.forceSync != 0u) --self.forceSync;
|
||||
self.descriptorSpanBytes = 0;
|
||||
return self.rawInvalidations;
|
||||
}
|
||||
self.descriptorSpanBytes = bytes;
|
||||
return self.rawInvalidations;
|
||||
}
|
||||
|
||||
bool ShaderMemory::accessible(void* context, std::uint64_t address, std::uint64_t bytes) {
|
||||
@@ -644,6 +749,7 @@ std::shared_ptr<const ShaderRecompiler::ResourceCapture> ShaderMemory::capture(c
|
||||
runtime.stampWrites = &stampWrites;
|
||||
runtime.unchangedSince = &unchangedSince;
|
||||
runtime.supplyWords = &supplyWords;
|
||||
runtime.descriptorScope = &descriptorScope;
|
||||
auto capture = invocation != nullptr ? invocation->Capture(runtime) : handle != nullptr ? ShaderRecompiler::CaptureResources(request, runtime, *handle) : ShaderRecompiler::CaptureResources(request, runtime);
|
||||
if (profile) {
|
||||
totals.captureNanoseconds += NanosecondsSince(started);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/SubmissionCapture.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
|
||||
#include <algorithm>
|
||||
#include <bit>
|
||||
#include <charconv>
|
||||
@@ -174,7 +175,7 @@ std::string_view SubmissionCapture::KindName(SubmissionKind kind) {
|
||||
|
||||
std::string_view SubmissionCapture::QueueType(std::uint32_t queue) {
|
||||
if (queue == 0) return "graphics";
|
||||
if (queue >= 0x20 && queue < 0x58) return "compute";
|
||||
if (queue >= 0x20 && queue < DriverDetail::SubmissionQueueEnd) return "compute";
|
||||
char what[64];
|
||||
std::snprintf(what, sizeof(what), "queue 0x%x is neither the graphics queue nor a compute queue", queue);
|
||||
throw std::runtime_error(std::string("submission capture: ") + what);
|
||||
|
||||
@@ -383,6 +383,7 @@ struct VulkanDevice::State {
|
||||
bool fragmentShaderBarycentric = false;
|
||||
bool shaderTerminateInvocation = false;
|
||||
bool fusedExactArithmetic = false;
|
||||
bool fragmentBarycentricBuiltins = false;
|
||||
bool geometryShader = false;
|
||||
bool largePoints = false;
|
||||
bool sampleRateShading = false;
|
||||
@@ -404,6 +405,7 @@ struct VulkanDevice::State {
|
||||
bool exactRobustLoads = false;
|
||||
bool subgroupControlBarriers = true;
|
||||
bool workgroupScopeFences = true;
|
||||
bool ldsLockFences = 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;
|
||||
@@ -1109,6 +1111,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip
|
||||
phase = std::chrono::steady_clock::now();
|
||||
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), replay.cache, objects.module, objects.layout, capture.flags, capture.stageFlags, specialization.Info(), capture.requiredSubgroupSize, &objects.pipeline);
|
||||
result.pipelineMilliseconds = elapsed(phase);
|
||||
if ((capture.flags & VK_PIPELINE_CREATE_CAPTURE_STATISTICS_BIT_KHR) != 0) Graphics::LogPipelineStatistics_nid_no_patch(graphicsContext(), objects.pipeline);
|
||||
phase = std::chrono::steady_clock::now();
|
||||
const auto getCache = state->DeviceFunction<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData");
|
||||
std::size_t cacheBytes = 0;
|
||||
@@ -1304,6 +1307,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
state->tessellationPipelineCache = TessellationPipelineCacheUsable(driver.driverID);
|
||||
state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID);
|
||||
state->workgroupScopeFences = WorkgroupScopeFenceUsable(driver.driverID);
|
||||
state->ldsLockFences = LdsLockFencesRequired(driver.driverID);
|
||||
}
|
||||
if (driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY && hasExtension(VK_NV_SHADER_SM_BUILTINS_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceShaderSMBuiltinsPropertiesNV smBuiltins{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_SM_BUILTINS_PROPERTIES_NV};
|
||||
@@ -1340,6 +1344,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &barycentricFeatures};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
|
||||
state->fragmentShaderBarycentric = barycentricFeatures.fragmentShaderBarycentric == VK_TRUE;
|
||||
state->fragmentBarycentricBuiltins = state->fragmentShaderBarycentric;
|
||||
if (state->fragmentShaderBarycentric && hasExtension(VK_KHR_DRIVER_PROPERTIES_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceDriverProperties driver{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DRIVER_PROPERTIES};
|
||||
VkPhysicalDeviceProperties2 driverProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &driver};
|
||||
@@ -1394,7 +1399,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
deviceExtensions.push_back(VK_EXT_HDR_METADATA_EXTENSION_NAME);
|
||||
state->hdrMetadata = true;
|
||||
}
|
||||
if (state->fragmentShaderBarycentric) {
|
||||
if (state->fragmentBarycentricBuiltins) {
|
||||
deviceExtensions.push_back(VK_KHR_FRAGMENT_SHADER_BARYCENTRIC_EXTENSION_NAME);
|
||||
state->capabilities.push_back(spv::CapabilityFragmentBarycentricKHR);
|
||||
state->spirvExtensions.push_back("SPV_KHR_fragment_shader_barycentric");
|
||||
@@ -1811,8 +1816,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
// Recompiled storage-image access declares no format (the guest descriptor decides it).
|
||||
enabled.shaderStorageImageMultisample = available.shaderStorageImageMultisample;
|
||||
state->storageImageMultisample = enabled.shaderStorageImageMultisample == VK_TRUE;
|
||||
if (enabled.shaderStorageImageMultisample) state->capabilities.push_back(spv::CapabilityStorageImageMultisample);
|
||||
state->capabilities.push_back(spv::CapabilityImageMSArray);
|
||||
if (enabled.shaderStorageImageMultisample) {
|
||||
state->capabilities.push_back(spv::CapabilityStorageImageMultisample);
|
||||
state->capabilities.push_back(spv::CapabilityImageMSArray);
|
||||
}
|
||||
enabled.shaderStorageImageWriteWithoutFormat = available.shaderStorageImageWriteWithoutFormat;
|
||||
state->storageImageWriteWithoutFormat = enabled.shaderStorageImageWriteWithoutFormat == VK_TRUE;
|
||||
enabled.shaderStorageImageReadWithoutFormat = available.shaderStorageImageReadWithoutFormat;
|
||||
@@ -1931,7 +1938,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
|
||||
deviceInfo.pNext = &coverageReductionFeatures;
|
||||
}
|
||||
byteFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
|
||||
if (state->fragmentShaderBarycentric) {
|
||||
if (state->fragmentBarycentricBuiltins) {
|
||||
barycentricFeatures.pNext = byteFeatures.pNext;
|
||||
byteFeatures.pNext = &barycentricFeatures;
|
||||
}
|
||||
@@ -4041,6 +4048,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
|
||||
const auto& limits = state->properties.limits;
|
||||
ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}};
|
||||
target.fragmentShaderBarycentricEnabled = state->fragmentShaderBarycentric;
|
||||
target.fragmentBarycentricBuiltins = state->fragmentBarycentricBuiltins;
|
||||
target.nonConstantImageOffsets = state->maintenance8;
|
||||
target.narrowSubgroupClock = state->narrowSubgroupClock;
|
||||
target.float64DenormPreserve = state->float64DenormPreserve;
|
||||
@@ -4055,6 +4063,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
|
||||
target.largePixelFunctionLdsBuffer = state->driverId == VK_DRIVER_ID_NVIDIA_PROPRIETARY;
|
||||
target.pixelFunctionStorageDwords = state->pixelFunctionStorageDwords;
|
||||
target.deviceScopeLdsMemoryBarriers = !state->workgroupScopeFences;
|
||||
target.ldsLockFences = state->ldsLockFences;
|
||||
target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats;
|
||||
target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0;
|
||||
if (state->meshShader) {
|
||||
@@ -4127,7 +4136,7 @@ Graphics::Context VulkanDevice::buildContext() const {
|
||||
state->depthRangeUnrestricted,
|
||||
true,
|
||||
state->subgroup,
|
||||
state->fragmentShaderBarycentric,
|
||||
state->fragmentBarycentricBuiltins,
|
||||
state->samplerAnisotropy,
|
||||
state->textureCompressionBC,
|
||||
state->detiler.get(),
|
||||
@@ -4276,7 +4285,7 @@ bool VulkanDevice::PrebuildGraphicsLibraries(const Graphics::ShaderStages& stage
|
||||
return Graphics::Pipeline::Prebuild(context, state, Graphics::VertexInputLayout{}, layouts.View(), group);
|
||||
}
|
||||
|
||||
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe, const DrawTraceContext* trace) {
|
||||
void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipe, const DrawTraceContext* trace, std::uint64_t shape) {
|
||||
startJournalReplay();
|
||||
for (const auto& shader : shaders) {
|
||||
require(shader.program != nullptr, "missing compiled shader");
|
||||
@@ -4287,7 +4296,7 @@ void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParamete
|
||||
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
|
||||
if (traceDraws) APS5_LOG_OUT("VulkanDevice::Draw indices=%u instances=%u indexSize=%u address=0x%llx shaders=%zu colorTarget=%u", draw.indexCount, draw.instanceCount, draw.indexSize, static_cast<unsigned long long>(draw.indexAddress), shaders.size(), static_cast<unsigned>(graphics.hasColorTarget));
|
||||
const auto context = graphicsContext();
|
||||
Graphics::Draw(context, graphics, draw, shaders, snapshots, recipe, trace);
|
||||
Graphics::Draw(context, graphics, draw, shaders, snapshots, recipe, trace, shape);
|
||||
if (traceDraws) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete");
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
BufferPool(const BufferPool&) = delete;
|
||||
BufferPool& operator=(const BufferPool&) = delete;
|
||||
// The size a buffer for `bytes` is created with: its size class, or `bytes` itself when large.
|
||||
static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties);
|
||||
static std::size_t Capacity(std::size_t bytes);
|
||||
static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties);
|
||||
VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType = std::nullopt, std::uint32_t* allocatedType = nullptr, bool newImage = false);
|
||||
static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget);
|
||||
|
||||
@@ -76,6 +76,7 @@ std::uint64_t HtileMetadataBytes(VkExtent2D extent);
|
||||
bool HtileFillCovers(std::uint64_t htile, VkExtent2D extent, std::uint64_t address, std::size_t bytes);
|
||||
std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D);
|
||||
std::size_t DepthPlaneCopyCount();
|
||||
VkDeviceSize DepthPlaneCopyBytes();
|
||||
VkFormat HostDepthFormat(const Context& context, VkFormat format);
|
||||
float HostDepthBiasConstant(const Context& context, VkFormat format, float constant);
|
||||
std::shared_ptr<StorageTexture> DepthSurfaceStorage(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, std::uint32_t mip);
|
||||
|
||||
@@ -25,7 +25,7 @@ struct DrawTraceContext {
|
||||
// `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, non-indirect draw
|
||||
// built for its draw-cache entry (design_cpu_final M8; null otherwise, and always under
|
||||
// APS5_NO_DRAW_RECIPE=1).
|
||||
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr);
|
||||
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr, const DrawTraceContext* trace = nullptr, std::uint64_t shape = 0);
|
||||
|
||||
struct PixelFunctionLdsLayouts {
|
||||
VkDescriptorSetLayout empty = VK_NULL_HANDLE;
|
||||
@@ -124,6 +124,7 @@ struct DrawPassCounts {
|
||||
std::uint64_t storeFlushes = 0;
|
||||
};
|
||||
std::array<DrawPassCounts, 2> DrawPassTotals();
|
||||
std::array<std::uint64_t, PassBreakCount> DrawPassBreaks();
|
||||
const char* IndirectDrawPathName(IndirectDrawPath path);
|
||||
|
||||
// Draw packets that drew nothing, for the [draws] line: Nothing (an empty count, or no color writes
|
||||
|
||||
@@ -118,6 +118,10 @@ void SetHostImportDeferredSettles(bool enabled);
|
||||
std::uint64_t HostImportBudget(const VkPhysicalDeviceMemoryProperties& memory, std::uint64_t physicalBytes);
|
||||
void SetHostImportMaxBytes(std::uint64_t bytes);
|
||||
std::uint64_t HostImportLiveBytes();
|
||||
// A flip ends a frame: an import used in the current or the previous frame is never evicted for another one.
|
||||
void NoteHostImportFrame();
|
||||
std::uint64_t HostImportFramesEnded();
|
||||
std::uint64_t HostImportEpoch();
|
||||
std::uint64_t HostImportBudgetRemaining(const Context& context);
|
||||
struct HostImportSettleCounts {
|
||||
std::uint64_t submitted = 0;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#define VK_NO_PROTOTYPES
|
||||
#endif
|
||||
#include <vulkan/vulkan.h>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
@@ -27,6 +28,23 @@ struct PassAccess {
|
||||
|
||||
enum class PassHazard : std::uint8_t { None, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite };
|
||||
|
||||
enum class PassBreak : std::uint8_t { Continued, NewBatch, Command, Label, Samples, Forced, Segment, Key, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite, Count };
|
||||
constexpr std::size_t PassBreakCount = static_cast<std::size_t>(PassBreak::Count);
|
||||
constexpr std::array<const char*, PassBreakCount> PassBreakNames{"continued", "new batch", "command", "label", "samples", "forced", "segment", "key", "read after write", "write after write", "write after read", "image", "unknown read", "unknown write"};
|
||||
|
||||
constexpr PassBreak PassBreakFor(PassHazard hazard) {
|
||||
switch (hazard) {
|
||||
case PassHazard::ReadAfterWrite: return PassBreak::ReadAfterWrite;
|
||||
case PassHazard::WriteAfterWrite: return PassBreak::WriteAfterWrite;
|
||||
case PassHazard::WriteAfterRead: return PassBreak::WriteAfterRead;
|
||||
case PassHazard::Image: return PassBreak::Image;
|
||||
case PassHazard::UnknownRead: return PassBreak::UnknownRead;
|
||||
case PassHazard::UnknownWrite: return PassBreak::UnknownWrite;
|
||||
case PassHazard::None: break;
|
||||
}
|
||||
return PassBreak::Key;
|
||||
}
|
||||
|
||||
class GuestRangeSet {
|
||||
public:
|
||||
void Insert(std::uint64_t begin, std::uint64_t end);
|
||||
|
||||
@@ -115,7 +115,8 @@ private:
|
||||
// APS5_NO_PIPELINE_CACHE=1 builds one per draw as before.
|
||||
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const ShaderResources& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
|
||||
// `created` reports whether this call built the pipeline (false: served from the store or private).
|
||||
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr);
|
||||
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, bool* created = nullptr, std::uint64_t* storeHash = nullptr);
|
||||
std::shared_ptr<Pipeline> TouchCachedPipeline(const Context& context, std::uint64_t hash, const std::weak_ptr<Pipeline>& expected);
|
||||
// Cumulative pipeline store outcomes since start-up (the [pipecache] line reports 10 s windows).
|
||||
struct PipelineStoreCounters {
|
||||
std::uint64_t hits = 0;
|
||||
|
||||
@@ -68,6 +68,7 @@ public:
|
||||
VkCommandBuffer commands = VK_NULL_HANDLE;
|
||||
bool continued = false;
|
||||
bool barrier = true;
|
||||
PassBreak reason = PassBreak::Continued;
|
||||
};
|
||||
DrawPassStart StartDrawPass(std::uint64_t key, bool forced, bool fenced, const PassAccess& access);
|
||||
void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function<void(VkCommandBuffer)> afterPass = {});
|
||||
@@ -673,6 +674,7 @@ private:
|
||||
std::uint64_t key = 0;
|
||||
std::uint32_t timing = NoTiming;
|
||||
std::function<void(VkCommandBuffer)> afterPass;
|
||||
PassBreak closedBy = PassBreak::NewBatch;
|
||||
} renderPass;
|
||||
PassHazards passHazards;
|
||||
// A pass ended in this batch: Submit records the host-read barrier its draws left out.
|
||||
@@ -780,7 +782,7 @@ private:
|
||||
// form, toward the host only); returns whether the batch's host-read barrier was included.
|
||||
bool closeStoreRun(bool atSubmit = false);
|
||||
// Ends the render pass a draw left open (vkCmdEndRenderPass, the pass's trailing barrier).
|
||||
void endOpenRenderPass(bool barrier = true);
|
||||
void endOpenRenderPass(PassBreak by, bool barrier = true);
|
||||
// Records the queued key stores as one run (`forWriter`: before a command writing, reading or
|
||||
// labelling over one, not at Submit).
|
||||
void recordKeyStores(bool forWriter);
|
||||
|
||||
@@ -65,6 +65,7 @@ std::shared_ptr<StorageTexture> CachedStorageLayers(const Context& context, cons
|
||||
// would return); a true answer counts as a use for the cache's eviction order, as a lookup would.
|
||||
bool StorageImageCached(const Context& context, const StorageTexture* image);
|
||||
bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddress);
|
||||
std::shared_ptr<StorageTexture> RepeatStorageSurface(const Context& context, const GuestTextureResource& resource, const std::shared_ptr<StorageTexture>& stored);
|
||||
|
||||
// Defined in Texture.cpp beside the pending-results registry, for the fast Revalidate below: whether
|
||||
// a storage image other than `except` has results pending in [address, address + bytes).
|
||||
|
||||
@@ -306,7 +306,7 @@ struct DrawKeyRange {
|
||||
std::uint32_t count;
|
||||
};
|
||||
inline constexpr std::array<DrawKeyRange, 50> DrawKeyRegisters{{
|
||||
{RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 1}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5},
|
||||
{RegisterBank::Context, 0x000, 1}, {RegisterBank::Context, 0x002, 2}, {RegisterBank::Context, 0x005, 1}, {RegisterBank::Context, 0x007, 7}, {RegisterBank::Context, 0x010, 6}, {RegisterBank::Context, 0x01a, 5},
|
||||
{RegisterBank::Context, 0x080, 4}, {RegisterBank::Context, 0x08c, 4}, {RegisterBank::Context, 0x090, 2}, {RegisterBank::Context, 0x094, 2}, {RegisterBank::Context, 0x0b4, 2}, {RegisterBank::Context, 0x105, 4}, {RegisterBank::Context, 0x10b, 3}, {RegisterBank::Context, 0x10f, 6},
|
||||
// SPI_PS_INPUT_CNTL_0..31, SPI_PS_INPUT_ENA/ADDR, SPI_PS_IN_CONTROL .. SPI_BARYC_CNTL, SPI_SHADER_POS/Z/COL_FORMAT,
|
||||
// CB_BLEND0..7_CONTROL, GE_MAX_OUTPUT_PER_SUBGROUP.
|
||||
|
||||
@@ -8,5 +8,5 @@ layout(location = 0) flat in uint paletteIndex;
|
||||
layout(location = 0) out vec4 color;
|
||||
|
||||
void main() {
|
||||
color = paletteIndex < 7u ? parameters.palette[paletteIndex] : parameters.palette[0];
|
||||
color = paletteIndex == 1u ? parameters.palette[1] : paletteIndex == 2u ? parameters.palette[2] : paletteIndex == 3u ? parameters.palette[3] : paletteIndex == 4u ? parameters.palette[4] : paletteIndex == 5u ? parameters.palette[5] : paletteIndex == 6u ? parameters.palette[6] : parameters.palette[0];
|
||||
}
|
||||
@@ -369,11 +369,10 @@ bool BufferPool::DeviceTiered(VkMemoryPropertyFlags properties) {
|
||||
return (properties & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) != 0 && (properties & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) == 0 && DeviceTierEnabled();
|
||||
}
|
||||
|
||||
std::size_t BufferPool::Capacity(std::size_t bytes, VkMemoryPropertyFlags properties) {
|
||||
std::size_t BufferPool::Capacity(std::size_t bytes) {
|
||||
constexpr std::size_t smallest = 256;
|
||||
if (exactSizes()) return bytes;
|
||||
if (bytes < classLimit) return std::max(smallest, std::bit_ceil(bytes));
|
||||
if (!DeviceTiered(properties)) return bytes;
|
||||
const auto step = std::bit_floor(bytes) >> deviceClassBits;
|
||||
return (bytes + step - 1) / step * step;
|
||||
}
|
||||
@@ -393,7 +392,7 @@ BufferPool::Tier& BufferPool::tierFor(std::size_t capacity, VkMemoryPropertyFlag
|
||||
|
||||
std::optional<BufferAllocation> BufferPool::Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) {
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
const auto capacity = Capacity(bytes, properties);
|
||||
const auto capacity = Capacity(bytes);
|
||||
std::lock_guard lock(mutex);
|
||||
++clock;
|
||||
if (profile) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DeferredDestruction.hpp"
|
||||
#include <atomic>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
@@ -25,7 +24,8 @@ struct Deferred {
|
||||
struct Registry {
|
||||
std::mutex mutex;
|
||||
std::unordered_map<VkDevice, Deferred> devices;
|
||||
std::atomic<PFN_vkGetDeviceProcAddr> resolver{nullptr};
|
||||
std::unordered_map<VkDevice, PFN_vkGetDeviceProcAddr> resolvers;
|
||||
PFN_vkGetDeviceProcAddr latest = nullptr;
|
||||
};
|
||||
|
||||
Registry& registry() {
|
||||
@@ -33,6 +33,13 @@ Registry& registry() {
|
||||
return state;
|
||||
}
|
||||
|
||||
PFN_vkGetDeviceProcAddr resolverFor(VkDevice device) {
|
||||
auto& state = registry();
|
||||
std::lock_guard lock(state.mutex);
|
||||
const auto found = state.resolvers.find(device);
|
||||
return found == state.resolvers.end() ? state.latest : found->second;
|
||||
}
|
||||
|
||||
void destroyAll(VkDevice device, const Deferred& deferred) noexcept {
|
||||
for (const auto buffer : deferred.buffers) deferred.destroyBuffer(device, buffer, nullptr);
|
||||
for (const auto memory : deferred.memory) deferred.freeMemory(device, memory, nullptr);
|
||||
@@ -56,18 +63,18 @@ bool defer(VkDevice device, THandle handle, std::vector<THandle> Deferred::*list
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL deferredDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* allocator) {
|
||||
if (buffer == VK_NULL_HANDLE || defer(device, buffer, &Deferred::buffers)) return;
|
||||
reinterpret_cast<PFN_vkDestroyBuffer>(registry().resolver.load(std::memory_order_acquire)(device, "vkDestroyBuffer"))(device, buffer, allocator);
|
||||
reinterpret_cast<PFN_vkDestroyBuffer>(resolverFor(device)(device, "vkDestroyBuffer"))(device, buffer, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL deferredFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* allocator) {
|
||||
if (memory == VK_NULL_HANDLE || defer(device, memory, &Deferred::memory)) return;
|
||||
reinterpret_cast<PFN_vkFreeMemory>(registry().resolver.load(std::memory_order_acquire)(device, "vkFreeMemory"))(device, memory, allocator);
|
||||
reinterpret_cast<PFN_vkFreeMemory>(resolverFor(device)(device, "vkFreeMemory"))(device, memory, allocator);
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL deferringDeviceProc(VkDevice device, const char* name) {
|
||||
if (std::strcmp(name, "vkDestroyBuffer") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&deferredDestroyBuffer);
|
||||
if (std::strcmp(name, "vkFreeMemory") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&deferredFreeMemory);
|
||||
return registry().resolver.load(std::memory_order_acquire)(device, name);
|
||||
return resolverFor(device)(device, name);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -80,10 +87,10 @@ PFN_vkGetDeviceProcAddr DeferDestructionToIdle(VkDevice device, PFN_vkGetDeviceP
|
||||
if (deferred.destroyBuffer == nullptr || deferred.freeMemory == nullptr) throw std::runtime_error("AGC graphics: missing Vulkan function: vkDestroyBuffer or vkFreeMemory");
|
||||
auto& state = registry();
|
||||
std::lock_guard lock(state.mutex);
|
||||
const auto current = state.resolver.load(std::memory_order_acquire);
|
||||
if (current != nullptr && current != resolver) throw std::runtime_error("AGC graphics: deferred destruction found two different device resolvers");
|
||||
if (!state.devices.empty() && state.latest != resolver) throw std::runtime_error("AGC graphics: deferred destruction found two different device resolvers");
|
||||
if (!state.devices.emplace(device, std::move(deferred)).second) throw std::runtime_error("AGC graphics: deferred destruction was set up twice for one device");
|
||||
state.resolver.store(resolver, std::memory_order_release);
|
||||
state.resolvers.insert_or_assign(device, resolver);
|
||||
state.latest = resolver;
|
||||
return &deferringDeviceProc;
|
||||
}
|
||||
|
||||
|
||||
@@ -1353,6 +1353,7 @@ public:
|
||||
const std::uint32_t layers;
|
||||
VkDeviceSize imageBytes = 0;
|
||||
std::uint64_t lastUse = 0;
|
||||
std::uint64_t lastPresent = 0;
|
||||
VkDeviceSize HeldBytes() const { return imageBytes + (staging != nullptr ? staging->Size() : 0u); }
|
||||
|
||||
private:
|
||||
@@ -1478,10 +1479,13 @@ void evictPlaneCopies(const std::tuple<VkDevice, std::uint64_t, std::uint32_t, s
|
||||
auto& copies = planeCopies();
|
||||
VkDeviceSize total = 0;
|
||||
for (const auto& entry : copies) total += entry.second->HeldBytes();
|
||||
const auto present = Recorder::Presents();
|
||||
const auto ceiling = budget + std::min(budget, std::numeric_limits<VkDeviceSize>::max() - budget);
|
||||
while (total > budget) {
|
||||
const bool over = total > ceiling;
|
||||
auto oldest = copies.end();
|
||||
for (auto it = copies.begin(); it != copies.end(); ++it) {
|
||||
if (it->first == keep) continue;
|
||||
if (it->first == keep || (!over && it->second->lastPresent + 1u >= present)) continue;
|
||||
if (oldest == copies.end() || it->second->lastUse < oldest->second->lastUse) oldest = it;
|
||||
}
|
||||
if (oldest == copies.end()) break;
|
||||
@@ -2218,6 +2222,7 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint6
|
||||
surface->ClearStale();
|
||||
continue;
|
||||
}
|
||||
GuestMemory::CollectWritesUncached(plane, static_cast<std::size_t>(planeBytes));
|
||||
surface->SyncFromMemory(resource);
|
||||
auto units = scope == DepthPlaneScope::Whole ? surface->staleUnits : surface->StaleUnitsIn(planeBytes, address, end);
|
||||
if (anyUnit(units)) {
|
||||
@@ -2435,6 +2440,12 @@ std::size_t DepthPlaneCopyCount() {
|
||||
return planeCopies().size();
|
||||
}
|
||||
|
||||
VkDeviceSize DepthPlaneCopyBytes() {
|
||||
VkDeviceSize total = 0;
|
||||
for (const auto& entry : planeCopies()) total += entry.second->HeldBytes();
|
||||
return total;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
thread_local bool depthCopySourceActive = false;
|
||||
@@ -2512,6 +2523,7 @@ std::shared_ptr<Texture> DepthSurfaceTexture(const Context& context, std::span<c
|
||||
auto& copy = copies[copyKey];
|
||||
if (copy == nullptr || copy->layers < layers) copy = std::make_unique<DepthPlaneCopy>(context, base.extent, imageFormat, layers);
|
||||
copy->lastUse = ++planeCopyClock();
|
||||
copy->lastPresent = Recorder::Presents();
|
||||
auto texture = copy->Refresh(slices, resource, components, cube || resource.dimension == TextureDimension::k2DArray ? VK_IMAGE_VIEW_TYPE_2D_ARRAY : VK_IMAGE_VIEW_TYPE_2D, layered && !cube ? resource.baseArray : 0u);
|
||||
evictPlaneCopies(copyKey);
|
||||
return texture;
|
||||
|
||||
@@ -567,6 +567,7 @@ struct DrawOutcome {
|
||||
// A recorded draw began a render pass of its own, or continued the previous draw's.
|
||||
bool passBegun = false;
|
||||
bool passContinued = false;
|
||||
PassBreak passBreak = PassBreak::Continued;
|
||||
bool keyStoresFlushed = false;
|
||||
bool storesFlushed = false;
|
||||
bool addressDraw = false;
|
||||
@@ -643,6 +644,8 @@ struct DrawProfile {
|
||||
std::uint64_t passesBegun = 0;
|
||||
std::uint64_t passesContinued = 0;
|
||||
std::array<DrawPassCounts, 2> passTotals{};
|
||||
std::array<std::uint64_t, PassBreakCount> passBreaks{};
|
||||
std::array<std::uint64_t, PassBreakCount> reportedPassBreaks{};
|
||||
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.
|
||||
@@ -713,6 +716,7 @@ void reportDraw(const std::array<double, PhaseCount>& us, const ShaderResources:
|
||||
if (outcome.recorded) ++(outcome.waited ? profile.waited : profile.recorded);
|
||||
if (outcome.completion) ++profile.completion;
|
||||
if (outcome.passBegun) ++profile.passesBegun;
|
||||
if (outcome.passBegun) ++profile.passBreaks[static_cast<std::size_t>(outcome.passBreak)];
|
||||
if (outcome.passContinued) ++profile.passesContinued;
|
||||
if (outcome.recorded) {
|
||||
auto& totals = profile.passTotals[outcome.addressDraw ? 1 : 0];
|
||||
@@ -742,8 +746,16 @@ void reportDraw(const std::array<double, PhaseCount>& us, const ShaderResources:
|
||||
const auto synchronous = profile.draws - profile.recorded - profile.waited;
|
||||
const auto average = [](double total, std::uint64_t count) { return count != 0 ? total / static_cast<double>(count) : 0.0; };
|
||||
char line[2048];
|
||||
int n = std::snprintf(line, sizeof(line), "[draws] %llu draws over 10 s (%llu recorded avg %.0f us, of them %llu with completion; %llu recorded then waited avg %.0f us; %llu synchronous avg %.0f us; render passes %llu begun, %llu draws continued one; waited or synchronous because:", static_cast<unsigned long long>(profile.draws), static_cast<unsigned long long>(profile.recorded), average(profile.recordedUs, profile.recorded), static_cast<unsigned long long>(profile.completion), static_cast<unsigned long long>(profile.waited), average(profile.waitedUs, profile.waited), static_cast<unsigned long long>(synchronous), average(profile.synchronousUs, synchronous), static_cast<unsigned long long>(profile.passesBegun), static_cast<unsigned long long>(profile.passesContinued));
|
||||
int n = std::snprintf(line, sizeof(line), "[draws] %llu draws over 10 s (%llu recorded avg %.0f us, of them %llu with completion; %llu recorded then waited avg %.0f us; %llu synchronous avg %.0f us; render passes %llu begun", static_cast<unsigned long long>(profile.draws), static_cast<unsigned long long>(profile.recorded), average(profile.recordedUs, profile.recorded), static_cast<unsigned long long>(profile.completion), static_cast<unsigned long long>(profile.waited), average(profile.waitedUs, profile.waited), static_cast<unsigned long long>(synchronous), average(profile.synchronousUs, synchronous), static_cast<unsigned long long>(profile.passesBegun));
|
||||
const auto room = [&] { return n > 0 && static_cast<std::size_t>(n) < sizeof(line); };
|
||||
bool listed = false;
|
||||
for (std::size_t i = 1; i < PassBreakCount && room(); ++i) {
|
||||
const auto count = profile.passBreaks[i] - profile.reportedPassBreaks[i];
|
||||
if (count == 0) continue;
|
||||
n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), "%s%s %llu", listed ? ", " : " (", PassBreakNames[i], static_cast<unsigned long long>(count));
|
||||
listed = true;
|
||||
}
|
||||
if (room()) n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), "%s, %llu draws continued one; waited or synchronous because:", listed ? ")" : "", static_cast<unsigned long long>(profile.passesContinued));
|
||||
for (std::size_t i = SyncNone + 1; i < SyncCount && room(); ++i) {
|
||||
if (profile.reasons[i] != 0) n += std::snprintf(line + n, sizeof(line) - static_cast<std::size_t>(n), " %s %llu", SyncReasonNames[i], static_cast<unsigned long long>(profile.reasons[i]));
|
||||
}
|
||||
@@ -792,6 +804,7 @@ void reportDraw(const std::array<double, PhaseCount>& us, const ShaderResources:
|
||||
profile.addressBuilds = 0;
|
||||
profile.draws = profile.recorded = profile.waited = profile.completion = 0;
|
||||
profile.passesBegun = profile.passesContinued = 0;
|
||||
profile.reportedPassBreaks = profile.passBreaks;
|
||||
profile.reasons.fill(0);
|
||||
profile.recordedUs = profile.waitedUs = profile.synchronousUs = 0;
|
||||
profile.targetLookups = profile.slowLookups = 0;
|
||||
@@ -1125,6 +1138,12 @@ std::array<DrawPassCounts, 2> DrawPassTotals() {
|
||||
return stats.passTotals;
|
||||
}
|
||||
|
||||
std::array<std::uint64_t, PassBreakCount> DrawPassBreaks() {
|
||||
auto& stats = Profile();
|
||||
std::lock_guard lock(stats.mutex);
|
||||
return stats.passBreaks;
|
||||
}
|
||||
|
||||
void CountDrawSkip(DrawSkip kind, double us) {
|
||||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
if (!profile) return;
|
||||
@@ -2269,6 +2288,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
|
||||
const bool continued = start.continued;
|
||||
outcome.passContinued = continued;
|
||||
outcome.passBegun = !continued;
|
||||
outcome.passBreak = start.reason;
|
||||
const auto commands = start.commands;
|
||||
if (!continued) {
|
||||
recorder->PrepareSampleSlot();
|
||||
@@ -2495,6 +2515,7 @@ struct PreparedDraw {
|
||||
PreparedDraw& operator=(const PreparedDraw&) = delete;
|
||||
std::shared_ptr<const DrawRecipe>* recipeOut = nullptr;
|
||||
const DrawTraceContext* trace = nullptr;
|
||||
std::uint64_t shape = 0;
|
||||
std::vector<std::uint32_t> stencilReferences;
|
||||
std::optional<PerformanceTimer> timing;
|
||||
DrawTimer timer;
|
||||
@@ -2527,6 +2548,137 @@ struct PreparedDraw {
|
||||
std::optional<DepthSampleLocations> depthLocations;
|
||||
};
|
||||
|
||||
bool ShapeMemoEnabled() {
|
||||
static const bool enabled = std::getenv("APS5_NO_SHAPE_MEMO") == nullptr;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool VerifyShapeMemo() {
|
||||
static const bool verify = std::getenv("APS5_VERIFY_SHAPE_MEMO") != nullptr;
|
||||
return verify;
|
||||
}
|
||||
|
||||
void noteShapeMemoVerified(bool pipeline) {
|
||||
static std::atomic<std::uint64_t> targets{0};
|
||||
static std::atomic<std::uint64_t> pipelines{0};
|
||||
static std::atomic<std::int64_t> last{0};
|
||||
(pipeline ? pipelines : targets).fetch_add(1, std::memory_order_relaxed);
|
||||
const auto now = std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
auto previous = last.load(std::memory_order_relaxed);
|
||||
if (now - previous < 10 || !last.compare_exchange_strong(previous, now)) return;
|
||||
std::fprintf(stderr, "[shape-memo] verified %llu targets and %llu pipelines\n", static_cast<unsigned long long>(targets.load()), static_cast<unsigned long long>(pipelines.load()));
|
||||
}
|
||||
|
||||
struct ShapeChoice {
|
||||
VkImageLayout attachmentLayout = VK_IMAGE_LAYOUT_UNDEFINED;
|
||||
std::uint32_t fragmentOutputs = 0;
|
||||
std::vector<std::uint64_t> stages;
|
||||
std::vector<VkVertexInputBindingDescription> bindings;
|
||||
std::vector<VkVertexInputAttributeDescription> attributes;
|
||||
std::vector<std::uint32_t> mainKey;
|
||||
std::vector<std::uint32_t> fragmentKey;
|
||||
std::uint64_t hash = 0;
|
||||
std::weak_ptr<Pipeline> pipeline;
|
||||
};
|
||||
|
||||
struct ShapeEntry {
|
||||
VkDevice device = VK_NULL_HANDLE;
|
||||
std::vector<std::weak_ptr<StorageTexture>> targets;
|
||||
std::vector<ShapeChoice> choices;
|
||||
};
|
||||
|
||||
struct ShapeMemoStorage {};
|
||||
constexpr std::size_t ShapeMemoEntries = 4096;
|
||||
constexpr std::size_t ShapeMemoChoices = 4;
|
||||
|
||||
std::unordered_map<std::uint64_t, ShapeEntry>& shapeMemo() {
|
||||
return HostThreadLocal<std::unordered_map<std::uint64_t, ShapeEntry>, ShapeMemoStorage>();
|
||||
}
|
||||
|
||||
const ShapeEntry* findShape(const Context& context, std::uint64_t shape) {
|
||||
if (shape == 0 || !ShapeMemoEnabled()) return nullptr;
|
||||
const auto& entries = shapeMemo();
|
||||
const auto found = entries.find(shape);
|
||||
return found == entries.end() || found->second.device != context.device ? nullptr : &found->second;
|
||||
}
|
||||
|
||||
std::shared_ptr<StorageTexture> shapeTarget(const Context& context, std::uint64_t shape, std::size_t index) {
|
||||
const auto* entry = findShape(context, shape);
|
||||
return entry == nullptr || index >= entry->targets.size() ? nullptr : entry->targets[index].lock();
|
||||
}
|
||||
|
||||
bool sameStages(std::span<const std::uint64_t> kept, std::span<const CompiledShader> shaders) {
|
||||
if (kept.size() != shaders.size() * 4u + 1u) return false;
|
||||
std::size_t at = 0;
|
||||
for (const auto& shader : shaders) {
|
||||
const auto identity = shader.program->Identity();
|
||||
if (identity.Empty() || kept[at] != static_cast<std::uint64_t>(shader.stage) || kept[at + 1] != identity.low || kept[at + 2] != identity.high || kept[at + 3] != shader.pushConstantOffset) return false;
|
||||
at += 4;
|
||||
}
|
||||
return kept[at] == PushConstantStages(shaders);
|
||||
}
|
||||
|
||||
bool sameChoice(const ShapeChoice& choice, const VertexInputLayout& input, const PipelineSetLayouts& layouts, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, std::uint32_t fragmentOutputs) {
|
||||
const auto sameBinding = [](const VkVertexInputBindingDescription& a, const VkVertexInputBindingDescription& b) { return a.binding == b.binding && a.stride == b.stride && a.inputRate == b.inputRate; };
|
||||
const auto sameAttribute = [](const VkVertexInputAttributeDescription& a, const VkVertexInputAttributeDescription& b) { return a.location == b.location && a.binding == b.binding && a.format == b.format && a.offset == b.offset; };
|
||||
return choice.attachmentLayout == attachmentLayout && choice.fragmentOutputs == fragmentOutputs && std::ranges::equal(choice.bindings, input.bindings, sameBinding) && std::ranges::equal(choice.attributes, input.attributes, sameAttribute) && std::ranges::equal(choice.mainKey, layouts.mainKey) && std::ranges::equal(choice.fragmentKey, layouts.fragmentKey) && sameStages(choice.stages, shaders);
|
||||
}
|
||||
|
||||
std::shared_ptr<Pipeline> shapePipeline(const Context& context, std::uint64_t shape, const VertexInputLayout& input, const PipelineSetLayouts& layouts, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, std::uint32_t fragmentOutputs) {
|
||||
const auto* entry = findShape(context, shape);
|
||||
if (entry == nullptr) return nullptr;
|
||||
for (const auto& choice : entry->choices) {
|
||||
if (!sameChoice(choice, input, layouts, shaders, attachmentLayout, fragmentOutputs)) continue;
|
||||
const auto hash = choice.hash;
|
||||
const auto expected = choice.pipeline;
|
||||
return TouchCachedPipeline(context, hash, expected);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void rememberShape(const Context& context, std::uint64_t shape, std::span<const TargetBinding> targets, const std::shared_ptr<Pipeline>* pipeline, std::uint64_t hash, const VertexInputLayout& input, const PipelineSetLayouts& layouts, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, std::uint32_t fragmentOutputs) {
|
||||
if (shape == 0 || !ShapeMemoEnabled()) return;
|
||||
auto& entries = shapeMemo();
|
||||
if (entries.size() >= ShapeMemoEntries && !entries.contains(shape)) entries.clear();
|
||||
auto& entry = entries[shape];
|
||||
if (entry.device != context.device) {
|
||||
entry = {};
|
||||
entry.device = context.device;
|
||||
}
|
||||
entry.targets.resize(targets.size());
|
||||
for (std::size_t index = 0; index < targets.size(); ++index) {
|
||||
const auto& resident = targets[index].resident;
|
||||
if (resident == nullptr || targets[index].color.arrayLastSlice != 0) entry.targets[index].reset();
|
||||
else if (entry.targets[index].owner_before(resident) || resident.owner_before(entry.targets[index])) entry.targets[index] = resident;
|
||||
}
|
||||
if (pipeline == nullptr || hash == 0) return;
|
||||
auto& choices = entry.choices;
|
||||
const auto kept = std::ranges::find_if(choices, [&](const ShapeChoice& choice) { return sameChoice(choice, input, layouts, shaders, attachmentLayout, fragmentOutputs); });
|
||||
if (kept != choices.end()) {
|
||||
kept->hash = hash;
|
||||
kept->pipeline = *pipeline;
|
||||
std::rotate(choices.begin(), kept, std::next(kept));
|
||||
return;
|
||||
}
|
||||
ShapeChoice choice;
|
||||
choice.attachmentLayout = attachmentLayout;
|
||||
choice.fragmentOutputs = fragmentOutputs;
|
||||
for (const auto& shader : shaders) {
|
||||
const auto identity = shader.program->Identity();
|
||||
if (identity.Empty()) return;
|
||||
choice.stages.insert(choice.stages.end(), {static_cast<std::uint64_t>(shader.stage), identity.low, identity.high, shader.pushConstantOffset});
|
||||
}
|
||||
choice.stages.push_back(PushConstantStages(shaders));
|
||||
choice.bindings = input.bindings;
|
||||
choice.attributes = input.attributes;
|
||||
choice.mainKey.assign(layouts.mainKey.begin(), layouts.mainKey.end());
|
||||
choice.fragmentKey.assign(layouts.fragmentKey.begin(), layouts.fragmentKey.end());
|
||||
choice.hash = hash;
|
||||
choice.pipeline = *pipeline;
|
||||
if (choices.size() >= ShapeMemoChoices) choices.pop_back();
|
||||
choices.insert(choices.begin(), std::move(choice));
|
||||
}
|
||||
|
||||
bool prepareDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, PreparedDraw& prepared) {
|
||||
auto*& recipeOut = prepared.recipeOut;
|
||||
auto& stencilReferences = prepared.stencilReferences;
|
||||
@@ -2630,7 +2782,13 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara
|
||||
Require(resident->Attachable(), "storage format cannot be a color attachment");
|
||||
return resident;
|
||||
}
|
||||
auto resident = CachedStorageSurface(context, SurfaceForTarget(color));
|
||||
const auto surface = SurfaceForTarget(color);
|
||||
auto resident = RepeatStorageSurface(context, surface, shapeTarget(context, prepared.shape, index));
|
||||
if (resident != nullptr && VerifyShapeMemo()) {
|
||||
Require(CachedStorageSurface(context, surface) == resident, "APS5_VERIFY_SHAPE_MEMO: the shape memo's resident target differs from the storage cache's");
|
||||
noteShapeMemoVerified(false);
|
||||
}
|
||||
if (resident == nullptr) resident = CachedStorageSurface(context, surface);
|
||||
Require(resident->Attachable(), "storage format cannot be a color attachment");
|
||||
Require(color.mipCount > 1 || color.depth > 1 || resident->GuestBytes() == colorLayout.Bytes(), "resident image layout differs from the color layout");
|
||||
return resident;
|
||||
@@ -2890,7 +3048,22 @@ bool prepareDraw(const Context& context, const State& state, const Pm4::DrawPara
|
||||
stencilReplay.emplace_back(reference, std::move(selectedPipeline));
|
||||
}
|
||||
auto& pipeline = prepared.pipeline;
|
||||
pipeline = stencilReplay.empty() ? CachedPipeline(context, pipelineState, inputs.VertexLayout(), *resources, shaders, lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) : stencilReplay.front().second;
|
||||
const auto attachmentLayout = lean ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
|
||||
if (!stencilReplay.empty()) {
|
||||
pipeline = stencilReplay.front().second;
|
||||
} else {
|
||||
const bool memoized = stencilReferences.empty() && std::none_of(targets.begin(), targets.end(), [](const TargetBinding& binding) { return binding.proxied || binding.multisampled; });
|
||||
const auto layouts = resources->SetLayouts();
|
||||
std::uint64_t storeHash = 0;
|
||||
if (memoized) pipeline = shapePipeline(context, prepared.shape, inputs.VertexLayout(), layouts, shaders, attachmentLayout, inputs.fragmentOutputs);
|
||||
if (pipeline != nullptr && VerifyShapeMemo()) {
|
||||
Require(CachedPipeline(context, pipelineState, inputs.VertexLayout(), layouts, shaders, attachmentLayout) == pipeline, "APS5_VERIFY_SHAPE_MEMO: the shape memo's pipeline differs from the pipeline store's");
|
||||
noteShapeMemoVerified(true);
|
||||
}
|
||||
const bool repeated = pipeline != nullptr;
|
||||
if (!repeated) pipeline = CachedPipeline(context, pipelineState, inputs.VertexLayout(), layouts, shaders, attachmentLayout, nullptr, &storeHash);
|
||||
if (memoized) rememberShape(context, prepared.shape, targets, repeated ? nullptr : &pipeline, storeHash, inputs.VertexLayout(), layouts, shaders, attachmentLayout, inputs.fragmentOutputs);
|
||||
}
|
||||
// Resident targets' views are stable while their storage image lives, so the framebuffer is
|
||||
// reused with the pipeline; a per-draw RenderTarget gets a framebuffer of its own.
|
||||
auto& owners = prepared.owners;
|
||||
@@ -3354,7 +3527,7 @@ void drawTessellationGeometry(const Context& context, const State& state, const
|
||||
Draw(context, meshed, captured, second, snapshots, nullptr, trace);
|
||||
}
|
||||
|
||||
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut, const DrawTraceContext* trace) {
|
||||
void Draw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots, std::shared_ptr<const DrawRecipe>* recipeOut, const DrawTraceContext* trace, std::uint64_t shape) {
|
||||
const DepthCopySourceScope depthCopySource(state.depthCopySource);
|
||||
if (state.stages.tessellationGeometry) {
|
||||
drawTessellationGeometry(context, state, draw, shaders, snapshots, trace);
|
||||
@@ -3364,6 +3537,7 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
|
||||
PreparedDraw prepared(profile);
|
||||
prepared.recipeOut = recipeOut;
|
||||
prepared.trace = trace;
|
||||
prepared.shape = shape;
|
||||
if (prepareDraw(context, state, draw, shaders, snapshots, prepared)) recordPreparedDraw(context, state, draw, shaders, prepared);
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ struct EvictingRefusal {
|
||||
std::uint64_t bytes;
|
||||
GuestAllocations::Mapped ranges;
|
||||
std::uint64_t epoch;
|
||||
std::uint64_t frames;
|
||||
std::uint64_t liveBytes;
|
||||
std::uint64_t budget;
|
||||
std::uint64_t room;
|
||||
@@ -294,6 +295,9 @@ struct HostImports {
|
||||
#endif
|
||||
std::uint64_t useCounter = 0;
|
||||
std::uint64_t buildFloor = 0;
|
||||
std::uint64_t frameFloor = 0;
|
||||
std::uint64_t previousFrameFloor = 0;
|
||||
std::uint64_t framesEnded = 0;
|
||||
std::array<HostImportRequesterCounts, ImportRequesterCount> counts{};
|
||||
VkPhysicalDevice reportedDevice = VK_NULL_HANDLE;
|
||||
std::chrono::steady_clock::time_point reportedAt{};
|
||||
@@ -1611,12 +1615,18 @@ bool pointerHeld(const HostImports& state, const HostImport& entry) {
|
||||
|
||||
constexpr std::uint64_t ProtectedUses = 256;
|
||||
|
||||
bool hasFrameHistory(const HostImports& state) {
|
||||
return state.previousFrameFloor != 0;
|
||||
}
|
||||
|
||||
bool evictable(const HostImports& state, const HostImport& entry) {
|
||||
return !pointerHeld(state, entry) && effectiveUse(state, entry) + ProtectedUses < state.useCounter;
|
||||
if (pointerHeld(state, entry)) return false;
|
||||
if (!hasFrameHistory(state)) return effectiveUse(state, entry) + ProtectedUses < state.useCounter;
|
||||
return effectiveUse(state, entry) < state.previousFrameFloor;
|
||||
}
|
||||
|
||||
bool evictingRefusalCurrent(const HostImports& state, const EvictingRefusal& refusal, std::uint64_t budget) {
|
||||
return refusal.epoch == state.epoch && refusal.liveBytes == state.liveBytes && budget <= refusal.budget && refusal.window == importWindow.load(std::memory_order_relaxed) && state.useCounter < refusal.ready && (state.buildFloor <= refusal.floor || state.useCounter < refusal.readyHeld);
|
||||
return refusal.epoch == state.epoch && refusal.frames == state.framesEnded && refusal.liveBytes == state.liveBytes && budget <= refusal.budget && refusal.window == importWindow.load(std::memory_order_relaxed) && state.useCounter < refusal.ready && (state.buildFloor <= refusal.floor || state.useCounter < refusal.readyHeld);
|
||||
}
|
||||
|
||||
void noteRetiredUnwatched(std::vector<std::pair<std::uint64_t, std::uint64_t>>& spans, const HostImport& entry, const GuestAllocations::Lease& lease) {
|
||||
@@ -1822,7 +1832,9 @@ const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std:
|
||||
const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint64_t end);
|
||||
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, ImportRequester requester);
|
||||
|
||||
// The bytes retireLeastRecent could free outside [begin, end).
|
||||
// The bytes retireLeastRecent could free outside [begin, end). Before two frames have ended the imports age by lookups as they
|
||||
// did before frames existed. After that readyHeld is 0 when an import a build still holds is already two frames old: it becomes
|
||||
// evictable as soon as that build's floor rises, which ends a remembered refusal.
|
||||
std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end, std::uint64_t& ready, std::uint64_t& readyHeld) {
|
||||
std::uint64_t bytes = 0;
|
||||
for (const auto& [base, entry] : state.imports) {
|
||||
@@ -1831,8 +1843,10 @@ std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std:
|
||||
bytes += entry.pinnedBytes;
|
||||
continue;
|
||||
}
|
||||
auto& horizon = pointerHeld(state, entry) ? readyHeld : ready;
|
||||
horizon = std::min(horizon, effectiveUse(state, entry) + ProtectedUses + 1);
|
||||
if (!hasFrameHistory(state)) {
|
||||
auto& horizon = pointerHeld(state, entry) ? readyHeld : ready;
|
||||
horizon = std::min(horizon, effectiveUse(state, entry) + ProtectedUses + 1);
|
||||
} else if (pointerHeld(state, entry) && effectiveUse(state, entry) < state.previousFrameFloor) readyHeld = 0;
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
@@ -2060,7 +2074,7 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
|
||||
std::fprintf(stderr, "[gpu] host import budget: %llu MiB live of %llu MiB (%s); %llu imports (%llu MiB) refused so far, %llu of them remembered, last 0x%llx+0x%llx by %s\n", static_cast<unsigned long long>(state.liveBytes >> 20u), static_cast<unsigned long long>(budget >> 20u), budgetSource(context, state, budget).c_str(), static_cast<unsigned long long>(refused), static_cast<unsigned long long>(refusedBytes >> 20u), static_cast<unsigned long long>(remembered), static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), ImportRequesterName(requester));
|
||||
}
|
||||
const auto room = budget + evictable > live ? budget + evictable - live : 0;
|
||||
if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined});
|
||||
if (evict) state.evictingRefusals.insert_or_assign(std::pair{base, bytes}, EvictingRefusal{base, bytes, GuestAllocations::Mapped(run.ranges.begin(), run.ranges.end()), state.epoch, state.framesEnded, state.liveBytes, budget, room, importWindow.load(std::memory_order_relaxed), state.buildFloor, ready, readyHeld, joined});
|
||||
if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room));
|
||||
if (containing != run.ranges.end() && run.ranges.size() > 1) remember((*containing)->address, (*containing)->bytes, GuestAllocations::Mapped{*containing});
|
||||
++state.budgetRefusals;
|
||||
@@ -2162,9 +2176,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
|
||||
GuestArena::GuestArenaUnmapAlias_nid_postfix(std::exchange(entry.alias, nullptr));
|
||||
#endif
|
||||
std::fprintf(stderr, "[gpu] host import of 0x%llx+0x%llx failed at %s (%d); falling back to copies, requested by %s\n", static_cast<unsigned long long>(base), static_cast<unsigned long long>(bytes), step, static_cast<int>(result), ImportRequesterName(requester));
|
||||
const bool exhausted = result == VK_ERROR_OUT_OF_HOST_MEMORY || result == VK_ERROR_OUT_OF_DEVICE_MEMORY;
|
||||
if (std::string_view(step) == "vkAllocateMemory") {
|
||||
state.allocationFailed = true;
|
||||
if (++state.allocationFailures >= 3 || state.liveBytes + bytes + budget / 8 >= budget) {
|
||||
if (exhausted && (++state.allocationFailures >= 3 || state.liveBytes + bytes + budget / 8 >= budget)) {
|
||||
state.ceiling = std::max<std::uint64_t>(state.liveBytes, 1);
|
||||
std::fprintf(stderr, "[gpu] host import budget lowered to the %.1f MiB live after vkAllocateMemory failed %s\n", state.liveBytes / 1048576.0, state.allocationFailures >= 3 ? "3 times in a row" : "within an eighth of the budget");
|
||||
}
|
||||
@@ -3963,6 +3978,26 @@ void SetHostImportMaxBytes(std::uint64_t bytes) {
|
||||
importMaxBytes.store(bytes, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void NoteHostImportFrame() {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
state.previousFrameFloor = state.frameFloor;
|
||||
state.frameFloor = state.useCounter + 1;
|
||||
++state.framesEnded;
|
||||
}
|
||||
|
||||
std::uint64_t HostImportEpoch() {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
return state.epoch;
|
||||
}
|
||||
|
||||
std::uint64_t HostImportFramesEnded() {
|
||||
auto& state = Imports();
|
||||
std::lock_guard lock(state.mutex);
|
||||
return state.framesEnded;
|
||||
}
|
||||
|
||||
void SetHostImportBudget(std::uint64_t bytes) {
|
||||
budgetOverride.store(bytes, std::memory_order_relaxed);
|
||||
auto& state = Imports();
|
||||
@@ -5381,6 +5416,7 @@ void GuestBufferMemory::UploadFinish(bool addressable) {
|
||||
if (space != nullptr) completeSpaceWrites();
|
||||
uploaded = true;
|
||||
for (const auto& region : regions) static_cast<void>(FlushDepthPlanes(region.begin, static_cast<std::size_t>(region.end - region.begin)));
|
||||
for (const auto& [begin, end] : writes) RequireNoResidentDepthRead_nid_no_patch(begin, static_cast<std::size_t>(end - begin));
|
||||
for (const auto& [begin, end] : writes) StorageTexture::FlushPending(begin, static_cast<std::size_t>(end - begin), nullptr, "written buffer range");
|
||||
if (context.hostImportAlignment != 0) {
|
||||
auto& state = Imports();
|
||||
|
||||
@@ -248,11 +248,7 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
|
||||
case TextureDimension::kCube:
|
||||
Require(width == height, "guest cube texture descriptor is not square");
|
||||
Require(baseArray <= depth, "guest cube texture descriptor has a base array past its last array slice");
|
||||
if (baseArray == depth) depth += 5u;
|
||||
if (depth - baseArray + 1u < 6u) {
|
||||
Require(false, "guest cube texture descriptor holds fewer than 6 array slices" +
|
||||
describe() + " (base array " + std::to_string(baseArray) + ")");
|
||||
}
|
||||
if (depth - baseArray < 5u) depth = baseArray + 5u;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -1172,7 +1172,8 @@ VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span<cons
|
||||
std::array<std::uint32_t, 4> pattern{};
|
||||
std::memcpy(pattern.data(), texels.data(), texels.size());
|
||||
VkClearColorValue clear{};
|
||||
Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear), "multisampled DCC fast clear has no exact host value");
|
||||
const bool repeated = std::ranges::all_of(pattern, [&](std::uint32_t word) { return word == pattern.front(); });
|
||||
Require(ClearColorForTexel(target.format, target.elementBytes, pattern, clear) || (repeated && MultisampledFillClearColor(target, pattern.front(), clear)), "multisampled DCC fast clear has no exact host value");
|
||||
return clear;
|
||||
}
|
||||
|
||||
@@ -1278,12 +1279,16 @@ bool sameStorageForm(const Context& context, VkFormat rendered, VkFormat request
|
||||
return storage != VK_FORMAT_UNDEFINED && storage == HostStorageFormat(context, requested);
|
||||
}
|
||||
|
||||
VkFormat sampledFormat(const Context& context, std::uint32_t guestFormat) {
|
||||
return IsRepackedTextureFormat(guestFormat) ? ResolveTextureFormat(guestFormat) : SampledTextureFormat(context, guestFormat);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
discardRetiredTargets();
|
||||
const auto format = ResolveTextureFormat(resource.format);
|
||||
const auto format = sampledFormat(context, resource.format);
|
||||
const MultisampleTarget* other = nullptr;
|
||||
for (const auto& target : targets()) {
|
||||
if (!target->active->load(std::memory_order_acquire)) continue;
|
||||
@@ -1306,7 +1311,7 @@ std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const
|
||||
std::shared_ptr<Texture> MultisampleTargetFirstSample(const Context& context, const GuestTextureResource& resource, VkComponentMapping components) {
|
||||
std::lock_guard lock(targetsMutex());
|
||||
discardRetiredTargets();
|
||||
const auto format = ResolveTextureFormat(resource.format);
|
||||
const auto format = sampledFormat(context, resource.format);
|
||||
const MultisampleTarget* other = nullptr;
|
||||
for (const auto& target : targets()) {
|
||||
if (!target->active->load(std::memory_order_acquire)) continue;
|
||||
|
||||
@@ -1228,9 +1228,10 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
|
||||
return CachedPipeline(context, state, vertexInput, resources.SetLayouts(), shaders, attachmentLayout);
|
||||
}
|
||||
|
||||
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, bool* created) {
|
||||
std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout, bool* created, std::uint64_t* storeHash) {
|
||||
static const bool disabled = std::getenv("APS5_NO_PIPELINE_CACHE") != nullptr;
|
||||
if (created != nullptr) *created = false;
|
||||
if (storeHash != nullptr) *storeHash = 0;
|
||||
if (disabled) return std::make_shared<Pipeline>(context, state, vertexInput, resources, shaders, attachmentLayout);
|
||||
auto& store = Pipelines();
|
||||
struct PipelineKeyStorage {};
|
||||
@@ -1270,6 +1271,7 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
|
||||
if (auto pipeline = lookup(collision)) {
|
||||
++store.hits;
|
||||
++store.totals.hits;
|
||||
if (storeHash != nullptr) *storeHash = hash;
|
||||
return pipeline;
|
||||
}
|
||||
if (collision) {
|
||||
@@ -1290,7 +1292,10 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
|
||||
{
|
||||
std::lock_guard lock(store.mutex);
|
||||
bool collision = false;
|
||||
if (auto raced = lookup(collision)) return raced;
|
||||
if (auto raced = lookup(collision)) {
|
||||
if (storeHash != nullptr) *storeHash = hash;
|
||||
return raced;
|
||||
}
|
||||
if (collision) {
|
||||
++store.uncached;
|
||||
++store.totals.uncached;
|
||||
@@ -1325,12 +1330,27 @@ std::shared_ptr<Pipeline> CachedPipeline(const Context& context, const State& st
|
||||
++store.evicted;
|
||||
++store.totals.evicted;
|
||||
}
|
||||
if (storeHash != nullptr && store.index.contains(hash)) *storeHash = hash;
|
||||
}
|
||||
if (created != nullptr) *created = true;
|
||||
if (PipelineJournalEnabled() && !PipelineJournalReplaying() && state.tessellationCapture == nullptr) RecordGraphicsPipeline(MakeGraphicsPipelineRecipe(state, vertexInput, resources, shaders, attachmentLayout), shaders);
|
||||
return pipeline;
|
||||
}
|
||||
|
||||
std::shared_ptr<Pipeline> TouchCachedPipeline(const Context& context, std::uint64_t hash, const std::weak_ptr<Pipeline>& expected) {
|
||||
auto& store = Pipelines();
|
||||
std::lock_guard lock(store.mutex);
|
||||
reportPipelines(store);
|
||||
const auto found = store.index.find(hash);
|
||||
if (found == store.index.end()) return nullptr;
|
||||
const auto it = found->second;
|
||||
if (it->pipeline.owner_before(expected) || expected.owner_before(it->pipeline) || !alive(*it, context)) return nullptr;
|
||||
store.entries.splice(store.entries.end(), store.entries, it);
|
||||
++store.hits;
|
||||
++store.totals.hits;
|
||||
return it->pipeline;
|
||||
}
|
||||
|
||||
void BuildPipelineStoreKey(std::vector<std::byte>& key, const Context& context, const State& state, const VertexInputLayout& vertexInput, const PipelineSetLayouts& resources, std::span<const CompiledShader> shaders, VkImageLayout attachmentLayout) {
|
||||
pipelineKey(key, context, state, vertexInput, resources, shaders, attachmentLayout);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <new>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -72,7 +73,12 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope
|
||||
result = create(context.device, &info, nullptr, &cache);
|
||||
}
|
||||
Check(result, "vkCreatePipelineCache");
|
||||
if (!path.empty()) saver = std::thread([this] { run(); });
|
||||
try {
|
||||
if (!path.empty()) saver = std::thread([this] { run(); });
|
||||
} catch (...) {
|
||||
context.Function<PFN_vkDestroyPipelineCache>("vkDestroyPipelineCache")(context.device, cache, nullptr);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
PipelineCache::~PipelineCache() {
|
||||
@@ -179,7 +185,7 @@ void PipelineCache::load(std::vector<std::byte>& initialData) {
|
||||
std::fprintf(stderr, "[pipeline-cache] loaded %.1f KiB from %s\n", static_cast<double>(initialData.size()) / 1024.0, path.string().c_str());
|
||||
}
|
||||
|
||||
void PipelineCache::save(bool final, bool waitForPipelines) {
|
||||
void PipelineCache::save(bool final, bool waitForPipelines) try {
|
||||
std::lock_guard pathLock(pathMutex(path));
|
||||
const auto getData = context.Function<PFN_vkGetPipelineCacheData>("vkGetPipelineCacheData");
|
||||
const auto snapshot = [&] { return waitForPipelines ? synchronization.LockForSnapshot() : synchronization.TryLockForSnapshot(); };
|
||||
@@ -200,7 +206,6 @@ void PipelineCache::save(bool final, bool waitForPipelines) {
|
||||
auto disk = fileUnchanged ? File{} : readFile();
|
||||
if (disk.state == FileState::Unreadable) {
|
||||
std::fprintf(stderr, "[pipeline-cache] cannot read %s; not saving over it\n", path.string().c_str());
|
||||
settle(readHeader());
|
||||
return;
|
||||
}
|
||||
if (disk.state == FileState::Invalid) std::fprintf(stderr, "[pipeline-cache] replacing %s: %s\n", path.string().c_str(), disk.invalidReason);
|
||||
@@ -271,6 +276,8 @@ void PipelineCache::save(bool final, bool waitForPipelines) {
|
||||
}
|
||||
settle(header);
|
||||
if (final || profiling()) std::fprintf(stderr, "[pipeline-cache] saved %.1f KiB to %s in %.1f ms%s%s\n", static_cast<double>(size) / 1024.0, path.string().c_str(), std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - started).count(), mergedWithFile ? " (merged with the file)" : "", final ? " (teardown)" : "");
|
||||
} catch (const std::bad_alloc&) {
|
||||
std::fputs("[pipeline-cache] save skipped: host memory exhausted\n", stderr);
|
||||
}
|
||||
|
||||
void PipelineCache::SaveNow() {
|
||||
|
||||
@@ -1440,7 +1440,7 @@ VkCommandBuffer Recorder::Commands(VkAccessFlags* coveredAccess) {
|
||||
// Work recorded after a draw's render pass or an inline store run must see their writes: the
|
||||
// pass's end and the run's trailing barrier go in first (a per-batch run waits for Submit, or
|
||||
// for a caller whose ranges overlap a queued store: FlushStores).
|
||||
if (open->renderPass.open) endOpenRenderPass();
|
||||
if (open->renderPass.open) endOpenRenderPass(PassBreak::Command);
|
||||
if (open->run.open && !LabelRunsPerBatch()) closeStoreRun();
|
||||
if (coveredAccess != nullptr) *coveredAccess = open->coveredAccess;
|
||||
open->coveredAccess = 0;
|
||||
@@ -1455,20 +1455,22 @@ Recorder::DrawPassStart Recorder::StartDrawPass(std::uint64_t key, bool forced,
|
||||
ensureOpen();
|
||||
auto& pass = open->renderPass;
|
||||
if (!pass.open || forced || (countingSamples && segmentMode() && !segmentSlotReady(*open))) {
|
||||
const auto reason = !pass.open ? pass.closedBy : forced ? PassBreak::Forced : PassBreak::Segment;
|
||||
const auto commands = Commands();
|
||||
open->passHazards.Clear();
|
||||
open->passHazards.BeginPass();
|
||||
return {commands, false, true};
|
||||
return {commands, false, true, reason};
|
||||
}
|
||||
const auto hazard = open->passHazards.Check(access, pass.key == key);
|
||||
if (hazard == PassHazard::None && pass.key == key) return {open->commands, true, false};
|
||||
if (hazard == PassHazard::None && pass.key == key) return {open->commands, true, false, PassBreak::Continued};
|
||||
const auto reason = PassBreakFor(hazard);
|
||||
const bool merge = MergeBarriers() && !fenced && !pass.fenced && !pass.afterPass;
|
||||
const bool elide = merge && hazard == PassHazard::None;
|
||||
endOpenRenderPass(!merge);
|
||||
endOpenRenderPass(reason, !merge);
|
||||
const auto commands = Commands();
|
||||
if (!elide) open->passHazards.Clear();
|
||||
open->passHazards.BeginPass();
|
||||
return {commands, false, !elide};
|
||||
return {commands, false, !elide, reason};
|
||||
}
|
||||
|
||||
void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function<void(VkCommandBuffer)> afterPass) {
|
||||
@@ -1488,7 +1490,7 @@ void Recorder::SealPassWriteOnly() {
|
||||
if (open != nullptr) open->passHazards.SealWriteOnly();
|
||||
}
|
||||
|
||||
void Recorder::endOpenRenderPass(bool barrier) {
|
||||
void Recorder::endOpenRenderPass(PassBreak by, bool barrier) {
|
||||
auto& pass = open->renderPass;
|
||||
if (open->segmentActive) endSegment(*open);
|
||||
EndRenderPass(context, open->commands);
|
||||
@@ -1503,6 +1505,7 @@ void Recorder::endOpenRenderPass(bool barrier) {
|
||||
open->coveredAccess = barrier ? VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT : 0;
|
||||
open->hostReadOwed = true;
|
||||
pass = {};
|
||||
pass.closedBy = by;
|
||||
}
|
||||
|
||||
void Recorder::QueueKeyStore(VkBuffer buffer, VkDeviceSize first, VkDeviceSize last, std::shared_ptr<void> seed, std::uint64_t begin, std::uint64_t end) {
|
||||
@@ -1643,7 +1646,7 @@ void Recorder::RecordStore(VkBuffer buffer, VkDeviceSize offset, std::span<const
|
||||
// A label says the work before it is done: the queued key stores (results of that work) land
|
||||
// ahead of every store of the run.
|
||||
if (!open->keyStores.empty()) recordKeyStores(true);
|
||||
if (open->renderPass.open) endOpenRenderPass();
|
||||
if (open->renderPass.open) endOpenRenderPass(PassBreak::Label);
|
||||
open->coveredAccess = 0;
|
||||
// Two transfers to the same bytes have no order of their own within the run.
|
||||
const auto overlapsRecorded = [&] { return std::any_of(run.recorded.begin(), run.recorded.end(), [&](const auto& store) { return std::get<0>(store) == buffer && offset < std::get<2>(store) && std::get<1>(store) < end; }); };
|
||||
@@ -1716,7 +1719,7 @@ bool Recorder::closeStoreRun(bool atSubmit) {
|
||||
// land ahead of every store of the run, and the pass a draw left open ends.
|
||||
if (!open->keyStores.empty()) recordKeyStores(true);
|
||||
const bool passEndLast = open->renderPass.open && !open->renderPass.afterPass;
|
||||
if (open->renderPass.open) endOpenRenderPass();
|
||||
if (open->renderPass.open) endOpenRenderPass(PassBreak::Label);
|
||||
const auto commands = open->commands;
|
||||
run.timing = beginTiming(ClassKey(CommandClass::LabelRun));
|
||||
if (BarrierValidate()) {
|
||||
@@ -2119,7 +2122,7 @@ void Recorder::CountSamples() {
|
||||
gpuSampleCounter();
|
||||
countingSamples = true;
|
||||
if (segmentMode() || open == nullptr || open->samples != VK_NULL_HANDLE) return;
|
||||
if (open->renderPass.open) endOpenRenderPass();
|
||||
if (open->renderPass.open) endOpenRenderPass(PassBreak::Samples);
|
||||
beginSamples(*open);
|
||||
}
|
||||
|
||||
@@ -3630,7 +3633,7 @@ void Recorder::Submit() {
|
||||
if (open == nullptr) return;
|
||||
GuestMemory::AssertGpuLockHeld("Recorder::Submit");
|
||||
if (!open->keyStores.empty()) recordKeyStores(false);
|
||||
if (open->renderPass.open) endOpenRenderPass();
|
||||
if (open->renderPass.open) endOpenRenderPass(PassBreak::NewBatch);
|
||||
const bool segments = segmentMode();
|
||||
if (!segments) {
|
||||
endSamples(*open);
|
||||
|
||||
@@ -6,11 +6,21 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
#ifdef __APPLE__
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
namespace {
|
||||
|
||||
#ifdef __APPLE__
|
||||
void ClearPooledTail(void* mapping, std::size_t size, VkDeviceSize mappedBytes, VkMemoryPropertyFlags properties) {
|
||||
if (mapping == nullptr || (properties & VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) == 0 || mappedBytes <= size) return;
|
||||
std::memset(static_cast<std::byte*>(mapping) + size, 0, static_cast<std::size_t>(mappedBytes - size));
|
||||
}
|
||||
|
||||
#endif
|
||||
VkDeviceSize ClassAllocationBytes(const VkMemoryRequirements& requirements, std::size_t capacity) {
|
||||
const auto alignment = std::max<VkDeviceSize>(requirements.alignment, 1);
|
||||
return std::max<VkDeviceSize>(requirements.size, (static_cast<VkDeviceSize>(capacity) + alignment - 1) / alignment * alignment);
|
||||
@@ -38,7 +48,7 @@ VkBuffer BindExactBuffer(const Context& context, std::size_t size, VkBufferUsage
|
||||
|
||||
}
|
||||
|
||||
Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : context(context), size(size), capacity(BufferPool::Capacity(size, properties)), usage(BufferPool::Usage(usage, properties)), properties(properties) {
|
||||
Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(BufferPool::Usage(usage, properties)), properties(properties) {
|
||||
Require(size != 0, "zero-sized GPU buffer");
|
||||
const bool addressable = (usage & VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT) != 0;
|
||||
Require(!addressable || context.bufferDeviceAddress, "buffer device address is not enabled");
|
||||
@@ -52,6 +62,9 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag
|
||||
capacity = allocation->bytes;
|
||||
offset = allocation->offset;
|
||||
slab = allocation->slab;
|
||||
#ifdef __APPLE__
|
||||
ClearPooledTail(mapping, size, std::min<VkDeviceSize>(capacity, allocationBytes), properties);
|
||||
#endif
|
||||
if (allocation->bufferBytes != size) {
|
||||
context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")(context.device, buffer, nullptr);
|
||||
buffer = VK_NULL_HANDLE;
|
||||
@@ -93,6 +106,9 @@ Buffer::Buffer(const Context& context, std::size_t size, VkBufferUsageFlags usag
|
||||
Check(context.Function<PFN_vkBindBufferMemory>("vkBindBufferMemory")(context.device, buffer, memory, offset), "vkBindBufferMemory slab");
|
||||
initializeAddress(usage);
|
||||
mapping = slot.mapping;
|
||||
#ifdef __APPLE__
|
||||
ClearPooledTail(mapping, size, capacity, properties);
|
||||
#endif
|
||||
ready = true;
|
||||
return;
|
||||
}
|
||||
@@ -144,7 +160,7 @@ void Buffer::Invalidate() {
|
||||
Check(context.Function<PFN_vkInvalidateMappedMemoryRanges>("vkInvalidateMappedMemoryRanges")(context.device, 1, &range), "vkInvalidateMappedMemoryRanges");
|
||||
}
|
||||
|
||||
DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsageFlags usage) : context(context), size(size), capacity(BufferPool::Capacity(size, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)), usage(BufferPool::Usage(usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
|
||||
DeviceBuffer::DeviceBuffer(const Context& context, std::size_t size, VkBufferUsageFlags usage) : context(context), size(size), capacity(BufferPool::Capacity(size)), usage(BufferPool::Usage(usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
|
||||
Require(size != 0, "zero-sized device buffer");
|
||||
cache = GetBufferPool(context);
|
||||
if (const auto allocation = cache->Take(size, this->usage, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)) {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "prx/libc/include/HostThreadLocal.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -27,6 +30,9 @@ constexpr std::uint32_t spiPsInputAddr = 0x1B4;
|
||||
constexpr std::uint32_t spiPsInControl = 0x1B6;
|
||||
constexpr std::uint32_t spiBarycCntl = 0x1B8;
|
||||
constexpr std::uint32_t dbShaderControl = 0x203;
|
||||
constexpr std::uint32_t dbRenderOverride = 0x003;
|
||||
constexpr std::uint32_t paScVportZmin0 = 0x0B4;
|
||||
constexpr std::uint32_t paScVportZmax0 = 0x0B5;
|
||||
constexpr std::uint32_t spiShaderColFormat = 0x1C5;
|
||||
constexpr std::uint32_t defaultPixelInputs = 0x2u;
|
||||
|
||||
@@ -143,6 +149,20 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
|
||||
}
|
||||
const bool pixelKillEnable = ((shaderControl >> 6u) & 0x1u) != 0;
|
||||
const bool depthExportEnable = (shaderControl & 0x1u) != 0;
|
||||
float depthExportMin = 0.0f;
|
||||
float depthExportMax = 1.0f;
|
||||
if (depthExportEnable) {
|
||||
if ((read(context, dbRenderOverride, RegisterBank::Context) & 0x10000u) != 0) {
|
||||
depthExportMin = -std::numeric_limits<float>::infinity();
|
||||
depthExportMax = std::numeric_limits<float>::infinity();
|
||||
} else {
|
||||
depthExportMin = std::bit_cast<float>(read(context, paScVportZmin0, RegisterBank::Context));
|
||||
depthExportMax = std::bit_cast<float>(read(context, paScVportZmax0, RegisterBank::Context));
|
||||
if (!std::isfinite(depthExportMin) || !std::isfinite(depthExportMax) || depthExportMin > depthExportMax) {
|
||||
throw std::runtime_error("AGC graphics: invalid viewport depth clamp bounds for depth export");
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool sampleMaskExportEnable = ((shaderControl >> 8u) & 0x1u) != 0;
|
||||
const bool stencilOperationValue = !nullProgram && StencilOperationValueReplaces(context);
|
||||
const bool stencilRefExportEnable = ((shaderControl >> 1u) & 0x1u) != 0 || stencilOperationValue;
|
||||
@@ -176,6 +196,8 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con
|
||||
.linearCentroid = loaded(PixelInput::LinearCentroid),
|
||||
.pixelKillEnable = pixelKillEnable,
|
||||
.depthExportEnable = depthExportEnable,
|
||||
.depthExportMin = depthExportMin,
|
||||
.depthExportMax = depthExportMax,
|
||||
.sampleMaskExportEnable = sampleMaskExportEnable,
|
||||
.earlyZ = PixelEarlyZ(shaderControl),
|
||||
.executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0,
|
||||
|
||||
@@ -1545,6 +1545,32 @@ bool StorageImageServesKeys(const StorageTexture& image, std::uint64_t dccAddres
|
||||
return !MetadataMoved(image, resource);
|
||||
}
|
||||
|
||||
std::shared_ptr<StorageTexture> RepeatStorageSurface(const Context& context, const GuestTextureResource& requested, const std::shared_ptr<StorageTexture>& stored) {
|
||||
static const bool disabled = std::getenv("APS5_NO_TEXTURE_CACHE") != nullptr;
|
||||
if (disabled || stored == nullptr) return nullptr;
|
||||
const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers);
|
||||
Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented");
|
||||
const auto viewedBytes = DescribeSurface(viewed).guestBytes;
|
||||
RequireNoMultisampleTarget(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), "a storage image access");
|
||||
if (DepthSurfaceStorage(context, {}, viewed, 0) != nullptr) return nullptr;
|
||||
if (DepthPlanesStale()) static_cast<void>(FlushDepthPlanes(viewed.baseAddress, static_cast<std::size_t>(viewedBytes), DepthPlaneScope::Overlapped));
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr) recorder->BoundKeptBytes();
|
||||
const bool profile = LookupOutcomes::Profiled();
|
||||
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
|
||||
const auto resource = StorageSurface(context, viewed);
|
||||
auto& cache = StorageTextures();
|
||||
std::lock_guard lock(cache.mutex);
|
||||
const CacheHold held(storageCacheHeld);
|
||||
if (resource.mipCount > AllocatedLevels(resource)) return nullptr;
|
||||
const auto it = findStorageByImage(cache, context.device, stored.get());
|
||||
if (it == cache.entries.end() || MetadataMoved(*it->texture, resource)) return nullptr;
|
||||
it->texture->Refresh();
|
||||
cache.entries.splice(cache.entries.begin(), cache.entries, it);
|
||||
TextureCounts().storageHits.fetch_add(1, std::memory_order_relaxed);
|
||||
if (profile) LookupOutcomes::Add(LookupOutcomes::StorageHit, start);
|
||||
return it->texture;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
const char* roleName(ShaderRecompiler::DescriptorRole role) {
|
||||
|
||||
@@ -437,8 +437,8 @@ VkStencilOpState stencilFace(std::uint32_t compare, std::uint32_t ops, std::uint
|
||||
DepthTarget decodeDepthPlane(const Registers& cx) {
|
||||
const auto viewRegister = find(cx, 0x002);
|
||||
const auto view = viewRegister == cx.end() ? 0u : viewRegister->second;
|
||||
const auto viewMip = (view >> 26u) & 0xfu;
|
||||
const auto maxMip = (read(cx, 0x010) >> 16u) & 0xfu;
|
||||
const auto viewMip = maxMip == 0 ? 0u : (view >> 26u) & 0xfu;
|
||||
const auto zFormat = read(cx, 0x010) & 3u;
|
||||
const auto stencilRegister = find(cx, 0x011);
|
||||
const auto stencilInfo = stencilRegister == cx.end() ? 0u : stencilRegister->second;
|
||||
@@ -466,7 +466,6 @@ DepthTarget decodeDepthPlane(const Registers& cx) {
|
||||
}
|
||||
const auto size = read(cx, 0x007);
|
||||
depth.extent = {(size & 0x3fffu) + 1u, ((size >> 16u) & 0x3fffu) + 1u};
|
||||
if (maxMip == 0 && viewMip != 0) Require(false, "depth view mip " + std::to_string(viewMip) + " on a surface without mips");
|
||||
if (maxMip != 0) {
|
||||
char message[320];
|
||||
std::snprintf(message, sizeof(message), "depth view mip %u of %u at %ux%u (DB_Z_INFO 0x%08x, DB_DEPTH_VIEW 0x%08x)", viewMip, maxMip + 1u, depth.extent.width, depth.extent.height, read(cx, 0x010), view);
|
||||
@@ -1431,7 +1430,9 @@ void ApplyDepthRemap(const Context& context, State& state) {
|
||||
const auto& viewport = state.viewport;
|
||||
const bool inRange = viewport.minDepth >= 0 && viewport.minDepth <= 1 && viewport.maxDepth >= 0 && viewport.maxDepth <= 1;
|
||||
if (context.depthRangeUnrestricted || inRange) return;
|
||||
Require(state.stages.path == ShaderPath::Vertex && !state.stages.mesh && !state.stages.tessellation, "viewport depth outside [0, 1] is remapped on the vertex path only");
|
||||
const bool vertex = state.stages.path == ShaderPath::Vertex && !state.stages.tessellation;
|
||||
const bool tessellation = state.stages.path == ShaderPath::Tessellation && state.stages.tessellation.has_value();
|
||||
Require((vertex || tessellation) && !state.stages.mesh, "viewport depth outside [0, 1] is remapped on the vertex and tessellation paths only");
|
||||
Require(context.depthClamp, "viewport depth outside [0, 1] needs depth clamping to stand in for the console's clamp");
|
||||
const float clampMin = std::clamp(state.depthClampMin, 0.0f, 1.0f);
|
||||
const float clampMax = std::clamp(state.depthClampMax, 0.0f, 1.0f);
|
||||
@@ -2332,7 +2333,7 @@ std::optional<ColorMetadataPass> DecodeColorMetadataPass(const QueueState& queue
|
||||
Require(target.samples == 1, "CB metadata pass over a multisampled color target, whose texels are on the host, is unsupported (only a CB resolve is modeled)");
|
||||
}
|
||||
Require((read(cx, 0x31c + slot * 0xfu) & 0x10000000u) == 0 || target.dccAddress != 0, "CB metadata pass over a mipmapped DCC color target, whose keys are not modeled");
|
||||
if (target.cmaskAddress != 0 || target.dccAddress != 0) requireCovered(target);
|
||||
if (target.dccAddress != 0 || (target.cmaskAddress != 0 && pass.mode != ColorMetadataPass::Mode::EliminateFastClear)) requireCovered(target);
|
||||
pass.targets.push_back(target);
|
||||
}
|
||||
return pass;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp"
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <chrono>
|
||||
@@ -354,9 +355,11 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
||||
const auto& mips = geometry.mips;
|
||||
const auto arrayLayers = geometry.layers;
|
||||
const auto elementBytes = BytesPerElement(descriptor.format);
|
||||
#ifdef APS5_DEBUG_LOG
|
||||
AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u dim=%d tile=%d format=%u vk=%d element=%u", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, arrayLayers,
|
||||
static_cast<int>(descriptor.dimension), static_cast<int>(descriptor.tileMode), descriptor.format, static_cast<int>(vkFormat), elementBytes);
|
||||
for (const auto& mip : mips) AGC_LOG_DEFERRED(stdout, " mip %ux%u tiled=0x%llx+0x%llx linear=0x%llx+0x%llx blocksPerRow=%u pitch=%u tail=%d", mip.width, mip.height, static_cast<unsigned long long>(mip.tiledOffset), static_cast<unsigned long long>(mip.tiledSize), static_cast<unsigned long long>(mip.linearOffset), static_cast<unsigned long long>(mip.linearSize), mip.blocksPerRow, mip.pitchBytes, mip.tail ? 1 : 0);
|
||||
#endif
|
||||
|
||||
const auto guestBytes = geometry.guestBytes;
|
||||
Require(snapshot.size() == guestBytes, "texture snapshot size mismatch");
|
||||
@@ -642,7 +645,9 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
||||
copyLevels = descriptor.mipCount;
|
||||
copyLayers = geometry.imageLayers;
|
||||
copyElementBytes = BytesPerElement(descriptor.format);
|
||||
#ifdef APS5_DEBUG_LOG
|
||||
AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u repacked copy of storage image (guest format %u -> vk %d)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, descriptor.format, static_cast<int>(format));
|
||||
#endif
|
||||
|
||||
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
imageInfo.flags = descriptor.dimension == TextureDimension::kCube ? VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT : 0u;
|
||||
@@ -701,7 +706,9 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
||||
copyLevels = descriptor.mipCount;
|
||||
copyLayers = geometry.imageLayers;
|
||||
copyElementBytes = BytesPerElement(descriptor.format);
|
||||
#ifdef APS5_DEBUG_LOG
|
||||
AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u block copy of storage image (guest format %u -> vk %d)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, source->Descriptor().format, static_cast<int>(blockFormat));
|
||||
#endif
|
||||
|
||||
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
imageInfo.imageType = VK_IMAGE_TYPE_2D;
|
||||
@@ -759,7 +766,9 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
||||
copyLevels = descriptor.mipCount;
|
||||
copyLayers = geometry.imageLayers;
|
||||
copyElementBytes = BytesPerElement(descriptor.format);
|
||||
#ifdef APS5_DEBUG_LOG
|
||||
AGC_LOG_DEFERRED(stdout, "Texture address=0x%llx %ux%u mips=%u layers=%u depth copy of storage image (vk=%d -> %d)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, copyLayers, static_cast<int>(colorFormat), static_cast<int>(depthFormat));
|
||||
#endif
|
||||
|
||||
const bool depth1D = descriptor.dimension == TextureDimension::k1D || descriptor.dimension == TextureDimension::k1DArray;
|
||||
VkImageCreateInfo imageInfo{VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO};
|
||||
@@ -4502,11 +4511,78 @@ bool StorageDumpSelects(const GuestTextureResource& descriptor) {
|
||||
return selector.width != 0 && descriptor.width == selector.width && descriptor.height == selector.height && descriptor.depthOrLastArray + 1 == selector.layers && (selector.anyFormat || descriptor.format == selector.format);
|
||||
}
|
||||
|
||||
bool StorageDumpReady() {
|
||||
const char* gate = std::getenv("APS5_DUMP_STORAGE_GATE");
|
||||
std::error_code error;
|
||||
return gate == nullptr || std::filesystem::exists(gate, error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void StorageTexture::FlushDumpSelected(const char* reason) {
|
||||
const auto& selector = StorageDumpSelection();
|
||||
if (selector.address == 0 && selector.width == 0) return;
|
||||
if (!StorageDumpReady()) return;
|
||||
if (std::getenv("APS5_DUMP_STORAGE_GATE") != nullptr) {
|
||||
std::lock_guard gpu(GuestMemory::GpuMutex());
|
||||
static std::vector<std::uint64_t> captured;
|
||||
std::vector<std::shared_ptr<StorageTexture>> selected;
|
||||
{
|
||||
auto& live = Live();
|
||||
std::lock_guard lock(live.mutex);
|
||||
for (auto* texture : live.textures) {
|
||||
if (selected.size() + captured.size() >= 8) break;
|
||||
if (!StorageDumpSelects(texture->descriptor) || texture->sliceLinearBytes * texture->arrayLayers > (8u << 20u)) continue;
|
||||
if (std::find(captured.begin(), captured.end(), texture->descriptor.baseAddress) != captured.end()) continue;
|
||||
if (auto alive = texture->weak_from_this().lock()) selected.push_back(std::move(alive));
|
||||
}
|
||||
}
|
||||
if (!selected.empty()) {
|
||||
if (auto* recorder = Recorder::Active(); recorder != nullptr && recorder->Recording()) recorder->Submit();
|
||||
for (const auto& texture : selected) {
|
||||
const auto& context = texture->context;
|
||||
Check(context.Function<PFN_vkDeviceWaitIdle>("vkDeviceWaitIdle")(context.device), "private image readback wait");
|
||||
Buffer host(context, static_cast<std::size_t>(texture->sliceLinearBytes * texture->arrayLayers), VK_BUFFER_USAGE_TRANSFER_DST_BIT);
|
||||
CommandBatch batch(context);
|
||||
const auto commands = batch.Handle();
|
||||
VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER};
|
||||
barrier.srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
|
||||
barrier.image = texture->image;
|
||||
barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, texture->descriptor.mipCount, 0, texture->geometry.imageLayers};
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier);
|
||||
const auto regions = texture->CopyRegions();
|
||||
context.Function<PFN_vkCmdCopyImageToBuffer>("vkCmdCopyImageToBuffer")(commands, texture->image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, host.Handle(), static_cast<std::uint32_t>(regions.size()), regions.data());
|
||||
barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
|
||||
barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT | VK_ACCESS_MEMORY_WRITE_BIT;
|
||||
barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
|
||||
barrier.newLayout = VK_IMAGE_LAYOUT_GENERAL;
|
||||
const auto hostBarrier = WholeBufferBarrier(host.Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT | VK_PIPELINE_STAGE_HOST_BIT, 0, 0, nullptr, 1, &hostBarrier, 1, &barrier);
|
||||
batch.SubmitAndWait();
|
||||
char name[80];
|
||||
std::snprintf(name, sizeof(name), "storage_direct_%llx_%zu.raw", static_cast<unsigned long long>(texture->descriptor.baseAddress), captured.size());
|
||||
std::FILE* file = std::fopen(name, "wb");
|
||||
Require(file != nullptr, "cannot open private image readback file");
|
||||
const auto& mip = texture->mips[0];
|
||||
const std::uint32_t header[3] = {mip.pitchBytes / static_cast<std::uint32_t>(BytesPerElement(texture->descriptor.format)), mip.height, static_cast<std::uint32_t>(texture->storageFormat)};
|
||||
bool written = std::fwrite(header, sizeof(header), 1, file) == 1;
|
||||
for (std::uint32_t layer = 0; layer < texture->arrayLayers; ++layer) {
|
||||
if (!texture->geometry.HasLayer(0, layer)) continue;
|
||||
written = std::fwrite(host.Bytes().data() + texture->geometry.LinearLayerOffset(layer) + mip.linearOffset, 1, static_cast<std::size_t>(mip.linearSize), file) == mip.linearSize && written;
|
||||
}
|
||||
const bool closed = std::fclose(file) == 0;
|
||||
Require(written && closed, "cannot complete private image readback file");
|
||||
captured.push_back(texture->descriptor.baseAddress);
|
||||
std::fprintf(stderr, "[storage-dump] direct=%s bytes=%llu\n", name, static_cast<unsigned long long>(mip.linearSize * texture->arrayLayers));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
std::vector<std::pair<std::uint64_t, std::size_t>> ranges;
|
||||
{
|
||||
auto& pending = Pending();
|
||||
@@ -6287,7 +6363,7 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
detiler.Dispatch(commands, descriptor.tileMode, elementBytes, linear.Handle(), geometry.LinearLayerOffset(layer) + mip.linearOffset, tiled.Handle(), geometry.GuestLayerOffset(layer) + mip.tiledOffset, mip, true, layer, geometry.thick, {.pipeBankXor = descriptor.pipeBankXor});
|
||||
}
|
||||
}
|
||||
const bool dumpSelected = StorageDumpSelects(descriptor);
|
||||
const bool dumpSelected = StorageDumpSelects(descriptor) && StorageDumpReady();
|
||||
static int dumps = 0;
|
||||
std::unique_ptr<Buffer> dump;
|
||||
if (dumpSelected && dumps < 8) {
|
||||
@@ -6306,9 +6382,9 @@ void StorageTexture::writeBackLayers(const std::vector<bool>& layers) {
|
||||
context.Function<PFN_vkCmdPipelineBarrier>("vkCmdPipelineBarrier")(commands, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT | VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, nullptr, 1, &toCopy, 1, &backToGeneral);
|
||||
CopyBuffer(context, commands, tiled.Handle(), 0, host.Handle(), 0, original.size());
|
||||
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_HOST_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_HOST_READ_BIT);
|
||||
APS5_LOG_CHARS_OUT("StorageTexture writeback submit");
|
||||
APS5_LOG_CHARS_OUT_DEBUG("StorageTexture writeback submit");
|
||||
batch.SubmitAndWait();
|
||||
APS5_LOG_CHARS_OUT("StorageTexture writeback done");
|
||||
APS5_LOG_CHARS_OUT_DEBUG("StorageTexture writeback done");
|
||||
std::uint64_t storedBytes = 0;
|
||||
for (const auto& [from, to] : stores) storedBytes += to - from;
|
||||
countStorageWriteBack(drawn.rect ? storedBytes : guestBytes, false, drawn.drawn);
|
||||
|
||||
@@ -378,12 +378,14 @@ std::size_t publishUnits(const std::shared_ptr<UnitShadow>& shadow, std::uint64_
|
||||
std::lock_guard lock(registry.mutex);
|
||||
std::vector<std::uint8_t> changed, cpu;
|
||||
changedUnits(*shadow, first, last, changed, cpu);
|
||||
std::uint32_t ended = 0;
|
||||
for (auto unit = first; unit <= last; ++unit) {
|
||||
const auto k = static_cast<std::size_t>(unit - first);
|
||||
if (!selected[k] || !shadow->Live(unit)) continue;
|
||||
if (changed[k] == GuestMemory::BlockWritten) {
|
||||
// Newer bytes reached the import since the retile: the slab's are dead.
|
||||
shadow->generation[unit] = 0;
|
||||
++ended;
|
||||
(cpu[k] != 0 ? Stats().staleDroppedCpu : Stats().staleDroppedDriver).fetch_add(1, std::memory_order_relaxed);
|
||||
continue;
|
||||
}
|
||||
@@ -391,6 +393,7 @@ std::size_t publishUnits(const std::shared_ptr<UnitShadow>& shadow, std::uint64_
|
||||
const auto& slab = shadow->slabs[slabIndex];
|
||||
if (slab == nullptr) {
|
||||
shadow->generation[unit] = 0;
|
||||
++ended;
|
||||
continue;
|
||||
}
|
||||
const auto [begin, end] = shadow->UnitRange(unit);
|
||||
@@ -405,11 +408,12 @@ std::size_t publishUnits(const std::shared_ptr<UnitShadow>& shadow, std::uint64_
|
||||
ranges.emplace_back(begin, end);
|
||||
}
|
||||
shadow->published[unit] = 1;
|
||||
++ended;
|
||||
slab->lastUse = Recorder::Presents();
|
||||
bytes += end - begin;
|
||||
++units;
|
||||
}
|
||||
shadow->Recount();
|
||||
shadow->liveUnits -= ended;
|
||||
}
|
||||
if (units == 0) return 0;
|
||||
Stats().published[static_cast<std::size_t>(reason)].fetch_add(1, std::memory_order_relaxed);
|
||||
@@ -800,12 +804,12 @@ void MarkShadowed(const HostImport& import, std::span<const ShadowedRange> range
|
||||
++lost;
|
||||
continue;
|
||||
}
|
||||
if (!shadow->Live(unit)) ++shadow->liveUnits;
|
||||
shadow->generation[unit] = generation;
|
||||
shadow->published[unit] = 0;
|
||||
current->lastUse = present;
|
||||
}
|
||||
}
|
||||
shadow->Recount();
|
||||
}
|
||||
if (lost != 0) {
|
||||
Stats().slabLost.fetch_add(lost, std::memory_order_relaxed);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
@@ -10,7 +11,7 @@
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAgcDriverSubmitAcb(uint32_t queue, const Packet* packet) {
|
||||
if (queue < 0x20 || queue >= 0x58) {
|
||||
if (queue < 0x20 || queue >= AgcDriver::DriverDetail::SubmissionQueueEnd) {
|
||||
throw std::runtime_error(std::string(__func__) + ": unsupported compute queue");
|
||||
}
|
||||
AgcDriver::Submit(packet, queue);
|
||||
@@ -19,7 +20,7 @@ int APS5_VABI sceAgcDriverSubmitAcb(uint32_t queue, const Packet* packet) {
|
||||
|
||||
int APS5_VABI sceAgcDriverSubmitMultiAcbs(uint32_t queue, uint32_t* const* acbs, const uint32_t* sizes_in_dwords, uint32_t count) {
|
||||
if (count == 0) return 0;
|
||||
if (queue < 0x20 || queue >= 0x58) {
|
||||
if (queue < 0x20 || queue >= AgcDriver::DriverDetail::UnmeasuredSubmissionQueueEnd) {
|
||||
throw std::runtime_error(std::string(__func__) + ": unsupported compute queue");
|
||||
}
|
||||
return AgcDriver::SubmitMany(acbs, sizes_in_dwords, count, queue);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "prx/libSceAgcDriver/Submit/include/CommandBuffer.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Queues/SubmissionRing.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
@@ -15,7 +16,7 @@ extern "C" {
|
||||
int APS5_VABI sceAgcDriverSubmitCommandBuffer(void* queue_context, const Packet* packet) {
|
||||
if (!queue_context || !packet) return EINVAL;
|
||||
const auto queue = *reinterpret_cast<const std::uint32_t*>(static_cast<const char*>(queue_context) + 4);
|
||||
if (queue != 0 && (queue < 0x20 || queue >= 0x58)) {
|
||||
if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::UnmeasuredSubmissionQueueEnd)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": unsupported queue");
|
||||
}
|
||||
AgcDriver::Submit(packet, queue);
|
||||
@@ -26,7 +27,7 @@ int APS5_VABI sceAgcDriverSubmitMultiCommandBuffers(void* queue_context, uint32_
|
||||
if (count == 0) return 0;
|
||||
if (!queue_context || !command_buffers || !sizes_in_dwords) return EINVAL;
|
||||
const auto queue = *reinterpret_cast<const std::uint32_t*>(static_cast<const char*>(queue_context) + 4);
|
||||
if (queue != 0 && (queue < 0x20 || queue >= 0x58)) {
|
||||
if (queue != 0 && (queue < 0x20 || queue >= AgcDriver::DriverDetail::UnmeasuredSubmissionQueueEnd)) {
|
||||
throw std::runtime_error(std::string(__func__) + ": unsupported queue");
|
||||
}
|
||||
return AgcDriver::SubmitMany(command_buffers, sizes_in_dwords, count, queue);
|
||||
|
||||
@@ -686,6 +686,8 @@ void importCoverageHintBudgetTests(const Context& context, const BdaTestAccess&
|
||||
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);
|
||||
NoteHostImportFrame();
|
||||
NoteHostImportFrame();
|
||||
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");
|
||||
@@ -733,6 +735,8 @@ void failedImportJoinProbeBudgetTests(const Context& context, const BdaTestAcces
|
||||
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);
|
||||
NoteHostImportFrame();
|
||||
NoteHostImportFrame();
|
||||
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");
|
||||
@@ -1157,6 +1161,8 @@ void importAtMapOverBudgetTests(const Context& context) {
|
||||
leased.Upload(false);
|
||||
leased.WriteBack();
|
||||
}
|
||||
NoteHostImportFrame();
|
||||
NoteHostImportFrame();
|
||||
const auto live = HostImportLiveBytes();
|
||||
SetHostImportBudget(live + bytes - 1);
|
||||
change(mapped, true);
|
||||
@@ -1176,6 +1182,57 @@ void importAtMapOverBudgetTests(const Context& context) {
|
||||
for (auto* block : {kept, used, mapped}) ::operator delete(block, std::align_val_t{bytes});
|
||||
}
|
||||
|
||||
// Three ranges under a budget for two, each used by a build of its own in every frame, with more lookups between the
|
||||
// uses than a frame's worth of imports. The two imported first keep their imports and the third is refused at every
|
||||
// frame, instead of evicting one of the others (which then evicts the next, every frame). A range unused for two frames
|
||||
// is evicted for the third.
|
||||
void importEvictionFramesTests(const Context& context) {
|
||||
constexpr std::size_t bytes = 65536;
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = bytes;
|
||||
std::array<void*, 3> blocks{};
|
||||
std::array<std::uintptr_t, 3> at{};
|
||||
const auto change = [&](void* block, bool add) {
|
||||
auto* mutation = GuestAllocations::GuestAllocationsBegin_nid_postfix();
|
||||
if (add) GuestAllocations::GuestAllocationsAdd_nid_postfix(mutation, block, bytes, true, false, true);
|
||||
else GuestAllocations::GuestAllocationsRemove_nid_postfix(mutation, block);
|
||||
GuestAllocations::GuestAllocationsEnd_nid_postfix(mutation);
|
||||
};
|
||||
for (std::size_t index = 0; index < blocks.size(); ++index) {
|
||||
blocks[index] = ::operator new(bytes, std::align_val_t{bytes});
|
||||
at[index] = reinterpret_cast<std::uintptr_t>(blocks[index]);
|
||||
change(blocks[index], true);
|
||||
}
|
||||
SetHostImportBudget(HostImportLiveBytes() + 2 * bytes);
|
||||
const auto use = [&](std::size_t index) {
|
||||
GuestBufferMemory leased(importing);
|
||||
leased.AddReadable(at[index], 32);
|
||||
leased.Upload(false);
|
||||
leased.WriteBack();
|
||||
for (int lookup = 0; lookup < 300; ++lookup) static_cast<void>(HostImportFor(importing, at[index], 16));
|
||||
return HostImportSerial(importing, at[index], 16, false);
|
||||
};
|
||||
NoteHostImportFrame();
|
||||
NoteHostImportFrame();
|
||||
std::array<std::uint64_t, 2> serials{use(0), use(1)};
|
||||
Require(serials[0] != 0 && serials[1] != 0, "two ranges under a budget for two were not imported");
|
||||
const auto epoch = HostImportEpoch();
|
||||
NoteHostImportFrame();
|
||||
Require(HostImportEpoch() == epoch, "a frame end with no import change moved the imports epoch");
|
||||
for (int frame = 0; frame < 10; ++frame) {
|
||||
Require(use(2) == 0, "a range over the budget evicted an import used in the same frame (frame " + std::to_string(frame) + ")");
|
||||
NoteHostImportFrame();
|
||||
Require(use(0) == serials[0] && use(1) == serials[1], "an import used in every frame was imported again (frame " + std::to_string(frame) + ")");
|
||||
}
|
||||
NoteHostImportFrame();
|
||||
Require(use(1) == serials[1] && use(2) == 0, "a range over the budget evicted an import used in the previous frame");
|
||||
NoteHostImportFrame();
|
||||
Require(use(1) == serials[1] && use(2) != 0 && !HostImportCovers(importing, at[0], 16), "an import unused for two frames was not evicted for a range over the budget");
|
||||
SetHostImportBudget(0);
|
||||
for (auto* block : blocks) change(block, false);
|
||||
for (auto* block : blocks) ::operator delete(block, std::align_val_t{bytes});
|
||||
}
|
||||
|
||||
void importRequesterTests(const Context& context, const BdaTestAccess& access) {
|
||||
ClearHostImports(context.device);
|
||||
constexpr std::size_t piece = 65536;
|
||||
@@ -1669,6 +1726,8 @@ void evictingRefusalMemoryTests(const Context& context) {
|
||||
}
|
||||
before = counted();
|
||||
Require(HostImportFor(importing, address(filled + 2), 16) == nullptr && counted().remembered == before.remembered + 1, "a remembered evicting refusal ended when a build raised the floor while no import could age out of its protection yet");
|
||||
NoteHostImportFrame();
|
||||
NoteHostImportFrame();
|
||||
for (int use = 0; use < 200; ++use) Require(HostImportFor(importing, address(filled + 1), 16) != nullptr, "a live import was not found");
|
||||
before = counted();
|
||||
Require(HostImportFor(importing, address(filled + 2), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after the imports aged out of their protection");
|
||||
@@ -1705,6 +1764,8 @@ void evictingRefusalAgingTests(const Context& context) {
|
||||
static_cast<void>(HostImportFor(importing, address(filled), 16));
|
||||
auto before = counted();
|
||||
Require(HostImportFor(importing, address(filled), 16) == nullptr && counted().remembered == before.remembered + 1, "a refusal while the imports were recent but no longer held by a build was not remembered");
|
||||
NoteHostImportFrame();
|
||||
NoteHostImportFrame();
|
||||
for (int use = 0; use < 200; ++use) Require(HostImportFor(importing, address(filled - 1), 16) != nullptr, "a live import was not found");
|
||||
before = counted();
|
||||
Require(HostImportFor(importing, address(filled), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after imports no build held aged out of their protection");
|
||||
@@ -1868,6 +1929,42 @@ void importDeviceBudgetTests() {
|
||||
Require(HostImportBudgetWith(5888 * MiB, false, 0, true, 0) != 0, "a host heap report that leaves imports no room made the default import budget 0, which the refusal memory reads as no budget");
|
||||
}
|
||||
|
||||
void importAllocationErrorTests(const Context& context, VkResult failure, bool nearBudget) {
|
||||
constexpr std::size_t piece = 65536;
|
||||
constexpr std::size_t count = 8;
|
||||
auto* block = static_cast<std::uint8_t*>(::operator new(2 * count * piece, std::align_val_t{piece}));
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = piece;
|
||||
ImportAllocationProbe probe(importing);
|
||||
SetHostImportWindow(0);
|
||||
ClearHostImports(importing.device);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
for (std::size_t index = 0; index < count; ++index) mutation.Add(block + 2 * index * piece, piece, true, true, true);
|
||||
}
|
||||
SetHostImportBudget((nearBudget ? 2 : 64) * piece);
|
||||
std::size_t next = 0;
|
||||
const auto lookup = [&] { return HostImportFor(importing, reinterpret_cast<std::uintptr_t>(block + 2 * next++ * piece), 16) != nullptr; };
|
||||
const auto counted = [] { return HostImportCounters().requesters[static_cast<std::size_t>(ImportRequester::Access)]; };
|
||||
Require(lookup(), "the allocation error test's first import was not made");
|
||||
probe.failure = failure;
|
||||
const auto before = counted();
|
||||
for (int attempt = 0; attempt < 3; ++attempt) Require(!lookup(), "an import with an injected allocation failure was made");
|
||||
const auto after = counted();
|
||||
probe.failure = VK_SUCCESS;
|
||||
const bool exhausted = failure == VK_ERROR_OUT_OF_DEVICE_MEMORY || failure == VK_ERROR_OUT_OF_HOST_MEMORY;
|
||||
const auto failed = exhausted && nearBudget ? 1u : 3u;
|
||||
Require(after.failed - before.failed == failed && after.refused - before.refused == 3 - failed, "allocation error " + std::to_string(failure) + " changed the import failure/refusal counts");
|
||||
Require(lookup() != exhausted, "allocation error " + std::to_string(failure) + " incorrectly changed the host import budget");
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
for (std::size_t index = 0; index < count; ++index) mutation.Remove(block + 2 * index * piece);
|
||||
}
|
||||
ClearHostImports(importing.device);
|
||||
SetHostImportBudget(0);
|
||||
::operator delete(block, std::align_val_t{piece});
|
||||
}
|
||||
|
||||
void importCeilingTests(const Context& context, const BdaTestAccess& access, bool nearBudget) {
|
||||
constexpr std::size_t piece = 65536;
|
||||
constexpr std::size_t count = 24;
|
||||
@@ -1931,7 +2028,7 @@ void VKAPI_PTR reportHostHeap(VkPhysicalDevice, VkPhysicalDeviceMemoryProperties
|
||||
}
|
||||
}
|
||||
|
||||
void importReportFloorTests(const Context& context, const BdaTestAccess& access, bool setBudget) {
|
||||
void importReportFloorTests(const Context& context, bool setBudget, VkResult failure) {
|
||||
constexpr std::size_t piece = 65536;
|
||||
constexpr std::size_t count = 24;
|
||||
constexpr std::uint64_t GiB = std::uint64_t{1} << 30u;
|
||||
@@ -1939,6 +2036,7 @@ void importReportFloorTests(const Context& context, const BdaTestAccess& access,
|
||||
auto* block = static_cast<std::uint8_t*>(::operator new(2 * count * piece, std::align_val_t{piece}));
|
||||
auto importing = context;
|
||||
importing.hostImportAlignment = piece;
|
||||
ImportAllocationProbe probe(importing);
|
||||
importing.memory.memoryHeapCount = 2;
|
||||
importing.memory.memoryHeaps[0] = {8 * GiB, VK_MEMORY_HEAP_DEVICE_LOCAL_BIT};
|
||||
importing.memory.memoryHeaps[1] = {16 * GiB, 0};
|
||||
@@ -1957,11 +2055,10 @@ void importReportFloorTests(const Context& context, const BdaTestAccess& access,
|
||||
const auto lookup = [&] { return HostImportFor(importing, reinterpret_cast<std::uintptr_t>(block + 2 * next++ * piece), 16) != nullptr; };
|
||||
const std::string budget = setBudget ? "a set import budget" : "the default import budget";
|
||||
for (int made = 0; made < 3; ++made) Require(lookup(), budget + " was lowered below " + std::to_string(made + 1) + " imports by a host heap report that leaves imports no room, before any vkAllocateMemory failure");
|
||||
access.limitImports(0);
|
||||
Require(!lookup(), "an import past the mock's import limit was made");
|
||||
access.limitImports(std::nullopt);
|
||||
if (setBudget) Require(lookup(), "a set import budget took the host heap report's limit after a vkAllocateMemory failure far below it");
|
||||
else Require(!lookup(), "the default import budget did not take the host heap report's limit after a vkAllocateMemory failure");
|
||||
probe.failure = failure;
|
||||
Require(!lookup(), "an import with an injected allocation failure was made");
|
||||
probe.failure = VK_SUCCESS;
|
||||
Require(lookup() == setBudget, budget + " incorrectly applied the host heap report after allocation error " + std::to_string(failure));
|
||||
SetHostImportBudget(0);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
@@ -3556,7 +3653,7 @@ void RunHeapMirrorRunUnwatchTests(const Context& context, const BdaTestAccess& a
|
||||
std::cout << "an address-based build refreshes the unwatched heap of a run in full and its watched neighbour by its writes\n";
|
||||
}
|
||||
|
||||
void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access, bool remembered) {
|
||||
void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access, bool remembered, int frames) {
|
||||
constexpr std::size_t block = 65536;
|
||||
constexpr std::size_t heapBytes = 32 * block;
|
||||
constexpr std::size_t idleBytes = 16 * block;
|
||||
@@ -3651,6 +3748,7 @@ void RunAddressCopyCapTests(const Context& context, const BdaTestAccess& access,
|
||||
SetAddressCopyLimit(0);
|
||||
unrelated(true);
|
||||
}
|
||||
for (int frame = 0; frame < frames; ++frame) NoteHostImportFrame();
|
||||
for (int use = 0; use < 300; ++use) Require(HostImportFor(importing, busy, 64) != nullptr, "the busy range lost its import");
|
||||
if (!remembered) unwatch();
|
||||
const auto refused = builds();
|
||||
@@ -4288,10 +4386,17 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
|
||||
importDeviceBudgetTests();
|
||||
importCeilingTests(context, access, false);
|
||||
importCeilingTests(context, access, true);
|
||||
importReportFloorTests(context, access, true);
|
||||
importReportFloorTests(context, access, false);
|
||||
for (const auto failure : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_INVALID_EXTERNAL_HANDLE, VK_ERROR_UNKNOWN}) {
|
||||
importAllocationErrorTests(context, failure, false);
|
||||
importAllocationErrorTests(context, failure, true);
|
||||
}
|
||||
for (const auto failure : {VK_ERROR_OUT_OF_HOST_MEMORY, VK_ERROR_OUT_OF_DEVICE_MEMORY, VK_ERROR_INVALID_EXTERNAL_HANDLE, VK_ERROR_UNKNOWN}) {
|
||||
importReportFloorTests(context, true, failure);
|
||||
importReportFloorTests(context, false, failure);
|
||||
}
|
||||
importReportRefusalMemoryTests(context, access);
|
||||
sparseImportedHeapTests(context, access);
|
||||
importEvictionFramesTests(context);
|
||||
heapMirrorTests(context, access);
|
||||
partialHeapImportBudgetTests(context, access);
|
||||
importedHeapMirrorTests(context, access);
|
||||
|
||||
@@ -21,10 +21,11 @@ void RunHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, cons
|
||||
void RunSparseHeapMirrorUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access);
|
||||
void RunSparseHeapMirrorBudgetTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access);
|
||||
void RunHeapMirrorRunUnwatchTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access);
|
||||
void RunAddressCopyCapTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool remembered);
|
||||
void RunAddressCopyCapTests(const AgcDriver::Graphics::Context& context, const BdaTestAccess& access, bool remembered, int frames);
|
||||
void RunGuestAllocationTests();
|
||||
void RunGuestLeaseWaitTests();
|
||||
void RunUnmappedGapTests();
|
||||
void RunVanishedPageReadTests();
|
||||
void RunColorTargetLayoutTests();
|
||||
void RunLiveStackAccessTests();
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@ void Budget() {
|
||||
Expect(MemoryOf(probe.Handle()) != first, "the least recently used device buffer survived the eviction");
|
||||
}
|
||||
|
||||
void AddressAndHostUnchanged() {
|
||||
void AddressAndHostClasses() {
|
||||
mock = MockDevice{};
|
||||
auto context = mockContext();
|
||||
VkDeviceMemory scratch = VK_NULL_HANDLE;
|
||||
@@ -426,17 +426,23 @@ void AddressAndHostUnchanged() {
|
||||
Expect(MemoryOf(addressed.Handle()) != scratch && addressed.DeviceAddress() != 0, "an addressable device-local buffer took a scratch buffer without a device address");
|
||||
Buffer shadow(context, 2 * MiB - 256, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
|
||||
Expect(MemoryOf(shadow.Handle()) == scratch, "a device-local staging buffer did not share the scratch buffers' class");
|
||||
VkBuffer host = VK_NULL_HANDLE;
|
||||
VkDeviceMemory host = VK_NULL_HANDLE;
|
||||
{
|
||||
Buffer copy(context, 10 * MiB + 4096, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible);
|
||||
host = copy.Handle();
|
||||
Expect(mock.sizes.at(host) == 10 * MiB + 4096, "a large host buffer was rounded to a class");
|
||||
Expect(mock.usages.at(host) == VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "a host buffer was created with more usages than asked");
|
||||
host = MemoryOf(copy.Handle());
|
||||
Expect(mock.sizes.at(copy.Handle()) == 10 * MiB + 4096, "a 10 MiB + 4 KiB host buffer was created with " + std::to_string(mock.sizes.at(copy.Handle())) + " bytes, not the requested size");
|
||||
Expect(ClassOf(copy.Handle()) == 11 * MiB, "a 10 MiB + 4 KiB host buffer has " + std::to_string(ClassOf(copy.Handle())) + " bytes of memory, not its 11 MiB class");
|
||||
Expect(mock.usages.at(copy.Handle()) == VK_BUFFER_USAGE_TRANSFER_SRC_BIT, "a host buffer was created with more usages than asked");
|
||||
Expect(copy.Bytes().size() == 10 * MiB + 4096, "a host buffer maps its class instead of the requested size");
|
||||
}
|
||||
Buffer other(context, 10 * MiB + 8192, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible);
|
||||
Expect(other.Handle() != host, "a large host buffer of another exact size was reused");
|
||||
Buffer same(context, 10 * MiB + 4096, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible);
|
||||
Expect(same.Handle() == host, "a large host buffer of the same exact size was not reused");
|
||||
const auto made = mock.allocations;
|
||||
{
|
||||
Buffer other(context, 10 * MiB + 8192, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible);
|
||||
Expect(MemoryOf(other.Handle()) == host && mock.allocations == made, "a retained 11 MiB host buffer did not serve another size in its class");
|
||||
Expect(mock.sizes.at(other.Handle()) == 10 * MiB + 8192 && other.Bytes().size() == 10 * MiB + 8192, "a reused host buffer kept the size of the earlier one");
|
||||
}
|
||||
Buffer larger(context, 11 * MiB + 4096, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, HostVisible);
|
||||
Expect(MemoryOf(larger.Handle()) != host && mock.allocations == made + 1, "a host buffer was served by a retained buffer of a smaller class");
|
||||
}
|
||||
|
||||
void SmallDeviceClasses() {
|
||||
@@ -1198,7 +1204,7 @@ int main(int argc, char** argv) {
|
||||
KeptUntilFence();
|
||||
SmallDeviceRetention();
|
||||
Budget();
|
||||
AddressAndHostUnchanged();
|
||||
AddressAndHostClasses();
|
||||
SmallDeviceClasses();
|
||||
SlabMetadataFailure();
|
||||
SlabMapFailure();
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
if(NOT DEFINED REPLAY OR NOT DEFINED OUTPUT_ROOT)
|
||||
message(FATAL_ERROR "Missing replay tool or output root")
|
||||
endif()
|
||||
string(RANDOM LENGTH 12 ALPHABET 0123456789abcdef runId)
|
||||
set(runDirectory "${OUTPUT_ROOT}/compute-inspect-${runId}")
|
||||
file(MAKE_DIRECTORY "${runDirectory}")
|
||||
if(NOT DEFINED ENV{ANYPS5_REQUIRE_VULKAN} AND DEFINED VULKAN_PROBE)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "${VULKAN_PROBE}"
|
||||
WORKING_DIRECTORY "${runDirectory}"
|
||||
RESULT_VARIABLE probeResult OUTPUT_VARIABLE probeOutput ERROR_VARIABLE probeError TIMEOUT 20
|
||||
)
|
||||
file(WRITE "${runDirectory}/vulkan-probe.log" "${probeOutput}\n${probeError}")
|
||||
if(probeResult STREQUAL "77")
|
||||
message(STATUS "compute pipeline inspection: skipped, no usable Vulkan device")
|
||||
return()
|
||||
endif()
|
||||
endif()
|
||||
set(report "${runDirectory}/inspection.json")
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=1" "${REPLAY}" --fixture --inspect --output "${report}"
|
||||
WORKING_DIRECTORY "${runDirectory}"
|
||||
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20
|
||||
)
|
||||
file(WRITE "${runDirectory}/inspection.log" "${output}\n${error}")
|
||||
message("${error}")
|
||||
if(error MATCHES "APS5_PIPELINE_STATS requires VK_KHR_pipeline_executable_properties")
|
||||
message(STATUS "compute pipeline inspection: skipped, the device reports no executable statistics")
|
||||
return()
|
||||
endif()
|
||||
if(NOT result STREQUAL "0" OR NOT EXISTS "${report}")
|
||||
message(FATAL_ERROR "Inspection failed: ${result}\n${output}\n${error}")
|
||||
endif()
|
||||
file(READ "${report}" inspection)
|
||||
string(JSON mode GET "${inspection}" mode)
|
||||
string(JSON statistics GET "${inspection}" pipeline_statistics)
|
||||
string(JSON timingType TYPE "${inspection}" milliseconds)
|
||||
string(JSON flags GET "${inspection}" replay_pipeline_flags)
|
||||
string(JSON dispatches GET "${inspection}" dispatch_count)
|
||||
math(EXPR statisticsFlag "${flags} & 64")
|
||||
if(NOT mode STREQUAL "inspection" OR NOT statistics OR NOT timingType STREQUAL "NULL" OR statisticsFlag EQUAL 0 OR NOT dispatches EQUAL 0)
|
||||
message(FATAL_ERROR "Inspection report mixes counters and benchmark data: ${inspection}")
|
||||
endif()
|
||||
if(NOT error MATCHES "\\[pipeline-stats\\] executable=0 stages=")
|
||||
message(FATAL_ERROR "Inspection did not query an executable: ${error}")
|
||||
endif()
|
||||
set(normalReport "${runDirectory}/normal.json")
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=0" "${REPLAY}" --fixture --output "${normalReport}"
|
||||
WORKING_DIRECTORY "${runDirectory}"
|
||||
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 20
|
||||
)
|
||||
file(WRITE "${runDirectory}/normal.log" "${output}\n${error}")
|
||||
message("${error}")
|
||||
if(NOT result STREQUAL "0" OR NOT EXISTS "${normalReport}")
|
||||
message(FATAL_ERROR "Normal replay failed: ${result}\n${output}\n${error}")
|
||||
endif()
|
||||
file(READ "${normalReport}" normal)
|
||||
string(JSON mode GET "${normal}" mode)
|
||||
string(JSON statistics GET "${normal}" pipeline_statistics)
|
||||
string(JSON timingType TYPE "${normal}" milliseconds)
|
||||
string(JSON originalIdentity GET "${inspection}" capture_identity)
|
||||
string(JSON normalIdentity GET "${normal}" capture_identity)
|
||||
string(JSON originalFlags GET "${inspection}" pipeline_flags)
|
||||
string(JSON normalFlags GET "${normal}" pipeline_flags)
|
||||
if(NOT mode STREQUAL "benchmark" OR statistics OR NOT timingType STREQUAL "OBJECT" OR NOT originalIdentity STREQUAL normalIdentity OR NOT originalFlags EQUAL normalFlags)
|
||||
message(FATAL_ERROR "Replay mode changed the input identity or benchmark report")
|
||||
endif()
|
||||
function(expectFailure name statistics expected)
|
||||
execute_process(
|
||||
COMMAND "${CMAKE_COMMAND}" -E env "APS5_PIPELINE_STATS=${statistics}" "${REPLAY}" ${ARGN}
|
||||
WORKING_DIRECTORY "${runDirectory}"
|
||||
RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE error TIMEOUT 10
|
||||
)
|
||||
file(WRITE "${runDirectory}/${name}.log" "${output}\n${error}")
|
||||
if(result STREQUAL "0" OR NOT error MATCHES "${expected}")
|
||||
message(FATAL_ERROR "${name} was not rejected explicitly: ${result}\n${output}\n${error}")
|
||||
endif()
|
||||
endfunction()
|
||||
expectFailure(benchmark-guard 1 "unset APS5_PIPELINE_STATS before benchmarking" --fixture)
|
||||
expectFailure(missing-statistics 0 "requires APS5_PIPELINE_STATS=1" --fixture --inspect)
|
||||
expectFailure(repeated-mode 1 "unknown or repeated argument" --fixture --inspect --inspect)
|
||||
expectFailure(fixture-options 1 "cannot be combined with replay options" --write-fixture "${runDirectory}/fixture" --inspect)
|
||||
message(STATUS "compute pipeline inspection: counters queried, input identity retained, timings separated and benchmark guards preserved")
|
||||
@@ -210,6 +210,8 @@ enum class QuadMask {
|
||||
Nested,
|
||||
SeparateHalves,
|
||||
RealOnly,
|
||||
RealPartial,
|
||||
RealDivergent,
|
||||
Partial,
|
||||
WrongHalf,
|
||||
Divergent,
|
||||
@@ -225,7 +227,7 @@ bool quadProjection(std::uint32_t wave, QuadMask shape, bool expected, IrShaderS
|
||||
IrBuilder builder(program);
|
||||
builder.SetInsertionPoint(block);
|
||||
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
|
||||
if (shape == QuadMask::Divergent) {
|
||||
if (shape == QuadMask::Divergent || shape == QuadMask::RealDivergent) {
|
||||
auto& taken = program.CreateBlock();
|
||||
auto& other = program.CreateBlock();
|
||||
program.BlockOrder().push_back(&taken);
|
||||
@@ -245,17 +247,18 @@ bool quadProjection(std::uint32_t wave, QuadMask shape, bool expected, IrShaderS
|
||||
return std::array<IrValue*, 2>{&builder.CompositeExtract(mask, 0u), &builder.CompositeExtract(mask, 1u)};
|
||||
};
|
||||
auto& helper = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast<std::uint32_t>(StageInputKind::HelperInvocation)), &builder.Constant(0u)});
|
||||
const auto real = ballot(builder.IEqual(helper, builder.Constant(0u)));
|
||||
auto& realCondition = builder.IEqual(helper, builder.Constant(0u));
|
||||
const auto real = ballot(realCondition);
|
||||
const auto quads = expand(real);
|
||||
const auto host = expand(ballot(builder.ConstantBool(true)));
|
||||
auto words = quads;
|
||||
if (shape == QuadMask::Intersected || shape == QuadMask::Nested || shape == QuadMask::SeparateHalves || shape == QuadMask::RealOnly) {
|
||||
if (shape == QuadMask::Intersected || shape == QuadMask::Nested || shape == QuadMask::SeparateHalves || shape == QuadMask::RealOnly || shape == QuadMask::RealPartial || shape == QuadMask::RealDivergent) {
|
||||
const auto highHost = shape == QuadMask::SeparateHalves ? expand(ballot(builder.ConstantBool(true))) : host;
|
||||
const auto source = shape == QuadMask::Nested || shape == QuadMask::RealOnly ? real : quads;
|
||||
const auto source = shape == QuadMask::Nested || shape == QuadMask::RealOnly || shape == QuadMask::RealPartial || shape == QuadMask::RealDivergent ? real : quads;
|
||||
words = {&builder.BitwiseAnd(*source[0], *host[0]), &builder.BitwiseAnd(*source[1], *highHost[1])};
|
||||
if (shape == QuadMask::Nested) words = expand(words);
|
||||
}
|
||||
if (shape == QuadMask::Partial) {
|
||||
if (shape == QuadMask::Partial || shape == QuadMask::RealPartial) {
|
||||
words = {&builder.BitwiseAnd(*words[0], builder.Constant(0x0000ffffu)), &builder.Constant(0u)};
|
||||
}
|
||||
if (shape == QuadMask::WrongHalf) words[1] = words[0];
|
||||
@@ -266,7 +269,8 @@ bool quadProjection(std::uint32_t wave, QuadMask shape, bool expected, IrShaderS
|
||||
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
|
||||
ConstantFolder{}.Fold(program);
|
||||
const auto* result = user.Argument(0)->Resolve();
|
||||
return expected ? result->HasImmediate() && result->ImmediateBool() : !result->HasImmediate();
|
||||
if (shape == QuadMask::RealOnly) return result == &realCondition;
|
||||
return expected ? result->HasImmediate() && result->ImmediateBool() : result->Opcode() == IrOpcode::INotEqual32;
|
||||
}
|
||||
|
||||
bool run(const char* name, bool passed) {
|
||||
@@ -284,8 +288,10 @@ int main() {
|
||||
passed &= run("intersecting whole-quad entry masks preserves pixel helpers", quadProjection(wave, QuadMask::Intersected, true));
|
||||
passed &= run("a nested whole-quad entry mask covers pixel helpers", quadProjection(wave, QuadMask::Nested, true));
|
||||
passed &= run("independent low and high whole-quad masks cover pixel helpers", quadProjection(wave, QuadMask::SeparateHalves, true));
|
||||
passed &= run("an unexpanded entry mask must keep the helper bit test", quadProjection(wave, QuadMask::RealOnly, false));
|
||||
passed &= run("an unexpanded entry mask must reuse the original helper test", quadProjection(wave, QuadMask::RealOnly, false));
|
||||
passed &= run("a partial whole-quad mask must keep the lane bit test", quadProjection(wave, QuadMask::Partial, false));
|
||||
passed &= run("a partial real-lane mask must keep the lane bit test", quadProjection(wave, QuadMask::RealPartial, false));
|
||||
passed &= run("a divergent real-lane ballot must keep the lane bit test", quadProjection(wave, QuadMask::RealDivergent, false));
|
||||
passed &= run("a divergent whole-quad ballot must keep the lane bit test", quadProjection(wave, QuadMask::Divergent, false));
|
||||
passed &= run("whole-quad entry proofs do not apply to other stages", quadProjection(wave, QuadMask::Intersected, false, IrShaderStage::Vertex));
|
||||
}
|
||||
|
||||
@@ -51,6 +51,16 @@ VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL mockProc(VkDevice, const char* name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL reloadedQueueWaitIdle(VkQueue queue) {
|
||||
events.push_back(Name("reloaded idle ", queue));
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL reloadedProc(VkDevice device, const char* name) {
|
||||
if (std::strcmp(name, "vkQueueWaitIdle") == 0) return reinterpret_cast<PFN_vkVoidFunction>(&reloadedQueueWaitIdle);
|
||||
return mockProc(device, name);
|
||||
}
|
||||
|
||||
Context ContextFor(VkDevice device, PFN_vkGetDeviceProcAddr proc) {
|
||||
Context context{};
|
||||
context.device = device;
|
||||
@@ -115,12 +125,36 @@ void OtherDevicesDestroyAtOnce() {
|
||||
DestroyDeferredObjects(device);
|
||||
}
|
||||
|
||||
PFN_vkGetDeviceProcAddr TrySetUp(VkDevice device, PFN_vkGetDeviceProcAddr resolver) {
|
||||
try {
|
||||
return DeferDestructionToIdle(device, resolver);
|
||||
} catch (const std::exception&) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void ALaterDeviceUsesItsOwnResolver() {
|
||||
events.clear();
|
||||
const auto first = Handle<VkDevice>(0x103);
|
||||
const auto firstProc = DeferDestructionToIdle(first, mockProc);
|
||||
DestroyDeferredObjects(first);
|
||||
const auto second = Handle<VkDevice>(0x104);
|
||||
const auto proc = TrySetUp(second, reloadedProc);
|
||||
Expect(proc != nullptr, "a device set up after the last one went could not use a resolver from a reloaded Vulkan loader");
|
||||
if (proc == nullptr) return;
|
||||
Expect(ContextFor(second, proc).Function<PFN_vkQueueWaitIdle>("vkQueueWaitIdle") == &reloadedQueueWaitIdle, "a later device did not pass through to its own resolver");
|
||||
Expect(reinterpret_cast<PFN_vkQueueWaitIdle>(firstProc(first, "vkQueueWaitIdle")) == &mockQueueWaitIdle, "an earlier device's wrapper passed through to a later device's resolver");
|
||||
Expect(TrySetUp(Handle<VkDevice>(0x105), mockProc) == nullptr, "two live devices with different resolvers were accepted");
|
||||
DestroyDeferredObjects(second);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
DeferredUntilTheQueueIsIdle();
|
||||
OtherDevicesDestroyAtOnce();
|
||||
ALaterDeviceUsesItsOwnResolver();
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", error.what());
|
||||
return 1;
|
||||
|
||||
@@ -75,11 +75,13 @@ ModuleFacts inspect(const ShaderRecompiler::RecompileResult& result) {
|
||||
}
|
||||
|
||||
constexpr std::array<std::uint32_t, 3> PositionExport{0xf80008cfu, 0u, 0xbf810000u};
|
||||
constexpr std::array<std::uint32_t, 5> DomainPositionExport{0xf8000941u, 0u, 0xf80008cfu, 0x06050605u, 0xbf810000u};
|
||||
|
||||
ShaderRecompiler::RecompileRequest baseRequest(bool remap) {
|
||||
ShaderRecompiler::RecompileRequest baseRequest(bool remap, ShaderRecompiler::ShaderStage stage = ShaderRecompiler::ShaderStage::Vertex) {
|
||||
using namespace ShaderRecompiler;
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Vertex, 0x10000u, PositionExport, 0, {}};
|
||||
const auto code = stage == ShaderStage::TessellationEvaluation ? std::span<const std::uint32_t>(DomainPositionExport) : std::span<const std::uint32_t>(PositionExport);
|
||||
request.shader = {stage, 0x10000u, code, 0, {}};
|
||||
request.context.waveSize = 64;
|
||||
ShaderVertexStageInfo vertex{};
|
||||
vertex.depthRemapMode = 2u;
|
||||
@@ -87,9 +89,13 @@ ShaderRecompiler::RecompileRequest baseRequest(bool remap) {
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 64;
|
||||
static constexpr std::array<std::uint32_t, 2> capabilities{spv::CapabilityShader, spv::CapabilityClipDistance};
|
||||
static constexpr std::array<std::uint32_t, 3> capabilities{spv::CapabilityShader, spv::CapabilityClipDistance, spv::CapabilityTessellation};
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.depthRangeRemap = remap;
|
||||
if (stage == ShaderStage::TessellationEvaluation) {
|
||||
request.target.tessellation = TessellationTargetLimits{32u, 128u, 128u, 128u, 4096u, 128u, 128u};
|
||||
request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {0u, 3u, 0u, 1u}};
|
||||
}
|
||||
request.layout.pushConstantSizeBytes = ShaderRecompiler::DepthRemapPushOffsetBytes;
|
||||
request.useCache = false;
|
||||
return request;
|
||||
@@ -99,9 +105,9 @@ void require(bool condition, const char* what) {
|
||||
if (!condition) throw std::runtime_error(what);
|
||||
}
|
||||
|
||||
void preparedTransformTests() {
|
||||
void preparedTransformTests(ShaderRecompiler::ShaderStage stage) {
|
||||
using namespace ShaderRecompiler;
|
||||
auto request = baseRequest(true);
|
||||
auto request = baseRequest(true, stage);
|
||||
request.useCache = true;
|
||||
const auto handle = PrepareShader(request);
|
||||
const auto materialize = [&](std::uint32_t mode) {
|
||||
@@ -120,15 +126,15 @@ void preparedTransformTests() {
|
||||
require(!PreparedShaderInvocation::TryCreate(other, handle).has_value(), "a prepared vertex shader matched a request of another depth remap mode");
|
||||
}
|
||||
|
||||
void pushedTransformTests() {
|
||||
void pushedTransformTests(ShaderRecompiler::ShaderStage stage) {
|
||||
using namespace ShaderRecompiler;
|
||||
const auto remapped = Recompile(baseRequest(true));
|
||||
const auto plain = Recompile(baseRequest(false));
|
||||
const auto remapped = Recompile(baseRequest(true, stage));
|
||||
const auto plain = Recompile(baseRequest(false, stage));
|
||||
const auto facts = inspect(remapped);
|
||||
require(!facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 1u) && !facts.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 2u), "the depth remap scale or offset is a specialization constant");
|
||||
require(facts.pushConstantVariable && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u) && facts.pushDwordLoads.contains(DepthRemapPushOffsetBytes / 4u + 1u), "the remapped vertex shader does not read the scale and offset from the push block");
|
||||
require(!inspect(plain).pushConstantVariable, "a vertex shader without a depth remap gained a push constant block");
|
||||
auto crowded = baseRequest(true);
|
||||
auto crowded = baseRequest(true, stage);
|
||||
crowded.layout.pushConstantSizeBytes = DepthRemapPushOffsetBytes + 4u;
|
||||
bool refused = false;
|
||||
try {
|
||||
@@ -139,18 +145,41 @@ void pushedTransformTests() {
|
||||
require(refused, "a remapped vertex shader with push data over the depth remap words was accepted");
|
||||
}
|
||||
|
||||
void clipDistanceCapacityTests(ShaderRecompiler::ShaderStage stage) {
|
||||
using namespace ShaderRecompiler;
|
||||
static constexpr std::array<std::uint32_t, 7> vertex{0xf80008cfu, 0u, 0xf80008dfu, 0u, 0xf80008efu, 0u, 0xbf810000u};
|
||||
static constexpr std::array<std::uint32_t, 9> domain{0xf8000941u, 0u, 0xf80008cfu, 0x05050505u, 0xf80008dfu, 0x05050505u, 0xf80008efu, 0x05050505u, 0xbf810000u};
|
||||
auto request = baseRequest(true, stage);
|
||||
request.shader.code = stage == ShaderStage::TessellationEvaluation ? std::span<const std::uint32_t>(domain) : std::span<const std::uint32_t>(vertex);
|
||||
request.context.vertex->paClVsOutCntl = 0xc0003fu;
|
||||
require(inspect(Recompile(request)).clipDistanceLength == 8u, "the remap did not preserve six guest clip distances beside its two planes");
|
||||
for (const auto control : {0xc0007fu, 0xc0403fu, 0xc000ffu}) {
|
||||
request.context.vertex->paClVsOutCntl = control;
|
||||
bool refused = false;
|
||||
try {
|
||||
static_cast<void>(Recompile(request));
|
||||
} catch (const std::runtime_error& error) {
|
||||
refused = std::string(error.what()).find("two free clip-distance components") != std::string::npos;
|
||||
}
|
||||
require(refused, "a remap that exceeds the combined clip and cull distance capacity was accepted");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
using namespace ShaderRecompiler;
|
||||
try {
|
||||
const auto remapped = inspect(Recompile(baseRequest(true)));
|
||||
const auto plain = inspect(Recompile(baseRequest(false)));
|
||||
require(remapped.clipDistanceBuiltIn && remapped.clipDistanceLength == plain.clipDistanceLength + 2u, "the remap did not add its two clip distances");
|
||||
require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w");
|
||||
require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants");
|
||||
preparedTransformTests();
|
||||
pushedTransformTests();
|
||||
for (const auto stage : {ShaderStage::Vertex, ShaderStage::TessellationEvaluation}) {
|
||||
const auto remapped = inspect(Recompile(baseRequest(true, stage)));
|
||||
const auto plain = inspect(Recompile(baseRequest(false, stage)));
|
||||
require(remapped.clipDistanceBuiltIn && remapped.clipDistanceLength == plain.clipDistanceLength + 2u, "the remap did not add its two clip distances");
|
||||
require(remapped.multiplies >= plain.multiplies + 2u, "the remap does not scale the clip z and offset it by w");
|
||||
require(!plain.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase) && !remapped.specializationDefaults.contains(PipelineSpecialization::DepthRemapBase + 3u), "unexpected specialization constants");
|
||||
preparedTransformTests(stage);
|
||||
pushedTransformTests(stage);
|
||||
clipDistanceCapacityTests(stage);
|
||||
}
|
||||
std::cout << "Depth range remap tests passed\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
@@ -158,6 +159,21 @@ VKAPI_ATTR VkResult VKAPI_CALL mapMemory(VkDevice, VkDeviceMemory, VkDeviceSize,
|
||||
return VK_ERROR_MEMORY_MAP_FAILED;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL unmapMemory(VkDevice, VkDeviceMemory) {}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL createDescriptorSetLayout(VkDevice, const VkDescriptorSetLayoutCreateInfo*, const VkAllocationCallbacks*, VkDescriptorSetLayout* layout) {
|
||||
*layout = newHandle<VkDescriptorSetLayout>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL createPipelineLayout(VkDevice, const VkPipelineLayoutCreateInfo*, const VkAllocationCallbacks*, VkPipelineLayout* layout) {
|
||||
*layout = newHandle<VkPipelineLayout>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR VkResult VKAPI_CALL createShaderModule(VkDevice, const VkShaderModuleCreateInfo*, const VkAllocationCallbacks*, VkShaderModule* module) {
|
||||
*module = newHandle<VkShaderModule>();
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
VKAPI_ATTR void VKAPI_CALL destroyDescriptorSetLayout(VkDevice, VkDescriptorSetLayout, const VkAllocationCallbacks*) {}
|
||||
VKAPI_ATTR void VKAPI_CALL destroyPipelineLayout(VkDevice, VkPipelineLayout, const VkAllocationCallbacks*) {}
|
||||
VKAPI_ATTR void VKAPI_CALL destroyShaderModule(VkDevice, VkShaderModule, const VkAllocationCallbacks*) {}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL formatProperties(VkPhysicalDevice, VkFormat, VkFormatProperties* properties) {
|
||||
*properties = {};
|
||||
@@ -216,7 +232,13 @@ PFN_vkVoidFunction VKAPI_CALL deviceProc(VkDevice, const char* name) {
|
||||
{"vkBindBufferMemory", reinterpret_cast<PFN_vkVoidFunction>(bindBufferMemory)},
|
||||
{"vkDestroyBuffer", reinterpret_cast<PFN_vkVoidFunction>(destroyBuffer)},
|
||||
{"vkMapMemory", reinterpret_cast<PFN_vkVoidFunction>(mapMemory)},
|
||||
{"vkUnmapMemory", reinterpret_cast<PFN_vkVoidFunction>(unmapMemory)}
|
||||
{"vkUnmapMemory", reinterpret_cast<PFN_vkVoidFunction>(unmapMemory)},
|
||||
{"vkCreateDescriptorSetLayout", reinterpret_cast<PFN_vkVoidFunction>(createDescriptorSetLayout)},
|
||||
{"vkCreatePipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(createPipelineLayout)},
|
||||
{"vkCreateShaderModule", reinterpret_cast<PFN_vkVoidFunction>(createShaderModule)},
|
||||
{"vkDestroyDescriptorSetLayout", reinterpret_cast<PFN_vkVoidFunction>(destroyDescriptorSetLayout)},
|
||||
{"vkDestroyPipelineLayout", reinterpret_cast<PFN_vkVoidFunction>(destroyPipelineLayout)},
|
||||
{"vkDestroyShaderModule", reinterpret_cast<PFN_vkVoidFunction>(destroyShaderModule)}
|
||||
};
|
||||
const auto it = table.find(name);
|
||||
return it == table.end() ? nullptr : it->second;
|
||||
@@ -467,6 +489,8 @@ void TestReadOnlyStalePlaneFlush(const Context& context) {
|
||||
*properties = {};
|
||||
properties->optimalTilingFeatures = VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT;
|
||||
};
|
||||
TextureDetiler detiler(storageContext);
|
||||
storageContext.detiler = &detiler;
|
||||
DepthTarget target{address, 0, extent, VK_FORMAT_D32_SFLOAT, 1.0f, 0};
|
||||
target.depthSwizzle = XorSwizzleMode(TextureTileMode::kZ64KBX);
|
||||
Require(DepthSurfaceView(storageContext, target, true) != VK_NULL_HANDLE, "a read-only depth plane must render");
|
||||
|
||||
@@ -17,7 +17,8 @@ constexpr Case Cases[]{
|
||||
{VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true},
|
||||
{VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true},
|
||||
{VK_DRIVER_ID_MESA_RADV, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false},
|
||||
{VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, false, false},
|
||||
{VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, false},
|
||||
{VK_DRIVER_ID_NVIDIA_PROPRIETARY, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL, true, true},
|
||||
{VK_DRIVER_ID_AMD_PROPRIETARY, VK_TRUE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_32_BIT_ONLY, true, true},
|
||||
{static_cast<VkDriverId>(0), VK_FALSE, VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE, false, false},
|
||||
};
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "prx/libc/include/GuestHeap.hpp"
|
||||
#include "ImageTableAbi.hpp"
|
||||
#include "SpirvBackend/SpirvSpecialization.hpp"
|
||||
#include <limits>
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
@@ -103,6 +104,7 @@ AgcDriver::QueueState makeState() {
|
||||
queue.context[0x114] = 0;
|
||||
queue.context[0xb4] = 0;
|
||||
queue.context[0xb5] = std::bit_cast<std::uint32_t>(1.0f);
|
||||
queue.context[0x3] = 0;
|
||||
queue.shader[0x008] = 0x100;
|
||||
queue.shader[0x009] = 0;
|
||||
return queue;
|
||||
@@ -313,6 +315,9 @@ void retainedPixelStageTests() {
|
||||
Require(!PixelProgramSkipped(queue), "an enabled high render target lost its pixel program");
|
||||
queue.context[0x8e] = queue.context[0x8f] = 0;
|
||||
}
|
||||
queue.context[0x3] = 0;
|
||||
queue.context[0xb4] = std::bit_cast<std::uint32_t>(0.0f);
|
||||
queue.context[0xb5] = std::bit_cast<std::uint32_t>(1.0f);
|
||||
queue.context[0x1b3] = queue.context[0x1b4] = 2;
|
||||
for (const auto [format, control] : {std::pair{1u, 1u}, std::pair{2u, 2u}, std::pair{9u, 0x100u}, std::pair{9u, 0x103u}}) {
|
||||
queue.context[0x1c4] = format;
|
||||
@@ -322,6 +327,25 @@ void retainedPixelStageTests() {
|
||||
const auto pixel = DecodePixelStageInfo(queue.context, ExportMappings(state), PixelProgramSkipped(queue));
|
||||
Require(pixel.depthExportEnable == ((control & 1u) != 0) && pixel.stencilRefExportEnable == ((control & 2u) != 0) && pixel.sampleMaskExportEnable == ((control & 0x100u) != 0), "decoding lost an enabled pixel export");
|
||||
}
|
||||
queue.context[0x1c4] = 1;
|
||||
queue.context[0x203] = 1;
|
||||
queue.context[0xb4] = std::bit_cast<std::uint32_t>(0.25f);
|
||||
queue.context[0xb5] = std::bit_cast<std::uint32_t>(0.75f);
|
||||
const auto clampState = DecodeState(queue);
|
||||
const auto clamped = DecodePixelStageInfo(queue.context, ExportMappings(clampState));
|
||||
Require(clamped.depthExportEnable && clamped.depthExportMin == 0.25f && clamped.depthExportMax == 0.75f, "PA_SC_VPORT_ZMIN_0/ZMAX_0 did not become the exported depth clamp");
|
||||
queue.context[0x3] = 0x10000;
|
||||
const auto unclamped = DecodePixelStageInfo(queue.context, ExportMappings(clampState));
|
||||
Require(unclamped.depthExportEnable && unclamped.depthExportMin == -std::numeric_limits<float>::infinity() && unclamped.depthExportMax == std::numeric_limits<float>::infinity(), "DB_RENDER_OVERRIDE.DISABLE_VIEWPORT_CLAMP did not leave the exported depth unclamped");
|
||||
queue.context[0x3] = 0;
|
||||
queue.context[0xb4] = std::bit_cast<std::uint32_t>(0.9f);
|
||||
expectFailure([&] { static_cast<void>(DecodePixelStageInfo(queue.context, ExportMappings(clampState))); }, "invalid viewport depth clamp bounds");
|
||||
queue.context[0xb4] = std::bit_cast<std::uint32_t>(std::numeric_limits<float>::quiet_NaN());
|
||||
expectFailure([&] { static_cast<void>(DecodePixelStageInfo(queue.context, ExportMappings(clampState))); }, "invalid viewport depth clamp bounds");
|
||||
queue.context[0x1c4] = 0;
|
||||
queue.context[0x203] = 0x10;
|
||||
const auto notExporting = DecodePixelStageInfo(queue.context, ExportMappings(clampState));
|
||||
Require(!notExporting.depthExportEnable && notExporting.depthExportMin == 0.0f && notExporting.depthExportMax == 1.0f, "a draw without depth export took the viewport depth clamp");
|
||||
queue.context[0x203] = 0;
|
||||
for (const auto alpha : {0xaa00u, 0xaa01u}) {
|
||||
queue.context[0x2dc] = alpha;
|
||||
@@ -1898,7 +1922,8 @@ void DisabledColorTests() {
|
||||
const auto thirdSlice = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(thirdSlice.depth && thirdSlice.depth->firstSlice == 3 && thirdSlice.depth->address == depthOnly.depth->address + 3u * AgcDriver::Graphics::DepthSliceBytes(depthOnly.depth->extent, 4u), "a depth view of slice 3 lost its slice or its Z offset");
|
||||
queue.context[0x002] = 1u << 26u;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "on a surface without mips");
|
||||
const auto singleLevelMip = AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(singleLevelMip.depth && singleLevelMip.depth->address == depthOnly.depth->address && singleLevelMip.depth->extent.width == depthOnly.depth->extent.width && singleLevelMip.depth->extent.height == depthOnly.depth->extent.height, "a depth view mip on a single-level surface did not select its first level");
|
||||
queue.context[0x002] = 0;
|
||||
{
|
||||
using AgcDriver::Graphics::ComputeElementMipLayout;
|
||||
@@ -3174,6 +3199,9 @@ void metadataPassTests() {
|
||||
queue.context[0x324] = 0;
|
||||
pass = DecodeColorMetadataPass(queue);
|
||||
Require(pass.has_value() && pass->targets.size() == 1 && pass->targets[0].dccAddress == keysAddress, "the metadata pass lost the target's DCC keys");
|
||||
auto partial = queue;
|
||||
partial.context[0x10f] = std::bit_cast<std::uint32_t>(16.0f);
|
||||
expectFailure([&] { DecodeColorMetadataPass(partial); }, "over part of a color target");
|
||||
auto mipmapped = queue;
|
||||
mipmapped.context[0x3b0] |= 1u << 28u;
|
||||
expectFailure([&] { DecodeColorMetadataPass(mipmapped); }, "mipmapped DCC");
|
||||
@@ -3397,6 +3425,11 @@ void cmaskTests() {
|
||||
queue.context[0x202] = 0xcc0020;
|
||||
auto pass = AgcDriver::Graphics::DecodeColorMetadataPass(queue);
|
||||
Require(pass.has_value() && pass->targets.size() == 1 && pass->targets[0].cmaskAddress == cmaskAddress, "the metadata pass lost the target's CMASK");
|
||||
auto partial = queue;
|
||||
partial.context[0x10f] = std::bit_cast<std::uint32_t>(16.0f);
|
||||
Require(AgcDriver::Graphics::DecodeColorMetadataPass(partial).has_value(), "a fast-clear eliminate of a CMASK target without DCC keys over part of it was refused");
|
||||
partial.context[0x202] = 0xcc0060;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeColorMetadataPass(partial); }, "over part of a color target");
|
||||
const AgcDriver::Graphics::Context context{};
|
||||
const auto texels = [&](std::uint32_t value) {
|
||||
for (std::size_t offset = 0; offset < colorMemory.size(); offset += 4) {
|
||||
@@ -4331,6 +4364,31 @@ void multisampleDccStateTests() {
|
||||
target.cmaskFastClear = true;
|
||||
expectFailure([&] { static_cast<void>(AgcDriver::Graphics::MultisampledDccClear(target, dcc, cmask)); }, "multisampled DCC CMASK compression or register clear");
|
||||
}
|
||||
for (const auto samples : {2u, 4u, 8u}) {
|
||||
for (const auto format : {VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB}) {
|
||||
for (const auto key : {0x00u, 0x40u, 0x80u, 0xc0u}) {
|
||||
for (const auto alphaOnMsb : {false, true}) {
|
||||
AgcDriver::Graphics::ColorTarget target{};
|
||||
target.extent = {64, 64};
|
||||
target.format = format;
|
||||
target.tileMode = AgcDriver::Graphics::ColorTileMode::RenderTarget;
|
||||
target.elementBytes = 4;
|
||||
target.samples = samples;
|
||||
target.dccAddress = 0x200000u;
|
||||
target.dccPipeAligned = true;
|
||||
target.dccAlphaOnMsb = alphaOnMsb;
|
||||
const std::vector<std::byte> dcc(AgcDriver::Graphics::MultisampledDccBytes(target), static_cast<std::byte>(key));
|
||||
const auto clear = AgcDriver::Graphics::MultisampledDccClear(target, dcc, {});
|
||||
const auto alpha = alphaOnMsb ? 3u : format == VK_FORMAT_B8G8R8A8_SRGB ? 2u : 0u;
|
||||
for (std::uint32_t channel = 0; channel < 4; ++channel) {
|
||||
const bool one = channel == alpha ? key == 0x40u || key == 0xc0u : key == 0x80u || key == 0xc0u;
|
||||
Require(clear.float32[channel] == static_cast<float>(one), "an sRGB multisampled DCC endpoint did not produce its exact linear clear value");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void multisampledDepthTests() {
|
||||
@@ -4719,6 +4777,17 @@ void DepthRemapTests() {
|
||||
floating.depthWrite = true;
|
||||
AgcDriver::Graphics::ApplyDepthRemap(remapping, floating);
|
||||
Require(floating.depthRemap.mode == 1 && floating.viewport.minDepth == 0 && floating.viewport.maxDepth == 1 && floating.depthClamp, "a written float depth target refused clipped clamp bounds");
|
||||
auto tessellated = AgcDriver::Graphics::DecodeState(queue);
|
||||
tessellated.stages.path = AgcDriver::Graphics::ShaderPath::Tessellation;
|
||||
tessellated.stages.tessellation = ShaderRecompiler::TessellationConfiguration{3u, 3u, 1u, 2u, 2u};
|
||||
auto nativeTessellation = tessellated;
|
||||
AgcDriver::Graphics::ApplyDepthRemap(unrestricted, nativeTessellation);
|
||||
Require(nativeTessellation.depthRemap.mode == 0 && nativeTessellation.viewport.minDepth == -0.5f && nativeTessellation.viewport.maxDepth == 1.5f, "an unrestricted tessellation draw was remapped");
|
||||
AgcDriver::Graphics::ApplyDepthRemap(remapping, tessellated);
|
||||
Require(tessellated.depthRemap.mode == 1 && tessellated.depthRemap.scale == 2.0f && tessellated.depthRemap.offset == -0.5f && tessellated.depthClamp, "a tessellation draw did not remap its depth range");
|
||||
auto combined = nativeTessellation;
|
||||
combined.stages.mesh = ShaderRecompiler::MeshConfiguration{};
|
||||
expectFailure([&] { AgcDriver::Graphics::ApplyDepthRemap(remapping, combined); }, "vertex and tessellation paths only");
|
||||
}
|
||||
|
||||
void DepthBoundsEmulationTests() {
|
||||
@@ -8448,7 +8517,7 @@ void barycentricEmulationTests() {
|
||||
for (std::uint32_t location = 0; location < 11u; ++location) crowded.push_back({location, location, false, true});
|
||||
expectFailure([&] { static_cast<void>(LayoutBarycentricEmulation(crowded, {true, false, false})); }, "exceed 32 locations");
|
||||
|
||||
const auto compile = [&](std::span<const std::uint32_t> code, bool barycentric, std::uint32_t secondInput = 0xffffffffu) {
|
||||
const auto compile = [&](std::span<const std::uint32_t> code, bool barycentric, std::uint32_t secondInput = 0xffffffffu, bool builtins = false) {
|
||||
auto queue = makeState();
|
||||
queue.context[0x1b3] = 0x2u;
|
||||
queue.context[0x1b4] = 0x2u;
|
||||
@@ -8463,6 +8532,7 @@ void barycentricEmulationTests() {
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 64;
|
||||
request.target.fragmentShaderBarycentricEnabled = barycentric;
|
||||
request.target.fragmentBarycentricBuiltins = builtins;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
return Recompile(request);
|
||||
@@ -8510,6 +8580,15 @@ void barycentricEmulationTests() {
|
||||
static constexpr std::array<std::uint32_t, 6> sharedSlot{0xc8080000u, 0xc8090001u, 0xc80e0402u, 0xf800180fu, 0x03030202u, 0xbf810000u};
|
||||
const auto shared = compile(sharedSlot, false, 0x400u);
|
||||
Require(shared.barycentricEmulation.active && !barycentricCapability(shared), "inputs sharing a slot with different interpolation did not emulate per-vertex parameters");
|
||||
static constexpr std::array<std::uint32_t, 5> interpolatesAndReadsIj{0xc8080000u, 0xc8090001u, 0xf800180fu, 0x02020100u, 0xbf810000u};
|
||||
for (const auto& code : {std::span<const std::uint32_t>(readsIj), std::span<const std::uint32_t>(interpolatesAndReadsIj)}) {
|
||||
const auto builtin = compile(code, false, 0xffffffffu, true);
|
||||
const auto builtinDecorations = decorations(builtin, spv::DecorationBuiltIn);
|
||||
Require(!builtin.barycentricEmulation.active && barycentricCapability(builtin) && std::find(builtinDecorations.begin(), builtinDecorations.end(), static_cast<std::uint32_t>(spv::BuiltInBaryCoordKHR)) != builtinDecorations.end() && decorations(builtin, spv::DecorationPerVertexKHR).empty(), "reading the I/J VGPRs with barycentric built-ins did not use BaryCoordKHR");
|
||||
Require(std::ranges::none_of(builtin.fragmentParameters, [](const FragmentParameter& parameter) { return parameter.perVertex; }), "barycentric built-ins made a parameter per-vertex");
|
||||
}
|
||||
Require(compile(interpolatesAndReadsIj, false).barycentricEmulation.active, "reading the I/J VGPRs next to v_interp_p1/p2 without the extension did not emulate barycentrics");
|
||||
Require(compile(readsVertex, false, 0xffffffffu, true).barycentricEmulation.active, "v_interp_mov of P10 with only barycentric built-ins did not emulate per-vertex parameters");
|
||||
fragment = compile(readsVertex, false);
|
||||
Require(fragment.barycentricEmulation.active && !fragment.barycentricEmulation.smooth && fragment.fragmentParameters.size() == 1u && fragment.fragmentParameters[0].perVertex, "v_interp_mov of P10 did not emulate per-vertex parameters");
|
||||
Require(decorations(fragment, spv::DecorationLocation) == std::vector<std::uint32_t>{0u} && decorations(fragment, spv::DecorationPerVertexKHR).empty() && !decorations(fragment, spv::DecorationFlat).empty() && !barycentricCapability(fragment), "the emulated per-vertex parameter was not a flat array at the first free location");
|
||||
@@ -11873,6 +11952,57 @@ void pipelineStoreGrowthTests(bool fixed) {
|
||||
Require(mock.live == 0, "pipeline store growth tests leaked Vulkan objects");
|
||||
}
|
||||
|
||||
void pipelineStoreTouchTests() {
|
||||
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 = 401;
|
||||
fragment.variantId = 402;
|
||||
const std::array<CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, PipelinePushSlotBytes}}};
|
||||
std::array<std::uint32_t, 1> mainKey{51}, fragmentKey{52};
|
||||
const PipelineSetLayouts resources{makeHandle<VkDescriptorSetLayout>(), makeHandle<VkDescriptorSetLayout>(), true, mainKey, fragmentKey};
|
||||
const auto layout = [](std::uint32_t configuration) {
|
||||
VertexInputLayout input;
|
||||
input.bindings.push_back({0, 4u * (configuration + 1u), VK_VERTEX_INPUT_RATE_VERTEX});
|
||||
return input;
|
||||
};
|
||||
std::uint64_t hash = 0;
|
||||
std::weak_ptr<Pipeline> touched;
|
||||
std::weak_ptr<Pipeline> other;
|
||||
{
|
||||
auto first = CachedPipeline(context, State{}, layout(0), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, nullptr, &hash);
|
||||
Require(hash != 0, "a stored pipeline reported no store hash");
|
||||
std::uint64_t secondHash = 0;
|
||||
auto second = CachedPipeline(context, State{}, layout(1), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, nullptr, &secondHash);
|
||||
Require(secondHash != 0 && secondHash != hash, "two configurations reported the same store hash");
|
||||
touched = first;
|
||||
other = second;
|
||||
const auto hits = PipelineStoreTotals().hits;
|
||||
Require(TouchCachedPipeline(context, hash, touched) == first && PipelineStoreTotals().hits == hits + 1, "touching a stored pipeline did not return it as a hit");
|
||||
Require(TouchCachedPipeline(context, hash, other) == nullptr, "touching a hash with another pipeline returned the stored one");
|
||||
Require(TouchCachedPipeline(context, hash, std::weak_ptr<Pipeline>{}) == nullptr, "touching a hash without a pipeline returned the stored one");
|
||||
}
|
||||
for (std::uint32_t configuration = 2; configuration < PipelineStoreTotals().bound; ++configuration) static_cast<void>(CachedPipeline(context, State{}, layout(configuration), resources, shaders));
|
||||
Require(TouchCachedPipeline(context, hash, touched) != nullptr, "the first pipeline left a store that was not yet full");
|
||||
bool created = false;
|
||||
static_cast<void>(CachedPipeline(context, State{}, layout(PipelineStoreTotals().bound), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created));
|
||||
Require(created, "the configuration that fills the store was already stored");
|
||||
static_cast<void>(CachedPipeline(context, State{}, layout(0), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created));
|
||||
Require(!created, "a touched pipeline was evicted as the least recently used one");
|
||||
static_cast<void>(CachedPipeline(context, State{}, layout(1), resources, shaders, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, &created));
|
||||
Require(created, "the least recently used pipeline stayed while a touched one was older");
|
||||
ClearCachedPipelines(context.device);
|
||||
Require(TouchCachedPipeline(context, hash, touched) == nullptr, "touching a pipeline of a cleared store returned it");
|
||||
Require(mock.live == 0, "pipeline store touch tests leaked Vulkan objects");
|
||||
}
|
||||
|
||||
// A journal longer than the store's starting bound: 1032 recorded pipelines replayed into an empty
|
||||
// store all stay resident (the replay grows the bound instead of dropping what does not fit).
|
||||
void pipelineStoreReplayGrowthTests() {
|
||||
@@ -12388,6 +12518,11 @@ int main(int argc, char** argv) {
|
||||
std::cout << "pipeline store bound follows the evicted pipelines that come back\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-store-touch") {
|
||||
pipelineStoreTouchTests();
|
||||
std::cout << "a touched pipeline is returned as a hit and stays recently used\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";
|
||||
@@ -12502,7 +12637,7 @@ int main(int argc, char** argv) {
|
||||
#else
|
||||
setenv("APS5_ADDRESS_COPY_MAX_MIB", "1", 1);
|
||||
#endif
|
||||
{
|
||||
for (const int frames : {0, 1, 2}) {
|
||||
mock = MockVulkan{};
|
||||
auto context = mockContext();
|
||||
context.bufferDeviceAddress = true;
|
||||
@@ -12517,9 +12652,9 @@ int main(int argc, char** argv) {
|
||||
},
|
||||
{},
|
||||
{}
|
||||
}, std::string_view(argv[1]) == "--address-copy-cap-remembered");
|
||||
}, std::string_view(argv[1]) == "--address-copy-cap-remembered", frames);
|
||||
Require(mock.live == 0, "the address copy cap test leaked Vulkan objects");
|
||||
}
|
||||
Require(mock.live == 0, "the address copy cap test leaked Vulkan objects");
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--address-space-teardown") {
|
||||
@@ -12799,6 +12934,7 @@ int main(int argc, char** argv) {
|
||||
Require(mock.live == 0, "BDA resources leaked Vulkan objects");
|
||||
RunGuestAllocationTests();
|
||||
RunUnmappedGapTests();
|
||||
RunVanishedPageReadTests();
|
||||
RunColorTargetLayoutTests();
|
||||
RunLiveStackAccessTests();
|
||||
RunTextureFormatTests();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <array>
|
||||
#include <span>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -328,3 +329,34 @@ void RunUnmappedGapTests() {
|
||||
GuestArena::GuestArenaRelease_nid_postfix(spare, page);
|
||||
#endif
|
||||
}
|
||||
|
||||
void RunVanishedPageReadTests() {
|
||||
#ifdef _WIN32
|
||||
namespace GuestMemory = AgcDriver::GuestMemory;
|
||||
Require(GuestArena::GuestArenaAvailable_nid_postfix(), "released-page read fixture needs the guest arena");
|
||||
constexpr std::size_t page = 4096;
|
||||
constexpr std::uint32_t ReadWriteProtection = 0x04;
|
||||
auto* block = static_cast<std::byte*>(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page));
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(block);
|
||||
GuestArena::GuestArenaCommit_nid_postfix(block, page, ReadWriteProtection, page);
|
||||
GuestArena::GuestArenaCommit_nid_postfix(block + page, page, ReadWriteProtection, page);
|
||||
std::memset(block, 0x5a, 2 * page);
|
||||
Require(GuestMemory::Accessible(block, 2 * page), "committed arena test pages are not readable");
|
||||
GuestArena::GuestArenaReset_nid_postfix(block + page, page);
|
||||
std::array<std::byte, 2 * page> read{};
|
||||
read.fill(std::byte{0xff});
|
||||
GuestMemory::ReadCommitted(address, read);
|
||||
const auto filled = [](std::span<const std::byte> bytes, std::byte value) { return std::all_of(bytes.begin(), bytes.end(), [&](std::byte byte) { return byte == value; }); };
|
||||
Require(filled(std::span(read).first(page), std::byte{0x5a}) && filled(std::span(read).subspan(page), std::byte{0}), "a committed read of a page released after it was checked did not read it as uncommitted");
|
||||
std::array<std::byte, 2 * page> expected{};
|
||||
std::fill(expected.begin(), expected.begin() + page, std::byte{0x5a});
|
||||
std::fill(expected.begin() + page, expected.end(), std::byte{0x77});
|
||||
Require(GuestMemory::EqualsCommitted(address, expected), "a committed compare of a page released after it was checked did not skip it");
|
||||
expected[page - 1] = std::byte{0};
|
||||
Require(!GuestMemory::EqualsCommitted(address, expected), "a committed compare missed a difference before a page released after it was checked");
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(address, 2 * page);
|
||||
GuestArena::GuestArenaReset_nid_postfix(block, page);
|
||||
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(address, 2 * page);
|
||||
GuestArena::GuestArenaRelease_nid_postfix(block, 2 * page);
|
||||
#endif
|
||||
}
|
||||
@@ -17,6 +17,8 @@ static inline bool GuestMemoryLifetimeHasHostImports(VkDevice device) {
|
||||
}
|
||||
#ifdef _WIN32
|
||||
constexpr const char* libraryName = "vulkan-1.dll";
|
||||
#elif defined(__APPLE__)
|
||||
constexpr const char* libraryName = "libvulkan.1.dylib";
|
||||
#else
|
||||
constexpr const char* libraryName = "libvulkan.so.1";
|
||||
#endif
|
||||
|
||||
@@ -371,9 +371,14 @@ void RunGuestTextureResourceTests() {
|
||||
result = DecodeTextureResource(pack(cubeExtraSlice));
|
||||
Require(result.baseArray == 0 && result.depthOrLastArray == 6, "a cube descriptor with a slice past its last whole cube lost a slice");
|
||||
Require(DescribeSurface(result).imageLayers == 7, "a cube allocation with a slice past its last whole cube omitted it");
|
||||
Fields cubePartial = cube;
|
||||
cubePartial.depth = 4;
|
||||
rejectFields(cubePartial, "fewer than 6 array slices");
|
||||
for (const auto [first, last] : {std::pair{0u, 4u}, std::pair{0u, 1u}, std::pair{2u, 5u}, std::pair{6u, 10u}}) {
|
||||
auto shortCube = cube;
|
||||
shortCube.baseArray = first;
|
||||
shortCube.depth = last;
|
||||
result = DecodeTextureResource(pack(shortCube));
|
||||
Require(result.baseArray == first && result.depthOrLastArray == first + 5u, "a cube descriptor with fewer than six slices did not expose six faces from its base slice");
|
||||
Require(DescribeSurface(result).imageLayers == first + 6u, "a cube allocation with fewer than six slices omitted faces");
|
||||
}
|
||||
|
||||
Fields multisampled = base;
|
||||
multisampled.typeRaw = 14;
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp"
|
||||
#include "AllocationFailure.hpp"
|
||||
#include "ShaderCacheDirectory.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <random>
|
||||
#include <string_view>
|
||||
|
||||
using namespace AgcDriver::Graphics;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool value, const char* reason) {
|
||||
if (value) return;
|
||||
std::fprintf(stderr, "FAIL: %s\n", reason);
|
||||
std::fflush(stderr);
|
||||
std::_Exit(1);
|
||||
}
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable changed;
|
||||
std::map<VkPipelineCache, std::vector<std::byte>> caches;
|
||||
std::uintptr_t next = 1;
|
||||
std::atomic<bool> armed{false};
|
||||
std::atomic<bool> failStartup{false};
|
||||
std::atomic<unsigned> queries{0};
|
||||
std::atomic<unsigned> faults{0};
|
||||
std::size_t initialBytes = 64;
|
||||
|
||||
std::vector<std::byte> Payload() {
|
||||
std::vector<std::byte> bytes(initialBytes);
|
||||
const std::array<std::uint32_t, 4> header{32, VK_PIPELINE_CACHE_HEADER_VERSION_ONE, 13, 42};
|
||||
std::memcpy(bytes.data(), header.data(), sizeof(header));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL Create(VkDevice, const VkPipelineCacheCreateInfo* info, const VkAllocationCallbacks*, VkPipelineCache* output) {
|
||||
std::lock_guard lock(mutex);
|
||||
*output = reinterpret_cast<VkPipelineCache>(next++);
|
||||
if (info->initialDataSize != 0) {
|
||||
const auto* bytes = static_cast<const std::byte*>(info->pInitialData);
|
||||
caches[*output] = std::vector<std::byte>(bytes, bytes + info->initialDataSize);
|
||||
} else caches[*output] = Payload();
|
||||
if (failStartup.exchange(false)) {
|
||||
++faults;
|
||||
AgcDriver::Tests::SetAllocationFailureCountdown(0);
|
||||
}
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL Destroy(VkDevice, VkPipelineCache cache, const VkAllocationCallbacks*) {
|
||||
std::lock_guard lock(mutex);
|
||||
Check(caches.erase(cache) == 1, "destroyed an unknown cache");
|
||||
changed.notify_all();
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL Merge(VkDevice, VkPipelineCache destination, std::uint32_t count, const VkPipelineCache* sources) {
|
||||
std::lock_guard lock(mutex);
|
||||
Check(count == 1, "expected one merge source");
|
||||
caches.at(destination) = caches.at(sources[0]);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL Data(VkDevice, VkPipelineCache cache, std::size_t* size, void* output) {
|
||||
std::lock_guard lock(mutex);
|
||||
const auto& bytes = caches.at(cache);
|
||||
if (output != nullptr) {
|
||||
Check(*size >= bytes.size(), "cache snapshot buffer is too small");
|
||||
std::memcpy(output, bytes.data(), bytes.size());
|
||||
}
|
||||
*size = bytes.size();
|
||||
if (output == nullptr && armed && ++queries == 2) {
|
||||
armed = false;
|
||||
++faults;
|
||||
changed.notify_all();
|
||||
AgcDriver::Tests::SetAllocationFailureCountdown(0);
|
||||
}
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL Resolve(VkDevice, const char* name) {
|
||||
if (std::strcmp(name, "vkCreatePipelineCache") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Create);
|
||||
if (std::strcmp(name, "vkDestroyPipelineCache") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Destroy);
|
||||
if (std::strcmp(name, "vkGetPipelineCacheData") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Data);
|
||||
if (std::strcmp(name, "vkMergePipelineCaches") == 0) return reinterpret_cast<PFN_vkVoidFunction>(Merge);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<char> Read(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
Check(static_cast<bool>(file), "saved cache file is missing");
|
||||
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
void Grow(VkPipelineCache cache, std::size_t bytes = 128) {
|
||||
std::lock_guard lock(mutex);
|
||||
caches.at(cache).resize(bytes, std::byte{9});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Check(argc == 2, "expected startup, now, busy, destructor, worker or read-retry");
|
||||
const std::string_view mode(argv[1]);
|
||||
Check(mode == "startup" || mode == "now" || mode == "busy" || mode == "destructor" || mode == "worker" || mode == "read-retry", "unknown mode");
|
||||
#ifndef _WIN32
|
||||
if (mode == "read-retry") return 77;
|
||||
#endif
|
||||
const auto root = std::filesystem::temp_directory_path() / ("aps5-cache-allocation-" + std::to_string(std::random_device{}()));
|
||||
Check(std::filesystem::create_directory(root), "test cache directory already exists");
|
||||
struct Cleanup {
|
||||
std::filesystem::path path;
|
||||
~Cleanup() { std::error_code error; std::filesystem::remove_all(path, error); }
|
||||
} cleanup{root};
|
||||
#ifdef _WIN32
|
||||
_putenv_s("ANYPS5_NO_SHADER_CACHE", "0");
|
||||
_wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", root.wstring().c_str());
|
||||
#else
|
||||
setenv("ANYPS5_NO_SHADER_CACHE", "0", 1);
|
||||
setenv("ANYPS5_SHADER_CACHE_DIR", root.string().c_str(), 1);
|
||||
#endif
|
||||
Context context{};
|
||||
context.device = reinterpret_cast<VkDevice>(std::uintptr_t{1});
|
||||
context.deviceProc = Resolve;
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
properties.vendorID = 13;
|
||||
properties.deviceID = 42;
|
||||
if (mode == "startup") {
|
||||
failStartup = true;
|
||||
bool failed = false;
|
||||
try {
|
||||
PipelineCache attempt(context, properties);
|
||||
} catch (const std::bad_alloc&) {
|
||||
failed = true;
|
||||
}
|
||||
Check(failed && faults == 1, "saver startup allocation failure was not propagated");
|
||||
Check(caches.empty(), "failed saver startup leaked the Vulkan pipeline cache");
|
||||
{
|
||||
PipelineCache retry(context, properties);
|
||||
Check(retry.Handle() != VK_NULL_HANDLE, "cache creation could not retry after failed saver startup");
|
||||
}
|
||||
Check(caches.empty(), "successful retry leaked a Vulkan pipeline cache");
|
||||
std::puts("startup: propagated the allocation failure, released the cache and allowed recreation");
|
||||
return 0;
|
||||
}
|
||||
if (mode == "read-retry") initialBytes = 32768;
|
||||
auto cache = std::make_unique<PipelineCache>(context, properties);
|
||||
cache->SaveNow();
|
||||
std::filesystem::path file;
|
||||
for (const auto& entry : std::filesystem::recursive_directory_iterator(root)) if (entry.path().extension() == ".bin") file = entry.path();
|
||||
Check(!file.empty(), "initial save created no cache file");
|
||||
#ifdef _WIN32
|
||||
if (mode == "read-retry") {
|
||||
{
|
||||
PipelineCache writer(context, properties);
|
||||
Grow(writer.Handle(), 40960);
|
||||
writer.SaveNow();
|
||||
}
|
||||
const auto before = Read(file);
|
||||
Check(before.size() == 24 + 40960, "the second writer did not update the cache file");
|
||||
Grow(cache->Handle(), 65536);
|
||||
std::error_code error;
|
||||
const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(file, error);
|
||||
Check(!error, "could not resolve the test cache path");
|
||||
struct TailLock {
|
||||
HANDLE handle = INVALID_HANDLE_VALUE;
|
||||
OVERLAPPED region{};
|
||||
void Release() {
|
||||
if (handle == INVALID_HANDLE_VALUE) return;
|
||||
Check(UnlockFileEx(handle, 0, 1, 0, ®ion) != 0, "could not unlock the cache body");
|
||||
CloseHandle(handle);
|
||||
handle = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
~TailLock() { Release(); }
|
||||
} locked;
|
||||
locked.handle = CreateFileW(native.c_str(), GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
Check(locked.handle != INVALID_HANDLE_VALUE, "could not open the cache body lock handle");
|
||||
locked.region.Offset = 16384;
|
||||
Check(LockFileEx(locked.handle, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &locked.region) != 0,
|
||||
"could not lock the cache body");
|
||||
{
|
||||
std::ifstream header(file, std::ios::binary);
|
||||
std::array<char, 24> bytes{};
|
||||
Check(static_cast<bool>(header.read(bytes.data(), bytes.size())), "the body lock unexpectedly blocked the header");
|
||||
}
|
||||
std::vector<std::byte> unreadable;
|
||||
Check(!ShaderRecompiler::ReadWholeFile(file, unreadable), "the body lock did not block a complete cache read");
|
||||
cache->SaveNow();
|
||||
locked.Release();
|
||||
Check(Read(file) == before, "a failed cache read overwrote the other writer's file");
|
||||
cache->SaveNow();
|
||||
Check(Read(file).size() == 24 + 65536, "a transient cache read failure prevented the later save");
|
||||
cache.reset();
|
||||
Check(caches.empty(), "cache read retry leaked Vulkan objects");
|
||||
std::puts("read-retry: kept the unreadable cache file and saved new pipelines after it became readable");
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
const auto original = Read(file);
|
||||
Grow(cache->Handle());
|
||||
queries = 0;
|
||||
armed = true;
|
||||
if (mode == "destructor") cache.reset();
|
||||
else if (mode == "worker") {
|
||||
std::unique_lock lock(mutex);
|
||||
Check(changed.wait_for(lock, 14s, [] { return faults == 1; }), "background save did not reach the allocation failure");
|
||||
} else if (mode == "busy") cache->SaveUnlessBusy();
|
||||
else cache->SaveNow();
|
||||
Check(faults == 1 && !armed, "the snapshot allocation was not exercised");
|
||||
Check(Read(file) == original, "a failed snapshot replaced the previous cache file");
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
if (mode == "worker") Check(changed.wait_for(lock, 2s, [] { return caches.size() == 1; }), "background snapshot did not release its temporary cache");
|
||||
Check(caches.size() == (cache ? 1u : 0u), "failed snapshot leaked its temporary merged cache");
|
||||
}
|
||||
if (!cache) {
|
||||
cache = std::make_unique<PipelineCache>(context, properties);
|
||||
Grow(cache->Handle());
|
||||
}
|
||||
cache->SaveNow();
|
||||
Check(Read(file).size() == 24 + 128 && Read(file) != original, "a failed snapshot prevented a later successful save");
|
||||
cache.reset();
|
||||
Check(caches.empty(), "pipeline cache allocation test leaked Vulkan objects");
|
||||
std::printf("%.*s: allocation failure kept the old file, released temporary caches and allowed a retry\n", static_cast<int>(mode.size()), mode.data());
|
||||
}
|
||||
@@ -109,6 +109,19 @@ void testNopPad() {
|
||||
AgcDriver::Pm4::Validate(largest, 0);
|
||||
}
|
||||
|
||||
void testWorkloadNopPayloadsIgnored() {
|
||||
AgcDriver::QueueState state;
|
||||
for (const auto& packet : {
|
||||
makePacket(0x10, {1, 0x18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
|
||||
makePacket(0x10, {1, 33, 0xffffffffu, 0xfffffffdu, 0, 0, 0, 0, 0, 0, 0}),
|
||||
makePacket(0x10, {1, 0, 0, 0, 0, 0, 0, 0}),
|
||||
}) execute(state, packet);
|
||||
check(state.markers.empty() && state.indexBase == 0 && !state.indexBufferSize && state.indexType == 0 &&
|
||||
state.instanceCount == 1 && state.predication.operation == 0 && state.predication.address == 0 &&
|
||||
state.shader.begin() == state.shader.end() && !state.savedContext && state.constantRam.empty(),
|
||||
"a workload NOP payload reached queue state");
|
||||
}
|
||||
|
||||
void testRegisters() {
|
||||
AgcDriver::QueueState state;
|
||||
execute(state, makePacket(0x79, {0x242, 4}));
|
||||
@@ -2095,6 +2108,7 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
testCatalog();
|
||||
testNopPad();
|
||||
testWorkloadNopPayloadsIgnored();
|
||||
testRewindPayloadBits();
|
||||
testWriteChangedKeepsUntouchedBytes();
|
||||
testWriteChangedStoresExactlyTheChangedRuns();
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -9326,6 +9327,109 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) {
|
||||
std::cout << "Multisampled target guest access tests passed\n";
|
||||
}
|
||||
|
||||
void repeatStorageGuardTests(const Device& device, Recorder& recorder) {
|
||||
const auto& base = device.GetContext();
|
||||
if ((base.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0 || !device.NullDescriptors()) {
|
||||
std::cout << "4x color targets or nullDescriptor unavailable: repeated storage surface guard not tested\n";
|
||||
return;
|
||||
}
|
||||
constexpr std::uint32_t side = 256;
|
||||
constexpr std::uint32_t targetSide = 64;
|
||||
constexpr std::size_t surfaceBytes = side * side * 4;
|
||||
constexpr std::size_t targetBytes = 65536u;
|
||||
constexpr std::size_t bytes = surfaceBytes + 2u * targetBytes;
|
||||
#ifdef _WIN32
|
||||
void* block = VirtualAlloc(nullptr, bytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
|
||||
#else
|
||||
void* block = std::aligned_alloc(65536, bytes);
|
||||
#endif
|
||||
Require(block != nullptr, "cannot allocate the repeated storage surface block");
|
||||
std::memset(block, 0, bytes);
|
||||
const auto address = reinterpret_cast<std::uint64_t>(block);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Add(block, bytes, true, true, true);
|
||||
}
|
||||
struct Unregister {
|
||||
const Context& context;
|
||||
void* block;
|
||||
~Unregister() {
|
||||
ClearMultisampleTargets(context.device);
|
||||
ClearCachedTextures(context.device);
|
||||
{
|
||||
GuestAllocations::Mutation mutation;
|
||||
mutation.Remove(block);
|
||||
}
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
} unregister{base, block};
|
||||
TextureDetiler detiler(base);
|
||||
auto context = base;
|
||||
context.detiler = &detiler;
|
||||
context.nullDescriptors = true;
|
||||
TextureCache cache(context);
|
||||
context.textureCache = &cache;
|
||||
GuestTextureResource resource{};
|
||||
resource.baseAddress = address;
|
||||
resource.width = side;
|
||||
resource.height = side;
|
||||
resource.mipCount = 1;
|
||||
resource.tileMode = TextureTileMode::kR64KBX;
|
||||
resource.dimension = TextureDimension::k2D;
|
||||
resource.format = 56;
|
||||
resource.dstSelX = 4;
|
||||
resource.dstSelY = 5;
|
||||
resource.dstSelZ = 6;
|
||||
resource.dstSelW = 7;
|
||||
Require(DescribeSurface(resource).guestBytes == surfaceBytes, "the repeated storage surface has an unexpected size");
|
||||
const auto guard = [&](std::uint64_t at) {
|
||||
ColorTarget target{};
|
||||
target.address = at;
|
||||
target.surfaceAddress = at;
|
||||
target.extent = {targetSide, targetSide};
|
||||
target.surfaceExtent = target.extent;
|
||||
target.format = VK_FORMAT_R8G8B8A8_UNORM;
|
||||
target.bytes = targetBytes;
|
||||
target.samples = 4;
|
||||
std::lock_guard gpu(GpuMutex());
|
||||
Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the guarding multisampled target has no view");
|
||||
recorder.Submit();
|
||||
device.WaitQueue();
|
||||
recorder.Sync();
|
||||
};
|
||||
const auto clearTargets = [&] {
|
||||
recorder.Submit();
|
||||
device.WaitQueue();
|
||||
recorder.Sync();
|
||||
ClearMultisampleTargets(context.device);
|
||||
};
|
||||
const auto refusal = [](const std::function<void()>& access) {
|
||||
try {
|
||||
access();
|
||||
} catch (const std::exception& error) {
|
||||
return std::string(error.what());
|
||||
}
|
||||
return std::string();
|
||||
};
|
||||
const auto stored = CachedStorageSurface(context, resource);
|
||||
Require(stored != nullptr, "the storage surface has no image");
|
||||
Require(RepeatStorageSurface(context, resource, stored) == stored && CachedStorageSurface(context, resource) == stored, "without a guarded target the repeat path and the lookup returned different images");
|
||||
guard(address + surfaceBytes);
|
||||
Require(RepeatStorageSurface(context, resource, stored) == stored && CachedStorageSurface(context, resource) == stored, "a guarded target that starts where the surface ends changed the repeat path's or the lookup's image");
|
||||
clearTargets();
|
||||
guard(address + targetBytes);
|
||||
const auto repeated = refusal([&] { static_cast<void>(RepeatStorageSurface(context, resource, stored)); });
|
||||
const auto looked = refusal([&] { static_cast<void>(CachedStorageSurface(context, resource)); });
|
||||
Require(!looked.empty() && looked.find("a storage image access") != std::string::npos && looked.find("multisampled color target") != std::string::npos, "the lookup did not refuse a surface a guarded target overlaps after its first byte: " + looked);
|
||||
Require(repeated == looked, "the repeat path decided differently from the lookup for a guarded target after the surface's first byte: " + repeated);
|
||||
clearTargets();
|
||||
std::cout << "Repeated storage surface guard tests passed\n";
|
||||
}
|
||||
|
||||
void oneFragmentFmaskGuardTests(const Device& device) {
|
||||
const auto& base = device.GetContext();
|
||||
if (!device.NullDescriptors()) {
|
||||
@@ -9906,8 +10010,8 @@ void singleCubeTests(const Device& device, Recorder& recorder) {
|
||||
context.detiler = &detiler;
|
||||
SampleProgram program(context, recorder, SAMPLE_ARRAY_SPV);
|
||||
const VkComponentMapping identity{};
|
||||
for (const auto first : {0u, 5u, 6u}) {
|
||||
std::array<std::uint32_t, 8> words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, first | (first << 16u), 0, 0, 0};
|
||||
for (const auto [first, last] : {std::pair{0u, 0u}, std::pair{5u, 5u}, std::pair{6u, 6u}, std::pair{0u, 4u}, std::pair{2u, 5u}}) {
|
||||
std::array<std::uint32_t, 8> words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, last | (first << 16u), 0, 0, 0};
|
||||
auto resource = DecodeTextureResource(words);
|
||||
const auto geometry = DescribeSurface(resource);
|
||||
std::vector<std::byte> memory(static_cast<std::size_t>(geometry.guestBytes) + 256);
|
||||
@@ -12127,6 +12231,10 @@ int main(int argc, char** argv) {
|
||||
std::cout << "Copied draw input tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--repeat-storage-guard-only") {
|
||||
repeatStorageGuardTests(device, recorder);
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--multisample-only") {
|
||||
multisampleGuestAccessTests(device, recorder);
|
||||
oneFragmentFmaskGuardTests(device);
|
||||
@@ -12191,6 +12299,7 @@ int main(int argc, char** argv) {
|
||||
bdaStagingTests(device, recorder);
|
||||
depthSurfaceProofTests(device, recorder);
|
||||
multisampleGuestAccessTests(device, recorder);
|
||||
repeatStorageGuardTests(device, recorder);
|
||||
oneFragmentFmaskGuardTests(device);
|
||||
tiledResolveTests(device, recorder);
|
||||
fmaskDecompressMsaaTests(device, recorder);
|
||||
|
||||
@@ -351,6 +351,7 @@ CompiledVariant sampleVariant() {
|
||||
image.flatLineCompatible = false;
|
||||
image.flatVolumeLineCompatible = false;
|
||||
image.dimensionQueryOnly = false;
|
||||
image.sampledOnly = false;
|
||||
image.foldTexelOffsetsOnly = true;
|
||||
image.remappedDepth1D = true;
|
||||
image.optionalDescriptor = true;
|
||||
@@ -526,6 +527,62 @@ std::vector<std::byte> packBytes(const std::vector<std::pair<std::vector<std::by
|
||||
return bytes;
|
||||
}
|
||||
|
||||
void verifyPrebuiltPackOwnership(const std::filesystem::path& directory) {
|
||||
using namespace ShaderRecompiler::ShaderDiskCache;
|
||||
SampleRequest sample;
|
||||
const auto key = sample.Key();
|
||||
const auto variant = sampleVariant();
|
||||
const auto path = directory / "ownership.pack";
|
||||
require(WriteFileAtomically(path, packBytes({{key, EncodeEntry(key, variant)}})), "cannot write the ownership test pack");
|
||||
CompiledVariant first, second;
|
||||
PackLoadTimings timings;
|
||||
const std::uint32_t* words = nullptr;
|
||||
{
|
||||
CachePack pack(path);
|
||||
require(pack.Valid(), "the ownership test pack is invalid");
|
||||
require(pack.Load(key, first, timings) == LoadStatus::Loaded, "cannot load the first packed variant");
|
||||
require(pack.Load(key, second, timings) == LoadStatus::Loaded, "cannot load the second packed variant");
|
||||
words = first.artifact.spirv.Words().data();
|
||||
require(!first.artifact.spirv.empty() && words == second.artifact.spirv.Words().data(), "packed SPIR-V was copied instead of sharing its mapping");
|
||||
}
|
||||
requireSameVariant(variant, first, "packed variant outlives its pack");
|
||||
#ifdef _WIN32
|
||||
MEMORY_BASIC_INFORMATION region{};
|
||||
require(VirtualQuery(words, ®ion, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED, "packed SPIR-V lost its mapping after pack destruction");
|
||||
#endif
|
||||
auto edited = first.artifact.spirv;
|
||||
edited[0] ^= 1u;
|
||||
require(edited.Words().data() != words && edited.Words()[0] != first.artifact.spirv.Words()[0], "editing packed SPIR-V modified the shared mapping");
|
||||
first = {};
|
||||
requireSameVariant(variant, second, "last packed variant retains its mapping");
|
||||
std::vector<std::byte> published;
|
||||
require(ReadWholeFile(path, published) && published == packBytes({{key, EncodeEntry(key, variant)}}), "editing packed SPIR-V modified the cache file");
|
||||
second = {};
|
||||
#ifdef _WIN32
|
||||
require(VirtualQuery(words, ®ion, sizeof(region)) == sizeof(region) && region.State == MEM_FREE, "the final packed variant did not release its mapping");
|
||||
#endif
|
||||
const auto offset = sizeof(PackHeader) + sizeof(PackRecord);
|
||||
published.insert(published.begin() + offset, std::byte{});
|
||||
PackHeader header;
|
||||
PackRecord record;
|
||||
std::memcpy(&header, published.data(), sizeof(header));
|
||||
std::memcpy(&record, published.data() + sizeof(header), sizeof(record));
|
||||
++header.fileBytes;
|
||||
++record.offset;
|
||||
std::memcpy(published.data() + sizeof(header), &record, sizeof(record));
|
||||
header.indexHash = ShaderRecompiler::HashBytes(std::span<const std::byte>(published).subspan(sizeof(header), sizeof(record)));
|
||||
std::memcpy(published.data(), &header, sizeof(header));
|
||||
require(WriteFileAtomically(path, published), "cannot publish an unaligned packed entry");
|
||||
{
|
||||
CachePack pack(path);
|
||||
require(pack.Valid(), "an unaligned packed entry was rejected");
|
||||
require(pack.Load(key, first, timings) == LoadStatus::Loaded && pack.Load(key, second, timings) == LoadStatus::Loaded, "an unaligned packed entry did not load");
|
||||
require(first.artifact.spirv.Words().data() != second.artifact.spirv.Words().data(), "an unaligned packed module did not use aligned owned storage");
|
||||
}
|
||||
requireSameVariant(variant, first, "unaligned packed variant outlives its pack");
|
||||
requireSameVariant(variant, second, "second unaligned packed variant outlives its pack");
|
||||
}
|
||||
|
||||
void refreshIndexHash(std::vector<std::byte>& pack, std::size_t entries) {
|
||||
using namespace ShaderRecompiler::ShaderDiskCache;
|
||||
const auto index = std::span(pack).subspan(sizeof(PackHeader), entries * sizeof(PackRecord));
|
||||
@@ -780,6 +837,7 @@ void verifyKeySensitivity() {
|
||||
changes("exact robust loads", [](SampleRequest& sample) { sample.request.target.exactRobustLoads = true; });
|
||||
changes("wave LDS memory barriers", [](SampleRequest& sample) { sample.request.target.waveLdsMemoryBarriers = true; });
|
||||
changes("device-scope LDS memory barriers", [](SampleRequest& sample) { sample.request.target.deviceScopeLdsMemoryBarriers = true; });
|
||||
changes("LDS lock fences", [](SampleRequest& sample) { sample.request.target.ldsLockFences = 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; });
|
||||
@@ -2541,6 +2599,13 @@ int main(int argc, char** argv) {
|
||||
std::cout << "housekeeping version " << directory.filename().string() << '\n';
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--prebuilt-pack-ownership") {
|
||||
const auto directory = std::filesystem::temp_directory_path() / ("aps5-pack-ownership-" + std::to_string(std::random_device{}()));
|
||||
std::filesystem::create_directories(directory);
|
||||
verifyPrebuiltPackOwnership(directory);
|
||||
std::filesystem::remove_all(directory);
|
||||
return 0;
|
||||
}
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--resource-plan-codec") {
|
||||
verifyResultRoundTrip();
|
||||
verifyEntryRoundTrip();
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "BdaAbi.hpp"
|
||||
#include "ImageTableAbi.hpp"
|
||||
#include "ShaderDiskCache.hpp"
|
||||
#include <limits>
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <algorithm>
|
||||
#include <set>
|
||||
@@ -1234,6 +1235,475 @@ void verifyTypeZeroImageMaterialization() {
|
||||
atomic ? "TYPE=0 or 1 atomic image descriptor was accepted without hardware evidence" : "TYPE=0 or 1 writable image descriptor was accepted without hardware evidence");
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> code{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xf4000400u, 0xfa000050u, 0xf4080a00u, 0xfa000080u, 0xbf8cc07fu, 0xf42c0814u, 0xfa000000u, 0xbf8cc07fu, 0xbf068010u, 0xbf850008u};
|
||||
for (std::uint32_t dword = 0; dword < 8u; dword++) {
|
||||
code.push_back(0xbe800300u | ((8u + dword) << 16u) | (32u + dword));
|
||||
}
|
||||
code.insert(code.end(), {0xf09c0f08u, 0x00a20000u, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xbf810000u});
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
struct alignas(256) Texture { std::array<std::uint8_t, 256> bytes{}; };
|
||||
static Texture textures[2];
|
||||
const auto makeImage = [&](const Texture& texture, std::uint32_t extent) {
|
||||
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texture.bytes.data()));
|
||||
return std::array<std::uint32_t, 8>{static_cast<std::uint32_t>(base >> 8u), static_cast<std::uint32_t>((base >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (extent << 30u), extent << 14u, 0xfacu | (Type2D << 28u), 0u, 0u, 0u, 0u};
|
||||
};
|
||||
const auto oldT = makeImage(textures[0], 3u);
|
||||
const auto newT = makeImage(textures[1], 7u);
|
||||
std::array<std::uint32_t, 4> output{};
|
||||
const auto outputAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output.data()));
|
||||
alignas(4096) static std::array<std::uint32_t, 1024> srt{};
|
||||
static const auto srtAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(srt.data()));
|
||||
const std::array<std::uint32_t, 4> outputV{static_cast<std::uint32_t>(outputAddress), static_cast<std::uint32_t>((outputAddress >> 32u) & 0xffffu), 16u, 0xfacu};
|
||||
const std::array<std::uint32_t, 2> userData{static_cast<std::uint32_t>(srtAddress), static_cast<std::uint32_t>(srtAddress >> 32u)};
|
||||
const std::array<std::uint32_t, 4> capabilities{29u, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x23001u, code, 0, {}};
|
||||
request.context.waveSize = 32u;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.bdaAbiVersion = BdaAbi::Version;
|
||||
request.target.fragmentShaderBarycentricEnabled = false;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
|
||||
static std::uint64_t syncBegin = 0;
|
||||
static std::uint64_t syncEnd = 0;
|
||||
static std::uint64_t scalarSyncWord = 0;
|
||||
static std::uint64_t waits = 0;
|
||||
static std::uint64_t rawFirstWords = 0;
|
||||
static std::uint64_t scalarSyncHits = 0;
|
||||
static bool spanAwareSync = true;
|
||||
static bool flushed = false;
|
||||
static std::array<std::uint32_t, 8> replacementT{};
|
||||
|
||||
struct FlushReset {
|
||||
~FlushReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); }
|
||||
} flushReset;
|
||||
|
||||
AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) {
|
||||
if (!flushed && address < syncEnd && syncBegin < address + bytes) {
|
||||
flushed = true;
|
||||
++waits;
|
||||
std::memcpy(srt.data(), replacementT.data(), sizeof(replacementT));
|
||||
}
|
||||
});
|
||||
|
||||
const auto pending = +[](std::uint64_t address, std::size_t bytes, std::span<std::byte>) -> AgcDriver::ShaderMemory::PendingWrite {
|
||||
using Pending = AgcDriver::ShaderMemory::PendingWrite;
|
||||
if (bytes >= 4096u) {
|
||||
return address <= srtAddress && srtAddress < address + bytes ? Pending::Sync : Pending::None;
|
||||
}
|
||||
if (scalarSyncWord != 0u && address < scalarSyncWord + sizeof(std::uint32_t) && scalarSyncWord < address + bytes) {
|
||||
++scalarSyncHits;
|
||||
return Pending::Sync;
|
||||
}
|
||||
if (flushed) return Pending::None;
|
||||
if (spanAwareSync) {
|
||||
if (address < syncEnd && syncBegin < address + bytes) return Pending::Sync;
|
||||
} else {
|
||||
if (address >= syncBegin && address < syncEnd) return Pending::Sync;
|
||||
}
|
||||
if (address >= srtAddress && address + bytes <= srtAddress + 32u) {
|
||||
if (bytes == sizeof(std::uint32_t) && address < syncBegin) ++rawFirstWords;
|
||||
return Pending::Raw;
|
||||
}
|
||||
return Pending::None;
|
||||
};
|
||||
const auto hookWaits = +[]() -> std::uint64_t { return waits; };
|
||||
|
||||
for (const int mode : {0, 1, 2}) {
|
||||
const bool allRaw = mode == 0;
|
||||
const bool primaryBarrier = mode == 1;
|
||||
srt.fill(0u);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin());
|
||||
std::copy(outputV.begin(), outputV.end(), srt.begin() + 16);
|
||||
srt[20] = 1u;
|
||||
syncBegin = allRaw ? srtAddress + 32u : srtAddress + 12u;
|
||||
syncEnd = srtAddress + 32u;
|
||||
scalarSyncWord = srtAddress + 84u;
|
||||
waits = 0u;
|
||||
rawFirstWords = 0u;
|
||||
scalarSyncHits = 0u;
|
||||
spanAwareSync = primaryBarrier;
|
||||
flushed = false;
|
||||
replacementT = newT;
|
||||
|
||||
AgcDriver::ShaderMemory memory({}, pending, nullptr, hookWaits);
|
||||
const auto capture = memory.Capture(request);
|
||||
require(scalarSyncHits == 0u, "descriptor pendingWrite barrier: a 4-byte scalar read widened its pendingWrite query into a neighbouring dword");
|
||||
const bool holdsNew = std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) {
|
||||
return std::equal(newT.begin(), newT.end(), value.dwords.begin());
|
||||
});
|
||||
const bool holdsOld = std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) {
|
||||
return std::equal(oldT.begin(), oldT.end(), value.dwords.begin());
|
||||
});
|
||||
if (allRaw) {
|
||||
require(!flushed && waits == 0u && rawFirstWords == 8u && holdsOld && !holdsNew,
|
||||
"descriptor pendingWrite barrier: an all-Raw T# did not return the complete pre-sync T#");
|
||||
} else {
|
||||
require(flushed && waits == 1u, "descriptor pendingWrite barrier: the pending GPU write on the second half of T# was not flushed");
|
||||
if (primaryBarrier) {
|
||||
require(rawFirstWords == 0u, "descriptor pendingWrite primary barrier: leading T# dwords took PendingWrite::Raw despite a pending Sync in the 32-byte descriptor span");
|
||||
} else {
|
||||
require(rawFirstWords > 0u, "descriptor pendingWrite secondary barrier test setup did not exercise initial Raw dword reads before the GPU wait");
|
||||
}
|
||||
require(holdsNew && !holdsOld,
|
||||
primaryBarrier ? "descriptor pendingWrite primary barrier: capture returned a torn or stale T# instead of the complete post-sync T#"
|
||||
: "descriptor pendingWrite secondary barrier: cached Raw dwords were not invalidated and re-read after the GPU wait");
|
||||
}
|
||||
}
|
||||
|
||||
static Texture extraTexture;
|
||||
const auto thirdT = makeImage(extraTexture, 11u);
|
||||
const auto makeTableV = [](const void* data, std::uint32_t stride, std::uint32_t numRecords) {
|
||||
const auto base = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
return std::array<std::uint32_t, 4>{
|
||||
static_cast<std::uint32_t>(base),
|
||||
static_cast<std::uint32_t>((base >> 32u) & 0xffffu) | (stride << 16u),
|
||||
numRecords,
|
||||
stride == 0u ? 0x31016facu : 0x01016facu,
|
||||
};
|
||||
};
|
||||
const std::vector<std::uint32_t> wholeTableCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u,
|
||||
0xf4080700u, 0xfa000030u, 0x7e200500u, 0x8f108510u, 0xf42c0502u, 0x20000000u,
|
||||
0xf09c0f08u, 0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u,
|
||||
};
|
||||
auto wideTableCode = wholeTableCode;
|
||||
std::replace(wideTableCode.begin(), wideTableCode.end(), 0x8f108510u, 0x9310b010u);
|
||||
const std::vector<std::uint32_t> indirectTableCode{
|
||||
0xf4080200u, 0xfa000000u, 0xf4080700u, 0xfa000010u, 0xf4080800u, 0xfa000020u,
|
||||
0xf4080100u, 0xfa000030u, 0x7e200500u, 0x8f108210u, 0xf4200444u, 0x20000000u,
|
||||
0x8711ff11u, 0x000000ffu, 0x8f128411u, 0xf4240b02u, 0x24000008u, 0xf4040b96u,
|
||||
0xfa000010u, 0xf40c0917u, 0xfa000020u, 0xf09c0f08u, 0x01090000u, 0xe0700000u,
|
||||
0x80070000u, 0xbf810000u,
|
||||
};
|
||||
alignas(4096) static std::array<std::uint32_t, 20> tableSrt{};
|
||||
static std::array<std::array<std::uint32_t, 4>, 3> tableMaterials{};
|
||||
static std::array<std::uint32_t, 4> tableOutput{};
|
||||
const auto tableSrtAddr = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(tableSrt.data()));
|
||||
const std::array<std::uint32_t, 2> tableUserData{static_cast<std::uint32_t>(tableSrtAddr), static_cast<std::uint32_t>(tableSrtAddr >> 32u)};
|
||||
const auto inPageSrtAddr = srtAddress + 512u * sizeof(std::uint32_t);
|
||||
const std::array<std::uint32_t, 2> inPageUserData{static_cast<std::uint32_t>(inPageSrtAddr), static_cast<std::uint32_t>(inPageSrtAddr >> 32u)};
|
||||
const auto fillWholeSrt = [&](std::uint32_t* dst, const void* heap, std::uint32_t stride, std::uint32_t numRecords) {
|
||||
const auto heapV = makeTableV(heap, stride, numRecords);
|
||||
const auto matV = makeTableV(tableMaterials.data(), 16u, 3u);
|
||||
const auto outV = makeTableV(tableOutput.data(), 0u, 16u);
|
||||
std::fill_n(dst, 20u, 0u);
|
||||
std::copy(heapV.begin(), heapV.end(), dst);
|
||||
std::copy(matV.begin(), matV.end(), dst + 8u);
|
||||
std::copy(outV.begin(), outV.end(), dst + 12u);
|
||||
};
|
||||
RecompileRequest tableReq = request;
|
||||
tableReq.shader = {ShaderStage::Compute, 0x23010u, wholeTableCode, 0, {}};
|
||||
tableReq.context.waveSize = 64u;
|
||||
tableReq.context.userData = tableUserData;
|
||||
tableReq.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
RecompileRequest wideTableReq = tableReq;
|
||||
wideTableReq.shader = {ShaderStage::Compute, 0x23018u, wideTableCode, 0, {}};
|
||||
wideTableReq.context.userData = inPageUserData;
|
||||
|
||||
static std::uint32_t phase = 0;
|
||||
static std::array<std::uint32_t, 8> pendingT0{};
|
||||
static std::array<std::uint32_t, 8> pendingT1{};
|
||||
static std::array<std::uint32_t, 8> pendingT2{};
|
||||
static std::uint64_t record0Addr = 0;
|
||||
static std::uint64_t record1Addr = 0;
|
||||
static std::uint64_t record2Addr = 0;
|
||||
|
||||
static std::uint32_t forcedHookCalls = 0;
|
||||
static std::uint32_t srtRootReads = 0;
|
||||
static std::uint64_t lastPendingAddr = 0;
|
||||
static bool gateSecondWaitOnSrtRoot = false;
|
||||
|
||||
AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) {
|
||||
if (phase == 1u && !flushed && address < syncEnd && syncBegin < address + bytes) {
|
||||
flushed = true;
|
||||
++waits;
|
||||
std::memcpy(reinterpret_cast<void*>(record0Addr), pendingT0.data(), sizeof(pendingT0));
|
||||
if (record1Addr != 0u) std::memcpy(reinterpret_cast<void*>(record1Addr), pendingT1.data(), sizeof(pendingT1));
|
||||
} else if (phase == 2u || phase == 3u) {
|
||||
if (waits == 0u && address < record1Addr + 32u && record1Addr + 12u < address + bytes) {
|
||||
++waits;
|
||||
std::memcpy(reinterpret_cast<void*>(record1Addr), pendingT1.data(), sizeof(pendingT1));
|
||||
} else if ((waits == 1u || (phase == 3u && waits >= 1u)) && address < record2Addr + 32u && record2Addr + 12u < address + bytes) {
|
||||
++waits;
|
||||
std::memcpy(reinterpret_cast<void*>(record0Addr), pendingT0.data(), sizeof(pendingT0));
|
||||
std::memcpy(reinterpret_cast<void*>(record2Addr), pendingT2.data(), sizeof(pendingT2));
|
||||
} else if (phase == 2u && waits == 2u && address < record0Addr + 32u && record0Addr < address + bytes) {
|
||||
++forcedHookCalls;
|
||||
}
|
||||
} else if (phase == 5u) {
|
||||
++waits;
|
||||
}
|
||||
});
|
||||
|
||||
const auto multiPending = +[](std::uint64_t address, std::size_t bytes, std::span<std::byte> known) -> AgcDriver::ShaderMemory::PendingWrite {
|
||||
using Pending = AgcDriver::ShaderMemory::PendingWrite;
|
||||
if (bytes >= 4096u) {
|
||||
return address <= srtAddress && srtAddress < address + bytes ? Pending::Sync : Pending::None;
|
||||
}
|
||||
if (address != lastPendingAddr) {
|
||||
lastPendingAddr = address;
|
||||
if (address == srtAddress + 512u * sizeof(std::uint32_t) + 48u) {
|
||||
++srtRootReads;
|
||||
}
|
||||
}
|
||||
if (phase == 0u || phase == 5u) return Pending::Raw;
|
||||
if (phase == 4u) {
|
||||
if (address >= record0Addr && address + bytes <= record0Addr + 32u) {
|
||||
if (known.size() == bytes) {
|
||||
std::memcpy(known.data(), reinterpret_cast<const std::byte*>(pendingT0.data()) + (address - record0Addr), bytes);
|
||||
}
|
||||
return Pending::KnownValue;
|
||||
}
|
||||
return Pending::Raw;
|
||||
}
|
||||
if (phase == 1u) {
|
||||
if (flushed) return Pending::None;
|
||||
if (spanAwareSync) {
|
||||
if (address < syncEnd && syncBegin < address + bytes) return Pending::Sync;
|
||||
} else {
|
||||
if (address >= syncBegin && address < syncEnd) return Pending::Sync;
|
||||
}
|
||||
return Pending::Raw;
|
||||
}
|
||||
if (waits == 0u) {
|
||||
if (address >= record1Addr + 12u && address < record1Addr + 32u && bytes == sizeof(std::uint32_t)) return Pending::Sync;
|
||||
return Pending::Raw;
|
||||
}
|
||||
if (waits == 1u || (phase == 3u && waits >= 1u)) {
|
||||
if (address >= record1Addr && address + bytes <= record1Addr + 32u) return Pending::None;
|
||||
if ((!gateSecondWaitOnSrtRoot || srtRootReads >= 2u) && address >= record2Addr + 12u && address < record2Addr + 32u && bytes == sizeof(std::uint32_t)) return Pending::Sync;
|
||||
return Pending::Raw;
|
||||
}
|
||||
if (phase == 2u && waits == 2u) {
|
||||
if (gateSecondWaitOnSrtRoot &&
|
||||
((srtRootReads == 3u && address == srtAddress + 512u * sizeof(std::uint32_t) + 56u) ||
|
||||
(srtRootReads == 4u && address == srtAddress + 512u * sizeof(std::uint32_t) + 60u))) {
|
||||
return Pending::Sync;
|
||||
}
|
||||
if ((gateSecondWaitOnSrtRoot && address >= srtAddress + 512u * sizeof(std::uint32_t) + 48u && address < srtAddress + 512u * sizeof(std::uint32_t) + 64u) ||
|
||||
(address < record0Addr + 32u && record0Addr < address + bytes)) {
|
||||
return Pending::Raw;
|
||||
}
|
||||
}
|
||||
return Pending::None;
|
||||
};
|
||||
|
||||
srt.fill(0u);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin());
|
||||
std::copy(outputV.begin(), outputV.end(), srt.begin() + 16);
|
||||
srt[20] = 1u;
|
||||
phase = 0u;
|
||||
waits = 0u;
|
||||
flushed = false;
|
||||
record0Addr = srtAddress;
|
||||
record1Addr = 0u;
|
||||
pendingT0 = thirdT;
|
||||
{
|
||||
AgcDriver::ShaderMemory knownTransitionMemory({}, multiPending, nullptr, hookWaits);
|
||||
const auto rawCapture = knownTransitionMemory.Capture(request);
|
||||
require(!flushed && waits == 0u && std::ranges::any_of(rawCapture->snapshot.images, [&](const DescriptorValue& value) {
|
||||
return std::equal(oldT.begin(), oldT.end(), value.dwords.begin());
|
||||
}), "raw-to-known descriptor transition: initial Raw capture did not cache oldT");
|
||||
phase = 4u;
|
||||
const auto knownCapture = knownTransitionMemory.Capture(request);
|
||||
require(!flushed && waits == 0u && std::ranges::any_of(knownCapture->snapshot.images, [&](const DescriptorValue& value) {
|
||||
return std::equal(thirdT.begin(), thirdT.end(), value.dwords.begin());
|
||||
}), "raw-to-known descriptor transition: cached Raw descriptor was not invalidated and replaced by KnownValue");
|
||||
}
|
||||
|
||||
for (std::uint32_t record = 0; record < 128u; ++record) {
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + record * 8u);
|
||||
}
|
||||
fillWholeSrt(tableSrt.data(), srt.data(), 32u, 128u);
|
||||
phase = 0u;
|
||||
waits = 0u;
|
||||
flushed = false;
|
||||
record0Addr = srtAddress;
|
||||
record1Addr = 0u;
|
||||
pendingT0 = thirdT;
|
||||
{
|
||||
AgcDriver::ShaderMemory fullPageMemory({}, multiPending, nullptr, hookWaits);
|
||||
const auto initialCapture = fullPageMemory.Capture(tableReq);
|
||||
const auto& initialCol = initialCapture->snapshot.tables.tables.at(0);
|
||||
require(initialCol.keys == 128u && initialCol.words.at(initialCol.codes.at(0)).dwords == oldT,
|
||||
"full raw page readWords test: initial raw capture did not populate the table with oldT");
|
||||
phase = 4u;
|
||||
const auto knownCapture = fullPageMemory.Capture(tableReq);
|
||||
const auto& knownCol = knownCapture->snapshot.tables.tables.at(0);
|
||||
require(!flushed && waits == 0u && knownCol.words.at(knownCol.codes.at(0)).dwords == thirdT,
|
||||
"full raw page readWords: cached Raw descriptor words did not consume KnownValue replacement");
|
||||
phase = 1u;
|
||||
pendingT0 = newT;
|
||||
spanAwareSync = true;
|
||||
syncBegin = srtAddress + 12u;
|
||||
syncEnd = srtAddress + 32u;
|
||||
const auto updatedCapture = fullPageMemory.Capture(tableReq);
|
||||
const auto& updatedCol = updatedCapture->snapshot.tables.tables.at(0);
|
||||
require(flushed && waits == 1u && updatedCol.words.at(updatedCol.codes.at(0)).dwords == newT,
|
||||
"full raw page readWords: readWords() bypassed pendingWrite policy on a fully cached wordwise page");
|
||||
}
|
||||
|
||||
for (const bool spanSync : {false, true}) {
|
||||
std::fill(srt.begin(), srt.end(), 0u);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin());
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + 12u);
|
||||
fillWholeSrt(srt.data() + 512u, srt.data(), 48u, 2u);
|
||||
phase = 1u;
|
||||
waits = 0u;
|
||||
flushed = false;
|
||||
spanAwareSync = spanSync;
|
||||
record0Addr = srtAddress;
|
||||
record1Addr = srtAddress + 48u;
|
||||
syncBegin = record1Addr + 12u;
|
||||
syncEnd = record1Addr + 32u;
|
||||
pendingT0 = newT;
|
||||
pendingT1 = thirdT;
|
||||
AgcDriver::ShaderMemory multiRunMemory({}, multiPending, nullptr, hookWaits);
|
||||
const auto multiRunCapture = multiRunMemory.Capture(wideTableReq);
|
||||
const auto& col = multiRunCapture->snapshot.tables.tables.at(0);
|
||||
require(flushed && waits == 1u && col.keys == 2u, "multi-run table wait: second run did not flush");
|
||||
require(col.words.at(col.codes.at(0)).dwords == newT && col.words.at(col.codes.at(1)).dwords == thirdT,
|
||||
"multi-run table wait: a GPU wait on a later run did not rebuild descriptors from an earlier run");
|
||||
}
|
||||
|
||||
{
|
||||
alignas(4096) static std::array<std::uint32_t, 16> indRoot{};
|
||||
alignas(4096) static std::array<std::uint32_t, 8> indRecords{};
|
||||
alignas(4096) static std::array<std::array<std::uint32_t, 8>, 2> indLinks{};
|
||||
static std::array<std::uint32_t, 8> indKeys{};
|
||||
const auto ptrAddr = [](const void* p) { return static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(p)); };
|
||||
const auto storePtr = [](std::uint32_t* at, std::uint64_t pointer) {
|
||||
at[0] = static_cast<std::uint32_t>(pointer);
|
||||
at[1] = static_cast<std::uint32_t>(pointer >> 32u);
|
||||
};
|
||||
std::fill(srt.begin(), srt.end(), 0u);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + 8u);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + 24u);
|
||||
storePtr(indRecords.data() + 2u, ptrAddr(indLinks[0].data()));
|
||||
storePtr(indRecords.data() + 6u, ptrAddr(indLinks[1].data()));
|
||||
storePtr(indLinks[0].data() + 4u, srtAddress);
|
||||
storePtr(indLinks[1].data() + 4u, srtAddress + 64u);
|
||||
const auto keysV = makeTableV(indKeys.data(), 0u, static_cast<std::uint32_t>(sizeof(indKeys)));
|
||||
const auto outV = makeTableV(tableOutput.data(), 0u, static_cast<std::uint32_t>(sizeof(tableOutput)));
|
||||
const auto recsV = makeTableV(indRecords.data(), 0u, static_cast<std::uint32_t>(sizeof(indRecords)));
|
||||
indRoot.fill(0u);
|
||||
std::copy(keysV.begin(), keysV.end(), indRoot.begin());
|
||||
std::copy(outV.begin(), outV.end(), indRoot.begin() + 4u);
|
||||
std::copy(recsV.begin(), recsV.end(), indRoot.begin() + 12u);
|
||||
const std::array<std::uint32_t, 2> indUserData{static_cast<std::uint32_t>(ptrAddr(indRoot.data())), static_cast<std::uint32_t>(ptrAddr(indRoot.data()) >> 32u)};
|
||||
RecompileRequest indReq = tableReq;
|
||||
indReq.shader = {ShaderStage::Compute, 0x23020u, indirectTableCode, 0, {}};
|
||||
indReq.context.userData = indUserData;
|
||||
phase = 1u;
|
||||
waits = 0u;
|
||||
flushed = false;
|
||||
spanAwareSync = false;
|
||||
record0Addr = srtAddress + 32u;
|
||||
record1Addr = srtAddress + 96u;
|
||||
syncBegin = record1Addr + 12u;
|
||||
syncEnd = record1Addr + 32u;
|
||||
pendingT0 = newT;
|
||||
pendingT1 = thirdT;
|
||||
AgcDriver::ShaderMemory indirectMemory({}, multiPending, nullptr, hookWaits);
|
||||
const auto indCapture = indirectMemory.Capture(indReq);
|
||||
const auto& col = indCapture->snapshot.tables.tables.at(0);
|
||||
require(flushed && waits == 1u && col.keys == 2u, "indirect table wait: second record did not flush");
|
||||
require(col.words.at(col.codes.at(0)).dwords == newT && col.words.at(col.codes.at(1)).dwords == thirdT,
|
||||
"indirect table wait: a GPU wait on a later indirect record did not rebuild an earlier record");
|
||||
}
|
||||
|
||||
const auto postForcedSrtAddr = srtAddress + 256u * sizeof(std::uint32_t);
|
||||
const std::array<std::uint32_t, 2> postForcedUserData{static_cast<std::uint32_t>(postForcedSrtAddr), static_cast<std::uint32_t>(postForcedSrtAddr >> 32u)};
|
||||
RecompileRequest postForcedReq = request;
|
||||
postForcedReq.context.userData = postForcedUserData;
|
||||
|
||||
for (const int mode : {0, 1, 2}) {
|
||||
const bool stridedRuns = mode != 0;
|
||||
gateSecondWaitOnSrtRoot = mode == 2;
|
||||
const std::uint32_t stride = stridedRuns ? 48u : 32u;
|
||||
const std::size_t stepWords = stride / sizeof(std::uint32_t);
|
||||
std::fill(srt.begin(), srt.end(), 0u);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin());
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + stepWords);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + 2u * stepWords);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + 256u);
|
||||
std::copy(outputV.begin(), outputV.end(), srt.begin() + 256u + 16u);
|
||||
srt[256u + 20u] = 1u;
|
||||
fillWholeSrt(srt.data() + 512u, srt.data(), stride, 3u);
|
||||
RecompileRequest twoWaitReq = stridedRuns ? wideTableReq : tableReq;
|
||||
twoWaitReq.context.userData = inPageUserData;
|
||||
phase = 2u;
|
||||
waits = 0u;
|
||||
forcedHookCalls = 0u;
|
||||
srtRootReads = 0u;
|
||||
lastPendingAddr = 0u;
|
||||
flushed = false;
|
||||
record0Addr = srtAddress;
|
||||
record1Addr = srtAddress + stride;
|
||||
record2Addr = srtAddress + 2u * stride;
|
||||
pendingT0 = newT;
|
||||
pendingT1 = thirdT;
|
||||
pendingT2 = newT;
|
||||
AgcDriver::ShaderMemory twoWaitMemory({}, multiPending, nullptr, hookWaits);
|
||||
const auto twoWaitCapture = twoWaitMemory.Capture(twoWaitReq);
|
||||
const auto& col = twoWaitCapture->snapshot.tables.tables.at(0);
|
||||
require(waits == 2u && forcedHookCalls == 15u && (!gateSecondWaitOnSrtRoot || srtRootReads == 5u) && col.keys == 3u,
|
||||
"multi-wait readWords: forced third attempt did not execute or nested forceSync scope was not preserved");
|
||||
require(col.words.at(col.codes.at(0)).dwords == newT && col.words.at(col.codes.at(1)).dwords == thirdT && col.words.at(col.codes.at(2)).dwords == newT,
|
||||
"multi-wait readWords: retry pass accepted an unstable epoch after a second invalidation");
|
||||
phase = 5u;
|
||||
waits = 0u;
|
||||
forcedHookCalls = 0u;
|
||||
const auto normalAfterForced = twoWaitMemory.Capture(postForcedReq);
|
||||
const auto cachedAfterForced = twoWaitMemory.Capture(postForcedReq);
|
||||
require(waits == 0u && forcedHookCalls == 0u && std::ranges::any_of(normalAfterForced->snapshot.images, [&](const DescriptorValue& value) {
|
||||
return std::equal(oldT.begin(), oldT.end(), value.dwords.begin());
|
||||
}) && std::ranges::any_of(cachedAfterForced->snapshot.images, [&](const DescriptorValue& value) {
|
||||
return std::equal(oldT.begin(), oldT.end(), value.dwords.begin());
|
||||
}), "forced pass scope: a normal pass over Raw words after a forced third attempt waited on the hook or bumped rawInvalidations");
|
||||
|
||||
std::fill(srt.begin(), srt.end(), 0u);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin());
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + stepWords);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + 2u * stepWords);
|
||||
std::copy(oldT.begin(), oldT.end(), srt.begin() + 256u);
|
||||
std::copy(outputV.begin(), outputV.end(), srt.begin() + 256u + 16u);
|
||||
srt[256u + 20u] = 1u;
|
||||
fillWholeSrt(srt.data() + 512u, srt.data(), stride, 3u);
|
||||
phase = 3u;
|
||||
waits = 0u;
|
||||
forcedHookCalls = 0u;
|
||||
srtRootReads = 0u;
|
||||
lastPendingAddr = 0u;
|
||||
flushed = false;
|
||||
AgcDriver::ShaderMemory unstableMemory({}, multiPending, nullptr, hookWaits);
|
||||
bool refused = false;
|
||||
try {
|
||||
static_cast<void>(unstableMemory.Capture(twoWaitReq));
|
||||
} catch (const std::runtime_error&) {
|
||||
refused = true;
|
||||
}
|
||||
require(refused && waits >= 3u, "unstable epoch refusal: capture succeeded despite invalidation on every retry attempt");
|
||||
phase = 5u;
|
||||
waits = 0u;
|
||||
forcedHookCalls = 0u;
|
||||
const auto normalAfterUnstable = unstableMemory.Capture(postForcedReq);
|
||||
require(waits == 0u && forcedHookCalls == 0u && std::ranges::any_of(normalAfterUnstable->snapshot.images, [&](const DescriptorValue& value) {
|
||||
return std::equal(oldT.begin(), oldT.end(), value.dwords.begin());
|
||||
}), "forced pass exception scope: a normal pass over Raw words after a throwing forced attempt waited on the hook or bumped rawInvalidations");
|
||||
}
|
||||
gateSecondWaitOnSrtRoot = false;
|
||||
}
|
||||
|
||||
void verifyOutOfRangeConstantReads() {
|
||||
@@ -3454,6 +3924,38 @@ void verifyImageTableMaskedOffset() {
|
||||
require(codes.size() == 4u && codes[0] == codes[3] && codes[2] == Abi::NullCode && codes[1] != codes[0] && (codes[1] & Abi::PoisonFlag) == 0u, "image table masked offset: the whole table map is wrong");
|
||||
}
|
||||
|
||||
void verifyImageTableFirstLaneScale() {
|
||||
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[3] = TableT(textures[1].bytes.data(), TableFormat32UInt, TableType2D);
|
||||
heap[9] = heap[0];
|
||||
FillTableSrt(guest, heap.data(), 32u, 12u);
|
||||
const auto userData = TableUserData(guest);
|
||||
const auto scaled = [](std::initializer_list<std::uint32_t> offset) {
|
||||
std::vector<std::uint32_t> code(TableWholeCode);
|
||||
const auto first = std::find(code.begin(), code.end(), 0x7e200500u);
|
||||
require(first != code.end() && first[1] == 0x8f108510u, "image table first-lane scale: the program has no v_readfirstlane_b32 and s_lshl_b32");
|
||||
code.insert(code.erase(first, first + 2), offset);
|
||||
return code;
|
||||
};
|
||||
const auto code = scaled({0x34020085u, 0xd7460001u, 0x04050301u, 0x7e200501u});
|
||||
const auto request = TableRequest(code, userData);
|
||||
const auto plan = GetResourcePlan(request);
|
||||
require(plan->info.images.size() == 1u && plan->info.images[0].table == 0u && plan->info.images[0].tableOperation == TableOperation::Supported, "image table first-lane scale: 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->stride == 96u && column->addend == 0u && column->offset == 0u && column->dwordCount == 8u && column->unkeyed, "image table first-lane scale: the column is wrong");
|
||||
const auto compiled = CompileTable(request);
|
||||
const auto& snapshot = compiled.capture->snapshot.tables.tables.at(0);
|
||||
require(snapshot.records == 4u && snapshot.keys == 4u && snapshot.fault == 0u, "image table first-lane scale: the snapshot does not hold the four 96-byte records");
|
||||
const auto codes = TableCodes(TableMap(*compiled.result), 0);
|
||||
require(codes.size() == 4u && codes[0] == codes[3] && codes[2] == Abi::NullCode && codes[1] != codes[0] && (codes[0] & Abi::PoisonFlag) == 0u && (codes[1] & Abi::PoisonFlag) == 0u, "image table first-lane scale: the table map is wrong");
|
||||
}
|
||||
|
||||
void verifyImageTableRegistration() {
|
||||
using namespace ShaderRecompiler;
|
||||
namespace Abi = ImageTableAbi;
|
||||
@@ -5740,7 +6242,10 @@ void verifyPixelRequestSerialization() {
|
||||
minimal.context.pixel->layerExported = true;
|
||||
minimal.context.pixel->ditheredAlphaToCoverage = true;
|
||||
const auto encoded = serializer.Serialize(minimal);
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQS8AAAA=", "new requests did not use serialization version 47");
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQTEAAAA=", "new requests did not use serialization version 49");
|
||||
auto withBarycentric = minimal;
|
||||
withBarycentric.target.fragmentBarycentricBuiltins = true;
|
||||
require(serializer.Deserialize(serializer.Serialize(withBarycentric)).request.target.fragmentBarycentricBuiltins && !serializer.Deserialize(encoded).request.target.fragmentBarycentricBuiltins, "barycentric built-ins were lost in serialization");
|
||||
const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u};
|
||||
auto withFallbacks = minimal;
|
||||
withFallbacks.context.linkedCallFallbacks = fallbacks;
|
||||
@@ -5858,7 +6363,7 @@ void verifyPixelRequestSerialization() {
|
||||
for (std::size_t bytes = 0; bytes < 45u; ++bytes) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated pixel mapping, packing, dual-source, alpha-to-coverage, stencil reference export flag, primitive class, layer export, dither or front-face flag, quad pixel mask or quad sample mask was accepted");
|
||||
}
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQTAAAAA="}) {
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQTIAAAA="}) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
|
||||
}
|
||||
static constexpr std::string_view version18Request{
|
||||
@@ -5930,6 +6435,132 @@ std::string encodeBase64(const std::vector<std::uint8_t>& bytes) {
|
||||
return text;
|
||||
}
|
||||
|
||||
void verifyDepthExportClamp() {
|
||||
using namespace ShaderRecompiler;
|
||||
static constexpr std::array<std::uint32_t, 3> code{0xf8001881u, 0x00000000u, 0xbf810000u};
|
||||
ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter);
|
||||
pixel.hasPerspectiveCenterVgpr = true;
|
||||
pixel.depthExportEnable = true;
|
||||
pixel.depthExportMin = 0.25f;
|
||||
pixel.depthExportMax = 0.75f;
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}};
|
||||
request.context.waveSize = 64;
|
||||
request.context.pixel = pixel;
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 64;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
const auto result = Recompile(request);
|
||||
const auto& words = result.spirv.Words();
|
||||
std::map<std::uint32_t, std::uint32_t> floats;
|
||||
std::vector<std::array<std::uint32_t, 3>> clamps;
|
||||
std::vector<std::uint32_t> stored;
|
||||
std::uint32_t depthVariable = 0;
|
||||
bool depthReplacing = false;
|
||||
for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) {
|
||||
const auto op = static_cast<spv::Op>(words[at] & 0xffffu);
|
||||
if (op == spv::OpConstant && (words[at] >> 16u) == 4u) floats[words[at + 2]] = words[at + 3];
|
||||
if (op == spv::OpExtInst && words[at + 4] == 81u) clamps.push_back({words[at + 2], words[at + 6], words[at + 7]});
|
||||
if (op == spv::OpDecorate && words[at + 2] == spv::DecorationBuiltIn && words[at + 3] == spv::BuiltInFragDepth) depthVariable = words[at + 1];
|
||||
if (op == spv::OpStore) stored.push_back(words[at + 1]), stored.push_back(words[at + 2]);
|
||||
if (op == spv::OpExecutionMode && words[at + 2] == spv::ExecutionModeDepthReplacing) depthReplacing = true;
|
||||
}
|
||||
require(depthVariable != 0u && depthReplacing, "depth export: the pixel shader does not replace gl_FragDepth");
|
||||
require(clamps.size() == 1u && floats[clamps[0][1]] == 0x3e800000u && floats[clamps[0][2]] == 0x3f400000u, "depth export: the exported depth was not NClamped to [ZMIN, ZMAX]");
|
||||
bool clampStored = false;
|
||||
for (std::size_t i = 0; i + 1 < stored.size(); i += 2) clampStored = clampStored || (stored[i] == depthVariable && stored[i + 1] == clamps[0][0]);
|
||||
require(clampStored, "depth export: gl_FragDepth was not written from the clamped value");
|
||||
const auto replay = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(request));
|
||||
require(replay.request.context.pixel->depthExportMin == 0.25f && replay.request.context.pixel->depthExportMax == 0.75f, "depth export: the clamp did not survive serialization");
|
||||
{
|
||||
auto tail = request;
|
||||
tail.target.largePixelFunctionLdsBuffer = true;
|
||||
tail.target.pixelFunctionStorageDwords = 2048u;
|
||||
const auto both = RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(tail));
|
||||
require(both.request.target.largePixelFunctionLdsBuffer && both.request.target.pixelFunctionStorageDwords == 2048u &&
|
||||
both.request.context.pixel->depthExportMin == 0.25f && both.request.context.pixel->depthExportMax == 0.75f,
|
||||
"depth export: a version 49 request lost the bounds or the version 45 target tail");
|
||||
auto bytes = decodeBase64(RequestSerializer{}.Serialize(tail));
|
||||
require(bytes.size() > 8u && bytes[4] == 49u, "depth export: a depth exporting request did not use serialization version 49");
|
||||
static constexpr std::array<std::uint8_t, 8> boundBytes{0x00u, 0x00u, 0x80u, 0x3eu, 0x00u, 0x00u, 0x40u, 0x3fu};
|
||||
const auto found = std::search(bytes.begin(), bytes.end(), boundBytes.begin(), boundBytes.end());
|
||||
require(found != bytes.end() && std::search(found + 1, bytes.end(), boundBytes.begin(), boundBytes.end()) == bytes.end(),
|
||||
"depth export: the clamp bounds were not written exactly once");
|
||||
bytes.erase(found, found + boundBytes.size());
|
||||
for (const std::uint8_t legacyVersion : {48u, 47u, 46u}) {
|
||||
if (legacyVersion == 47u) bytes.pop_back();
|
||||
bytes[4] = legacyVersion;
|
||||
const auto legacy = RequestSerializer{}.Deserialize(encodeBase64(bytes));
|
||||
require(legacy.request.context.pixel->depthExportEnable && legacy.request.context.pixel->depthExportMin == 0.0f &&
|
||||
legacy.request.context.pixel->depthExportMax == 1.0f && legacy.request.target.largePixelFunctionLdsBuffer &&
|
||||
legacy.request.target.pixelFunctionStorageDwords == 2048u,
|
||||
legacyVersion == 46u ? "depth export: a version 46 depth exporting request did not read with default bounds and its target tail"
|
||||
: legacyVersion == 47u ? "depth export: a version 47 depth exporting request did not read with default bounds and its target tail"
|
||||
: "depth export: a version 48 depth exporting request did not read with default bounds and its target tail");
|
||||
}
|
||||
for (const bool depthExport : {false, true}) {
|
||||
auto dithered = request;
|
||||
auto ditheredPixel = *dithered.context.pixel;
|
||||
ditheredPixel.depthExportEnable = depthExport;
|
||||
ditheredPixel.ditheredAlphaToCoverage = true;
|
||||
ditheredPixel.ditheredAlphaSamples = 2u;
|
||||
dithered.context.pixel = ditheredPixel;
|
||||
auto ditheredBytes = decodeBase64(RequestSerializer{}.Serialize(dithered));
|
||||
require(ditheredBytes.size() > 8u && ditheredBytes[4] == 49u, "a two-sample dithered request did not use serialization version 49");
|
||||
if (depthExport) {
|
||||
const auto bounds = std::search(ditheredBytes.begin(), ditheredBytes.end(), boundBytes.begin(), boundBytes.end());
|
||||
require(bounds != ditheredBytes.end(), "a two-sample dithered depth exporting request lacks the clamp bounds");
|
||||
ditheredBytes.erase(bounds, bounds + boundBytes.size());
|
||||
}
|
||||
ditheredBytes[4] = 48u;
|
||||
const auto main48 = RequestSerializer{}.Deserialize(encodeBase64(ditheredBytes));
|
||||
require(main48.request.context.pixel->ditheredAlphaToCoverage && main48.request.context.pixel->ditheredAlphaSamples == 2u && main48.request.context.pixel->depthExportEnable == depthExport &&
|
||||
(!depthExport || (main48.request.context.pixel->depthExportMin == 0.0f && main48.request.context.pixel->depthExportMax == 1.0f)),
|
||||
"a version 48 two-sample dithered request, as main writes it with no depth tail, did not decode as written");
|
||||
}
|
||||
}
|
||||
std::vector<std::uint64_t> key;
|
||||
RecompileCacheKey::Build(request, key);
|
||||
const auto first = key;
|
||||
auto other = request;
|
||||
auto changed = pixel;
|
||||
changed.depthExportMax = 1.0f;
|
||||
other.context.pixel = changed;
|
||||
RecompileCacheKey::Build(other, key);
|
||||
require(key != first && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(other), "depth export: the cache keys ignore the clamp");
|
||||
auto plain = pixel;
|
||||
plain.depthExportEnable = false;
|
||||
auto unexported = request;
|
||||
unexported.context.pixel = plain;
|
||||
RecompileCacheKey::Build(unexported, key);
|
||||
const auto plainKey = key;
|
||||
plain.depthExportMin = 0.0f;
|
||||
plain.depthExportMax = 1.0f;
|
||||
unexported.context.pixel = plain;
|
||||
RecompileCacheKey::Build(unexported, key);
|
||||
require(key == plainKey, "depth export: the clamp keyed a shader without depth export");
|
||||
auto open = pixel;
|
||||
open.depthExportMin = -std::numeric_limits<float>::infinity();
|
||||
open.depthExportMax = std::numeric_limits<float>::infinity();
|
||||
auto unclamped = request;
|
||||
unclamped.context.pixel = open;
|
||||
const auto unclampedResult = Recompile(unclamped);
|
||||
const auto& openWords = unclampedResult.spirv.Words();
|
||||
bool openClamp = false;
|
||||
bool openStore = false;
|
||||
std::uint32_t openDepth = 0;
|
||||
for (std::size_t at = 5; at < openWords.size() && (openWords[at] >> 16u) != 0; at += openWords[at] >> 16u) {
|
||||
const auto op = static_cast<spv::Op>(openWords[at] & 0xffffu);
|
||||
if (op == spv::OpExtInst && openWords[at + 4] == 81u) openClamp = true;
|
||||
if (op == spv::OpDecorate && openWords[at + 2] == spv::DecorationBuiltIn && openWords[at + 3] == spv::BuiltInFragDepth) openDepth = openWords[at + 1];
|
||||
if (op == spv::OpStore && openWords[at + 1] == openDepth && openDepth != 0u) openStore = true;
|
||||
}
|
||||
require(!openClamp && openStore, "depth export: an unclamped export (DISABLE_VIEWPORT_CLAMP) was clamped or not stored");
|
||||
}
|
||||
|
||||
void verifyVertexInfoVersion19() {
|
||||
using namespace ShaderRecompiler;
|
||||
const std::array<std::uint32_t, 1> code{0xbf810000u};
|
||||
@@ -5954,6 +6585,7 @@ void verifyVertexInfoVersion19() {
|
||||
version20Vertex.pop_back();
|
||||
version20Vertex.pop_back();
|
||||
version20Vertex.pop_back();
|
||||
version20Vertex.pop_back();
|
||||
version20Vertex[4] = 20u;
|
||||
const auto replay20Vertex = serializer.Deserialize(encodeBase64(version20Vertex));
|
||||
require(replay20Vertex.request.context.vertex.has_value() && replay20Vertex.request.context.vertex->paClVsOutCntl == 0x89abcdefu, "a main version20 request lost PA_CL_VS_OUT_CNTL");
|
||||
@@ -5967,6 +6599,7 @@ void verifyVertexInfoVersion19() {
|
||||
legacy.pop_back();
|
||||
legacy.pop_back();
|
||||
legacy.pop_back();
|
||||
legacy.pop_back();
|
||||
legacy.erase(legacy.begin() + offset, legacy.begin() + offset + 4u);
|
||||
for (const auto version : {16u, 17u, 18u, 19u}) {
|
||||
legacy[4] = static_cast<std::uint8_t>(version);
|
||||
@@ -6013,11 +6646,11 @@ void verifyVertexInfoVersion19() {
|
||||
const auto mergingBytes = decodeBase64(serializer.Serialize(merging));
|
||||
require(mergingBytes.size() == plainPixel.size() && std::equal(plainPixel.begin(), plainPixel.begin() + mergeOffset, mergingBytes.begin()) && mergingBytes[mergeOffset] == 3u && mergingBytes[mergeOffset + 1] == 0x0fu && mergingBytes[mergeOffset + 2] == 0xf0u && mergingBytes[mergeOffset + 5] == 2u && mergingBytes[mergeOffset + 6] == 1u && mergingBytes[mergeOffset + 34] == 0xefu && mergingBytes[mergeOffset + 37] == 0x89u && mergingBytes[mergeOffset + 38] == 1u && mergingBytes[mergeOffset + 39] == 1u && std::equal(plainPixel.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes), plainPixel.end(), mergingBytes.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes)), "the color target merge is not the 40 bytes the version 46 pixel info adds after the sample order");
|
||||
require(serializer.Deserialize(serializer.Serialize(merging)).request.context.pixel->targetMerge == merging.context.pixel->targetMerge, "a serialized request lost its color target merge");
|
||||
// A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag or storage size at the end.
|
||||
// A version 43 request: no color target merge, and no fused arithmetic flag, pixel function LDS flag, storage size or barycentric built-ins flag at the end.
|
||||
const auto version43Bytes = [&](std::vector<std::uint8_t> bytes) {
|
||||
require(bytes.end() - bytes.begin() > mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes) && std::equal(plainPixel.begin() + mergeOffset, plainPixel.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes), bytes.begin() + mergeOffset), "a replay fixture does not hold the default color target merge after the sample order");
|
||||
bytes.erase(bytes.begin() + mergeOffset, bytes.begin() + mergeOffset + static_cast<std::ptrdiff_t>(mergeBytes));
|
||||
bytes.resize(bytes.size() - sizeof(std::uint32_t) - 2u);
|
||||
bytes.resize(bytes.size() - sizeof(std::uint32_t) - 3u);
|
||||
return bytes;
|
||||
};
|
||||
auto version42 = version43Bytes(plainPixel);
|
||||
@@ -6179,13 +6812,14 @@ void verifyDepthRemapRequests() {
|
||||
const auto plainBytes = decodeBase64(plain);
|
||||
const auto remappedBytes = decodeBase64(remapped);
|
||||
constexpr std::size_t pointTailBytes = 13u;
|
||||
require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 13u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 13 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 6 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 6 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag, the pixel function LDS flag and the pixel function storage size of a vertex request");
|
||||
require(plainBytes.size() == remappedBytes.size() && plainBytes.size() > 14u + pointTailBytes && std::equal(plainBytes.begin(), plainBytes.end() - 14 - pointTailBytes, remappedBytes.begin()) && std::equal(plainBytes.end() - 7 - pointTailBytes, plainBytes.end(), remappedBytes.end() - 7 - pointTailBytes), "the depth remap flag and mode are not the 6 bytes before the f32 flush and denormal preserve flags, the point tail, the fused arithmetic flag, the pixel function LDS flag, the pixel function storage size and the barycentric built-in flag of a vertex request");
|
||||
for (const std::uint8_t legacyVersion : {32u, 34u}) {
|
||||
auto legacyBytes = remappedBytes;
|
||||
legacyBytes.resize(legacyBytes.size() - sizeof(std::uint32_t));
|
||||
legacyBytes.pop_back();
|
||||
legacyBytes.pop_back();
|
||||
legacyBytes.pop_back();
|
||||
legacyBytes.pop_back();
|
||||
legacyBytes[4] = legacyVersion;
|
||||
legacyBytes.resize(legacyBytes.size() - 6u - pointTailBytes);
|
||||
const auto legacy = serializer.Deserialize(encodeBase64(legacyBytes));
|
||||
@@ -6197,6 +6831,7 @@ void verifyDepthRemapRequests() {
|
||||
version35.pop_back();
|
||||
version35.pop_back();
|
||||
version35.pop_back();
|
||||
version35.pop_back();
|
||||
version35[4] = 35u;
|
||||
version35.resize(version35.size() - 1u - pointTailBytes);
|
||||
const auto replay35 = serializer.Deserialize(encodeBase64(version35));
|
||||
@@ -6244,8 +6879,8 @@ void verifyMergedHullConfiguration() {
|
||||
require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged");
|
||||
constexpr std::size_t pointTailBytes = 13u;
|
||||
auto legacy = decodeBase64(merged);
|
||||
require(legacy.size() > 54u + pointTailBytes && legacy[4] == 47u, "a merged hull request did not use serialization version 47");
|
||||
legacy.resize(legacy.size() - sizeof(std::uint32_t) - 2u - pointTailBytes);
|
||||
require(legacy.size() > 55u + pointTailBytes && legacy[4] == 49u, "a merged hull request did not use serialization version 49");
|
||||
legacy.resize(legacy.size() - sizeof(std::uint32_t) - 3u - pointTailBytes);
|
||||
legacy.pop_back();
|
||||
require(legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration");
|
||||
legacy.erase(legacy.end() - 52);
|
||||
@@ -6357,7 +6992,7 @@ void verifyLegacyPixelRequests() {
|
||||
require(!pixel.orderedPixelShader, "a legacy request became a primitive-ordered pixel shader");
|
||||
require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned");
|
||||
require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned");
|
||||
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules && !request.target.float16DenormPreserve, "legacy request trailer was misread");
|
||||
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u && !request.target.narrowSubgroupClock && !request.target.float64DenormPreserve && !request.target.registrationModules && !request.target.float16DenormPreserve && !request.target.fragmentBarycentricBuiltins, "legacy request trailer was misread");
|
||||
const auto upgraded = serializer.Deserialize(serializer.Serialize(request));
|
||||
require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping");
|
||||
}
|
||||
@@ -8418,6 +9053,27 @@ void verifyCubeDepthCompareMode() {
|
||||
require(modes[selected].cube && (modes[selected].emulatedCompare & EmulatedCompare::Enabled) != 0u, "cube color comparison did not select its emulated runtime mode");
|
||||
}
|
||||
|
||||
void verifyShortCubeMode() {
|
||||
using namespace ShaderRecompiler;
|
||||
ImageResource image{};
|
||||
image.resourceClass = ImageResourceClass::Sampled;
|
||||
image.dimension = RdnaImageDimension::Dim2DArray;
|
||||
DescriptorValue descriptor{{0x02143930u, 71u << 20u, 0u, 0xb1b00facu, 4u, 0u, 0u, 0u}, 8u};
|
||||
const auto modes = ResourceMaterializer::RuntimeImageModes(image);
|
||||
require(modes[ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)].cube, "a sampled cube with five slices did not select a cube runtime mode");
|
||||
image.sampledOnly = false;
|
||||
expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)); }, "fewer than six slices is implemented only for sampling", "a loaded cube with five slices was accepted");
|
||||
for (const auto range : {0u, 5u, 5u | (5u << 16u), 6u}) {
|
||||
descriptor.dwords[4] = range;
|
||||
require(modes[ResourceMaterializer::RuntimeImageMode(image, descriptor, modes)].cube, "a loaded cube with six faces or more was rejected");
|
||||
}
|
||||
image.sampledOnly = true;
|
||||
image.resourceClass = ImageResourceClass::Storage;
|
||||
image.written = true;
|
||||
descriptor.dwords[4] = 3u | (1u << 16u);
|
||||
expectFailure([&] { static_cast<void>(ResourceMaterializer::RuntimeImageMode(image, descriptor, ResourceMaterializer::RuntimeImageModes(image))); }, "fewer than six slices is implemented only for sampling", "a stored cube with three slices was accepted");
|
||||
}
|
||||
|
||||
void verifyVolumeArrayLoadMode() {
|
||||
using namespace ShaderRecompiler;
|
||||
const DescriptorValue descriptor{{0x02466600u, 0x01400000u, 0u, 0xa0500204u, 1u, 0x00700000u, 0u, 0u}, 8u};
|
||||
@@ -8711,6 +9367,7 @@ int main(int argc, char** argv) {
|
||||
verifyImageTableBulkKeyScan();
|
||||
verifyImageTableSrtKey();
|
||||
verifyImageTableMaskedOffset();
|
||||
verifyImageTableFirstLaneScale();
|
||||
verifyImageTableKeyUnion();
|
||||
verifyImageTableLateKey();
|
||||
verifyImageTableUnkeyedAccess();
|
||||
@@ -8816,6 +9473,7 @@ int main(int argc, char** argv) {
|
||||
verifyVertexInfoVersion19();
|
||||
verifyDepthRemapRequests();
|
||||
verifyMergedHullConfiguration();
|
||||
verifyDepthExportClamp();
|
||||
verifyPositionExportComponents();
|
||||
verifyLegacyPixelRequests();
|
||||
verifyPixelExportReplay();
|
||||
@@ -8840,6 +9498,7 @@ int main(int argc, char** argv) {
|
||||
verifyGuardedNullPointers();
|
||||
verifyFmaskImageMode();
|
||||
verifyCubeDepthCompareMode();
|
||||
verifyShortCubeMode();
|
||||
verifyVolumeArrayLoadMode();
|
||||
verifyArrayVolumeStoreMode();
|
||||
verifyMultisamplePlaneReadMode();
|
||||
|
||||
@@ -24,7 +24,19 @@ constexpr std::uint32_t DsOperand32 = 0x00000401u;
|
||||
constexpr std::uint32_t FlatAtomicAddU32 = 0xdcc90000u;
|
||||
constexpr std::uint32_t FlatAtomicOperands = 0x0a7d0402u;
|
||||
|
||||
struct LockFence {
|
||||
std::uint32_t scope = 0u;
|
||||
std::uint32_t semantics = 0u;
|
||||
std::size_t accessesBefore = 0;
|
||||
};
|
||||
|
||||
struct LockRegion {
|
||||
std::vector<LockFence> fences;
|
||||
std::size_t accesses = 0;
|
||||
};
|
||||
|
||||
struct Usage {
|
||||
std::vector<LockRegion> regions;
|
||||
std::size_t elects = 0;
|
||||
std::size_t exchanges = 0;
|
||||
std::size_t releases = 0;
|
||||
@@ -40,7 +52,7 @@ void Require(bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> RecompileProgram(std::span<const std::uint32_t> code) {
|
||||
std::vector<std::uint32_t> RecompileProgram(std::span<const std::uint32_t> code, bool ldsLockFences) {
|
||||
const std::array<std::uint32_t, 7> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle, spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
const std::array<std::uint32_t, 4> userData{};
|
||||
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
@@ -59,6 +71,7 @@ std::vector<std::uint32_t> RecompileProgram(std::span<const std::uint32_t> code)
|
||||
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
|
||||
request.target.maxWorkgroupInvocations = 1024;
|
||||
request.target.maxWorkgroupSharedMemoryBytes = 49152;
|
||||
request.target.ldsLockFences = ldsLockFences;
|
||||
request.layout = {0, 0, 0, 128};
|
||||
request.useCache = false;
|
||||
auto result = Recompile(request);
|
||||
@@ -101,8 +114,10 @@ Usage Scan(std::span<const std::uint32_t> words) {
|
||||
if (op == spv::OpBranchConditional && count >= 4u && words[cursor + 1u] == laneZeroCondition) laneZeroBody = words[cursor + 2u];
|
||||
if (op == spv::OpConstant && count == 4) constants[words[cursor + 2]] = words[cursor + 3];
|
||||
if (op == spv::OpGroupNonUniformElect) ++usage.elects;
|
||||
if (locked && (op == spv::OpLoad || op == spv::OpStore || op == spv::OpAtomicIAdd || op == spv::OpAtomicCompareExchange)) ++usage.regions.back().accesses;
|
||||
if (op == spv::OpAtomicCompareExchange) {
|
||||
++usage.exchanges;
|
||||
if (!locked) usage.regions.emplace_back();
|
||||
locked = true;
|
||||
if (count >= 4u) lockPointer = words[cursor + 3u];
|
||||
}
|
||||
@@ -116,7 +131,13 @@ Usage Scan(std::span<const std::uint32_t> words) {
|
||||
++usage.storeReleases;
|
||||
locked = false;
|
||||
}
|
||||
if (op == spv::OpMemoryBarrier && locked) usage.fenceBetweenLock = true;
|
||||
if (op == spv::OpMemoryBarrier && locked) {
|
||||
usage.fenceBetweenLock = true;
|
||||
const auto scope = constants.find(words[cursor + 1]);
|
||||
const auto semantics = constants.find(words[cursor + 2]);
|
||||
Require(scope != constants.end() && semantics != constants.end(), "a memory barrier inside the workgroup lock has a non-constant scope or semantics");
|
||||
usage.regions.back().fences.push_back({scope->second, semantics->second, usage.regions.back().accesses});
|
||||
}
|
||||
if (op == spv::OpAtomicIAdd) {
|
||||
++usage.adds;
|
||||
if (locked) usage.addBetweenLock = true;
|
||||
@@ -156,11 +177,11 @@ Usage Scan(std::span<const std::uint32_t> words) {
|
||||
return usage;
|
||||
}
|
||||
|
||||
Usage Compile(std::initializer_list<std::uint32_t> body) {
|
||||
Usage Compile(std::initializer_list<std::uint32_t> body, bool ldsLockFences = false) {
|
||||
std::vector<std::uint32_t> code{0x7e020280u, 0x7e0402c1u, 0x7e060280u, 0x7e080281u};
|
||||
code.insert(code.end(), body);
|
||||
code.insert(code.end(), {0xbf8cc07fu, 0xbf810000u});
|
||||
return Scan(RecompileProgram(code));
|
||||
return Scan(RecompileProgram(code, ldsLockFences));
|
||||
}
|
||||
|
||||
void Check64BitOnlyLocks() {
|
||||
@@ -190,6 +211,28 @@ void CheckFlatAtomicTakesLockWith64Bit() {
|
||||
Require(!usage.fenceBetweenLock, "a 32-bit flat atomic into the LDS aperture emits a memory barrier while it holds the workgroup lock");
|
||||
}
|
||||
|
||||
void RequireFencedRegions(const Usage& usage, std::size_t expected, const std::string& name) {
|
||||
Require(usage.regions.size() == expected, name + " with LDS lock fences took the workgroup lock " + std::to_string(usage.regions.size()) + " times, expected " + std::to_string(expected));
|
||||
for (const auto& region : usage.regions) {
|
||||
Require(region.fences.size() == 2, name + " with LDS lock fences emitted " + std::to_string(region.fences.size()) + " memory barriers inside the workgroup lock, expected 2");
|
||||
const auto& acquire = region.fences.front();
|
||||
const auto& release = region.fences.back();
|
||||
Require(acquire.scope == spv::ScopeWorkgroup && release.scope == spv::ScopeWorkgroup, name + " with LDS lock fences emitted a lock fence that is not workgroup scope");
|
||||
Require((acquire.semantics & spv::MemorySemanticsAcquireMask) != 0 && (acquire.semantics & spv::MemorySemanticsWorkgroupMemoryMask) != 0, name + " with LDS lock fences does not start its critical section with an acquire fence on workgroup memory");
|
||||
Require((release.semantics & spv::MemorySemanticsReleaseMask) != 0 && (release.semantics & spv::MemorySemanticsWorkgroupMemoryMask) != 0, name + " with LDS lock fences does not end its critical section with a release fence on workgroup memory");
|
||||
Require(region.accesses != 0 && acquire.accessesBefore == 0 && release.accessesBefore == region.accesses, name + " with LDS lock fences accesses LDS outside the fences inside the workgroup lock");
|
||||
}
|
||||
}
|
||||
|
||||
void CheckLockFences() {
|
||||
RequireFencedRegions(Compile({DsAddU64, DsOperands}, true), 1, "ds_add_u64");
|
||||
const auto mixed = Compile({DsAddU64, DsOperands, DsAddU32Hi, DsOperand32}, true);
|
||||
RequireFencedRegions(mixed, 2, "ds_add_u32 with ds_add_u64");
|
||||
Require(mixed.adds == 1 && mixed.addBetweenLock, "with LDS lock fences, the 32-bit OpAtomicIAdd is not between the lock acquire and the lock release");
|
||||
const auto unfenced = Compile({DsAddU32Hi, DsOperand32}, true);
|
||||
Require(unfenced.regions.empty() && !unfenced.fenceBetweenLock, "with LDS lock fences, a program with only ds_add_u32 got a workgroup lock");
|
||||
}
|
||||
|
||||
void CheckFlatAtomicOnlyIsUnchanged() {
|
||||
const auto usage = Compile({FlatAtomicAddU32, FlatAtomicOperands});
|
||||
Require(usage.elects == 0, "a program with only a 32-bit flat atomic got a workgroup lock: elect " + std::to_string(usage.elects));
|
||||
@@ -212,6 +255,7 @@ int main() {
|
||||
CheckFlatAtomicTakesLockWith64Bit();
|
||||
CheckFlatAtomicOnlyIsUnchanged();
|
||||
Check32BitOnlyIsUnchanged();
|
||||
CheckLockFences();
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "SpirvLdsAtomicLock: %s\n", error.what());
|
||||
return 1;
|
||||
|
||||
@@ -474,7 +474,107 @@ void testInternedSpirvReleasesUnusedWords() {
|
||||
check(std::ranges::equal(again.Words(), words) && released.Lock().empty(), "SPIR-V interned again after its release changed its words");
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> foldedBitcasts(bool separateBlocks, bool differentInput, bool differentType, std::uint32_t nanOpcode, std::uint32_t decorationMask = 0u) {
|
||||
SpirvModule module;
|
||||
module.EmitCapability(spv::CapabilityShader);
|
||||
module.AddMemoryModel(spv::AddressingModelLogical, spv::MemoryModelGLSL450);
|
||||
const auto voidType = module.Type(spv::OpTypeVoid);
|
||||
const auto boolType = module.Type(spv::OpTypeBool);
|
||||
const auto wordType = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
const auto floatType = module.Type(spv::OpTypeFloat, 32u);
|
||||
const auto otherType = differentType ? module.Type(spv::OpTypeInt, 32u, 1u) : floatType;
|
||||
const auto vectorType = module.Type(spv::OpTypeVector, wordType, 3u);
|
||||
const auto input = module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassInput, vectorType), spv::StorageClassInput);
|
||||
module.AddAnnotation(spv::OpDecorate, input, spv::DecorationBuiltIn, spv::BuiltInGlobalInvocationId);
|
||||
const auto firstOutputType = nanOpcode != 0u ? boolType : floatType;
|
||||
const auto secondOutputType = nanOpcode != 0u ? boolType : otherType;
|
||||
const std::array outputs{
|
||||
module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassPrivate, firstOutputType), spv::StorageClassPrivate),
|
||||
module.DefineGlobalVariable(module.Type(spv::OpTypePointer, spv::StorageClassPrivate, secondOutputType), spv::StorageClassPrivate)};
|
||||
const auto mainId = module.AllocateId();
|
||||
const auto functionType = module.Type(spv::OpTypeFunction, voidType);
|
||||
const auto zero = module.Constant(spv::OpConstant, wordType, 0u);
|
||||
const auto trueValue = module.Constant(spv::OpConstantTrue, boolType);
|
||||
module.EmitEntryPoint(spv::ExecutionModelGLCompute, mainId, "main", {input});
|
||||
module.AddExecutionMode(mainId, spv::ExecutionModeLocalSize, 1u, 1u, 1u);
|
||||
module.AddFunction(spv::OpFunction, voidType, mainId, spv::FunctionControlMaskNone, functionType);
|
||||
module.AddFunction(spv::OpLabel, module.AllocateId());
|
||||
const auto loaded = module.AllocateId();
|
||||
const auto first = module.AllocateId();
|
||||
const auto second = module.AllocateId();
|
||||
const auto alias = module.AllocateId();
|
||||
module.AddFunction(spv::OpLoad, vectorType, loaded, input);
|
||||
module.AddFunction(spv::OpCompositeExtract, wordType, first, loaded, 0u);
|
||||
module.AddFunction(spv::OpCompositeExtract, wordType, second, loaded, 1u);
|
||||
module.AddFunction(spv::OpSelect, wordType, alias, trueValue, first, second);
|
||||
const auto emit = [&](bool other) {
|
||||
const auto cast = module.AllocateId();
|
||||
module.AddFunction(spv::OpBitcast, other ? otherType : floatType, cast, other ? (differentInput ? second : alias) : first);
|
||||
if ((decorationMask & (other ? 2u : 1u)) != 0u) module.AddAnnotation(spv::OpDecorate, cast, spv::DecorationRelaxedPrecision);
|
||||
if ((decorationMask & (other ? 8u : 4u)) != 0u) {
|
||||
const auto group = module.AllocateId();
|
||||
module.AddAnnotation(spv::OpDecorationGroup, group);
|
||||
module.AddAnnotation(spv::OpDecorate, group, spv::DecorationRelaxedPrecision);
|
||||
module.AddAnnotation(spv::OpGroupDecorate, group, cast);
|
||||
}
|
||||
auto result = cast;
|
||||
if (nanOpcode != 0u) {
|
||||
result = module.AllocateId();
|
||||
if (nanOpcode == spv::OpIsNan) module.AddFunction(nanOpcode, boolType, result, cast);
|
||||
else module.AddFunction(nanOpcode, boolType, result, cast, cast);
|
||||
}
|
||||
module.AddFunction(spv::OpStore, outputs[other ? 1u : 0u], result);
|
||||
};
|
||||
if (separateBlocks) {
|
||||
const auto condition = module.AllocateId();
|
||||
const auto left = module.AllocateId();
|
||||
const auto right = module.AllocateId();
|
||||
const auto merge = module.AllocateId();
|
||||
module.AddFunction(spv::OpINotEqual, boolType, condition, first, zero);
|
||||
module.AddFunction(spv::OpSelectionMerge, merge, spv::SelectionControlMaskNone);
|
||||
module.AddFunction(spv::OpBranchConditional, condition, left, right);
|
||||
module.AddFunction(spv::OpLabel, left);
|
||||
emit(false);
|
||||
module.AddFunction(spv::OpBranch, merge);
|
||||
module.AddFunction(spv::OpLabel, right);
|
||||
emit(true);
|
||||
module.AddFunction(spv::OpBranch, merge);
|
||||
module.AddFunction(spv::OpLabel, merge);
|
||||
} else {
|
||||
emit(false);
|
||||
emit(true);
|
||||
}
|
||||
module.AddFunction(spv::OpReturn);
|
||||
module.AddFunction(spv::OpFunctionEnd);
|
||||
return SpecializeSpirv(module.Finalize());
|
||||
}
|
||||
|
||||
void testFoldedBitcastsAndNanTests() {
|
||||
for (const auto nanOpcode : std::array<std::uint32_t, 3>{0u, spv::OpIsNan, spv::OpFUnordNotEqual}) {
|
||||
const auto same = opcodeCounts(foldedBitcasts(false, false, false, nanOpcode));
|
||||
const auto other = opcodeCounts(foldedBitcasts(false, true, false, nanOpcode));
|
||||
const auto branches = opcodeCounts(foldedBitcasts(true, false, false, nanOpcode));
|
||||
const auto count = [](const auto& counts, std::uint32_t op) {
|
||||
const auto found = counts.find(op);
|
||||
return found == counts.end() ? 0u : found->second;
|
||||
};
|
||||
check(count(same, spv::OpBitcast) == 1u, "folding a select left repeated bitcasts of one value in a block");
|
||||
check(count(other, spv::OpBitcast) == 2u, "folding merged bitcasts of different input values");
|
||||
check(count(branches, spv::OpBitcast) == 2u, "folding reused a bitcast from a sibling branch");
|
||||
if (nanOpcode != 0u) {
|
||||
check(count(same, nanOpcode) == 1u, "folding left repeated NaN tests of one value in a block");
|
||||
check(count(other, nanOpcode) == 2u, "folding merged NaN tests of different input values");
|
||||
check(count(branches, nanOpcode) == 2u, "folding reused a NaN test from a sibling branch");
|
||||
}
|
||||
}
|
||||
check(opcodeCounts(foldedBitcasts(false, false, true, 0u))[spv::OpBitcast] == 2u, "folding merged bitcasts to different result types");
|
||||
for (const auto decorations : {1u, 2u, 3u, 4u, 8u}) {
|
||||
check(opcodeCounts(foldedBitcasts(false, false, false, 0u, decorations))[spv::OpBitcast] == 2u, "folding merged a decorated bitcast with another value");
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
testFoldedBitcastsAndNanTests();
|
||||
testFoldingLeavesNoDeadBlocksOrConstants();
|
||||
testIdsBeyondTheBoundAreRejected();
|
||||
testStructExtractsAreNotShuffled();
|
||||
|
||||
@@ -384,7 +384,7 @@ std::optional<std::pair<std::uint32_t, std::uint32_t>> storedLaneFunction(const
|
||||
bool function = false;
|
||||
for (std::size_t offset = 5; offset < words.size(); offset += words[offset] >> spv::WordCountShift) {
|
||||
const auto opcode = static_cast<spv::Op>(words[offset] & spv::OpCodeMask);
|
||||
const auto* operands = &words[offset + 1];
|
||||
const auto* operands = words.data() + offset + 1;
|
||||
if (opcode == spv::OpFunction) function = true;
|
||||
if (opcode == spv::OpConstant) values[operands[1]] = {0u, operands[2]};
|
||||
if (opcode == spv::OpVariable && operands[2] == spv::StorageClassInput) inputs.insert(operands[1]);
|
||||
|
||||
@@ -144,11 +144,18 @@ void verifyEmittedBarriers() {
|
||||
require(single.subgroupControl == 0 && single.subgroupMemory == 0, "with wave LDS memory barriers, a single-wave workgroup did not keep workgroup-scope barriers");
|
||||
}
|
||||
|
||||
void verifyLdsLockFenceDrivers() {
|
||||
for (const auto& item : Cases) {
|
||||
require(AgcDriver::LdsLockFencesRequired(item.driver) == (item.driver == VK_DRIVER_ID_MOLTENVK), "driver " + std::to_string(static_cast<int>(item.driver)) + ": expected LDS lock fences only on MoltenVK");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
verifyDriverQuirk();
|
||||
verifyLdsLockFenceDrivers();
|
||||
verifyEmittedBarriers();
|
||||
verifyLdsMemoryBarrierScope();
|
||||
std::puts("subgroup barrier tests passed");
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/SynchronizationStatistics.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libc/include/GuestHeap.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
@@ -87,7 +88,9 @@ void testValidation() {
|
||||
expectFailure([] { sceAgcDriverAgrSubmitDcb(nullptr); });
|
||||
expectFailure([] { sceAgcDriverSubmitAcb(0x20, nullptr); });
|
||||
expectFailure([&] { sceAgcDriverSubmitAcb(0, &packet); });
|
||||
expectFailure([&] { sceAgcDriverSubmitAcb(0x58, &packet); });
|
||||
check(sceAgcDriverSubmitAcb(0x58, &packet) == 0, "queue 0x58 submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
expectFailure([&] { sceAgcDriverSubmitAcb(0x59, &packet); });
|
||||
packet.dw_num = 2;
|
||||
expectFailure([&] { sceAgcDriverSubmitDcb(&packet); });
|
||||
packet.dw_num = 3;
|
||||
@@ -208,6 +211,7 @@ void testEndOfPipeInterrupts() {
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip-only queue submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(output->ready.load() == 1, "flip-only queue flip was not handed to its output");
|
||||
check(AgcDriver::Graphics::HostImportFramesEnded() == 1, "the first flip did not end a host import frame");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 1 && events[0].udata == &graphicsTag && events[0].data == 1 && sceAgcDriverGetEqEventType(events.data()) == 0, "flip on a queue that never requested a release interrupt did not raise its event");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip-only queue event was not cleared");
|
||||
std::array<std::uint32_t, 8> words{0xc0064900, 0, 1u << 24u, 0, 0, 0, 0, 0};
|
||||
@@ -229,6 +233,7 @@ void testEndOfPipeInterrupts() {
|
||||
check(sceAgcDriverSubmitDcb(&flipPacket) == 0, "flip after a release interrupt submit failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(output->ready.load() == 2, "flip after a release interrupt was not handed to its output");
|
||||
check(AgcDriver::Graphics::HostImportFramesEnded() == 2, "a flip packet executed on the queue did not end a host import frame");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "flip on a queue that already raised its own release interrupt raised a second event");
|
||||
words[2] = 1u << 24u;
|
||||
check(sceAgcDriverSubmitDcb(&packet) == 0, "release before sustained flips submit failed");
|
||||
@@ -259,6 +264,13 @@ void testEndOfPipeInterrupts() {
|
||||
check(output->ready.load() == static_cast<int>(4 + sustainedFlips + index), "flip paired with a release interrupt was not handed to its output");
|
||||
check(owner->GetTriggeredEvents(events.data(), 2) == 0, "a flip raised an event within two flips of the queue's own release interrupt");
|
||||
}
|
||||
const auto framesBefore = AgcDriver::Graphics::HostImportFramesEnded();
|
||||
const auto readyBefore = output->ready.load();
|
||||
std::thread([] { AgcDriverSubmitFlip_nid_postfix(AgcDriver::FlipInfo{7, -1, 1, 0}); }).join();
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
static_cast<void>(owner->GetTriggeredEvents(events.data(), 2));
|
||||
check(output->ready.load() == readyBefore + 1, "flip from a thread that never submitted queue 0 was not handed to its output");
|
||||
check(AgcDriver::Graphics::HostImportFramesEnded() == framesBefore + 1, "a CPU flip from a thread that never submitted queue 0 did not end a host import frame");
|
||||
AgcDriverUnregisterVideoOutput_nid_postfix(7, output);
|
||||
alignas(8) static volatile std::uint64_t label = 0;
|
||||
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&label);
|
||||
@@ -433,6 +445,31 @@ void testWaitFreeSubmissionBehindHeldOne() {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
void testQueue0SubmissionAfterEarlierWaitFreeWork() {
|
||||
alignas(64) static volatile std::uint32_t first = 0, second = 0;
|
||||
constexpr std::uint32_t writes = 20000;
|
||||
std::vector<std::uint32_t> words;
|
||||
for (std::uint32_t i = 1; i <= writes; ++i) {
|
||||
const auto write = writeData(&first, i);
|
||||
words.insert(words.end(), write.begin(), write.end());
|
||||
}
|
||||
submit(0x20, words);
|
||||
submit(0, commands(writeData(&second, 1)));
|
||||
waitFor(&second, 1, "queue 0's submission never ran");
|
||||
check(first == writes, "queue 0 ran a submission before another queue's earlier wait-free submission finished");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
void testQueue0SubmissionBehindWaitingQueue() {
|
||||
alignas(64) static volatile std::uint32_t label = 0, other = 0, done = 0;
|
||||
submit(0x20, commands(waitEqual(&label, 1)));
|
||||
submit(0x20, commands(writeData(&other, 1)));
|
||||
submit(0, commands(writeData(&label, 1), writeData(&done, 1)));
|
||||
check(waitFor(&done, 1, "queue 0 never ran while another queue waited on it") < std::chrono::milliseconds(500), "queue 0 waited for a wait-free submission queued behind another queue's wait on queue 0");
|
||||
waitFor(&other, 1, "the wait-free submission behind the satisfied wait never ran");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
void testMultiAcbs() {
|
||||
alignas(64) static volatile std::uint32_t value = 0, done = 0, gate = 0;
|
||||
check(sceAgcDriverSubmitMultiAcbs(0x20, nullptr, nullptr, 0) == 0, "empty multi-ACB submit failed");
|
||||
@@ -1030,6 +1067,8 @@ int main(int argc, char** argv) {
|
||||
testMultiAcbs();
|
||||
testWaitFreeSubmissionBehindHeldOne();
|
||||
testWaitFreeSubmissionTheCpuWaitsFor();
|
||||
testQueue0SubmissionAfterEarlierWaitFreeWork();
|
||||
testQueue0SubmissionBehindWaitingQueue();
|
||||
testMultiSubmissions();
|
||||
testShaderHeaderAlignment();
|
||||
testHeaderWithoutProgramAddress();
|
||||
|
||||
@@ -133,6 +133,10 @@ struct Hull {
|
||||
void readRing(IrValue& offset, IrValue& predicate) {
|
||||
auto& loaded = builder.Emit(IrOpcode::LoadBufferU32, IrType::U32, {&ringResource(), &builder.Constant(0u), &offset, &offChip(), &predicate}, MemoryFlags{RingMemory, RingReadPc});
|
||||
static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&loaded}));
|
||||
}
|
||||
|
||||
IrValue& packed() {
|
||||
return builder.BitwiseOr(builder.ShiftLeftLogical(invocation(), builder.Constant(8u)), relativePatch());
|
||||
}
|
||||
|
||||
IrValue& merge(std::uint32_t firstMultiple, std::uint32_t secondMultiple) {
|
||||
@@ -341,6 +345,14 @@ bool LowersUnderComplementedBallot(Conjunction conjunction) {
|
||||
return LoweredAt(hull.program, address);
|
||||
}
|
||||
|
||||
bool OwnControlPoint(const IrValue* address, std::uint32_t stride) {
|
||||
if (address == nullptr || address->Opcode() != IrOpcode::IAdd32 || !address->Argument(1)->Resolve()->HasImmediate() || address->Argument(1)->Resolve()->ImmediateU32() != 0u) return false;
|
||||
const IrValue* scaled = address->Argument(0)->Resolve();
|
||||
if (scaled->Opcode() != IrOpcode::IMul32 || scaled->Argument(1)->Resolve()->ImmediateU32() != stride) return false;
|
||||
const IrValue* invocation = scaled->Argument(0)->Resolve();
|
||||
return invocation->Opcode() == IrOpcode::GetBuiltin && invocation->Argument(0)->Resolve()->ImmediateU32() == static_cast<std::uint32_t>(StageInputKind::InvocationId);
|
||||
}
|
||||
|
||||
bool run(const char* name, const std::function<bool()>& body) {
|
||||
bool passed = false;
|
||||
try {
|
||||
@@ -548,6 +560,29 @@ int main() {
|
||||
|
||||
passed &= run("a hull read of another control point's output at an immediate address past the last control point reads the last control point", [] {
|
||||
return EmittedControlPoint(IrShaderStage::TessellationControl, TessellationAttribute::ControlOutput, InputControlPoints * 16u) == InputControlPoints - 1u;
|
||||
});
|
||||
|
||||
passed &= run("a hull read at its control point masked out of the packed control point and relative patch reads its own input control point", [] {
|
||||
Hull hull;
|
||||
auto& masked = hull.builder.BitwiseAnd(hull.packed(), hull.builder.Constant(0x1f00u));
|
||||
hull.read(hull.inPatch(hull.builder.ShiftRightLogical(masked, hull.builder.Constant(3u))));
|
||||
const Outcome outcome = Lower(hull.program);
|
||||
if (!outcome.lowered) std::fprintf(stderr, "refused: %s\n", outcome.reason.c_str());
|
||||
return outcome.lowered && OwnControlPoint(outcome.emitted, LocalStride);
|
||||
});
|
||||
|
||||
passed &= run("a mask that drops the low bit of the packed control point is refused", [] {
|
||||
Hull hull;
|
||||
auto& masked = hull.builder.BitwiseAnd(hull.packed(), hull.builder.Constant(0x1e00u));
|
||||
hull.read(hull.inPatch(hull.builder.ShiftRightLogical(masked, hull.builder.Constant(3u))));
|
||||
return RefusedBecause(hull.program, "not affine");
|
||||
});
|
||||
|
||||
passed &= run("a mask with a gap below the packed control point is refused", [] {
|
||||
Hull hull;
|
||||
auto& masked = hull.builder.BitwiseAnd(hull.packed(), hull.builder.Constant(0x1f0fu));
|
||||
hull.read(hull.inPatch(hull.builder.ShiftRightLogical(masked, hull.builder.Constant(3u))));
|
||||
return RefusedBecause(hull.program, "not affine");
|
||||
});
|
||||
|
||||
return passed ? 0 : 1;
|
||||
|
||||
@@ -522,6 +522,64 @@ constexpr std::array<DitherCase, 56> TwoSampleDitherCases{{
|
||||
{0x00007e00u, 4u, {0x00u, 0x00u, 0x00u, 0x00u}},
|
||||
}};
|
||||
|
||||
void DccSrgbClearTests(AgcDriver::VulkanDevice& device, std::uint32_t samples) {
|
||||
const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 4);
|
||||
const ColorTargetLayout allocation(Width, Height, ColorTileMode::RenderTarget, 8);
|
||||
std::vector<std::unique_ptr<Block>> blocks;
|
||||
struct ClearTargets {
|
||||
AgcDriver::VulkanDevice& device;
|
||||
~ClearTargets() {
|
||||
std::lock_guard lock(AgcDriver::GuestMemory::GpuMutex());
|
||||
device.SubmitRecorded(true);
|
||||
device.WaitIdle();
|
||||
AgcDriver::Graphics::ClearMultisampleTargets(device.Device());
|
||||
AgcDriver::Graphics::ClearCachedTextures(device.Device());
|
||||
}
|
||||
} clearTargets{device};
|
||||
const auto targetFor = [&](const Block& block, std::uint32_t count, VkFormat format) {
|
||||
auto target = DecodeTarget(block, count);
|
||||
target.format = format;
|
||||
target.elementBytes = 4;
|
||||
target.bytes = layout.Bytes();
|
||||
return target;
|
||||
};
|
||||
for (const auto format : {VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_B8G8R8A8_SRGB}) {
|
||||
for (const auto key : {0x00u, 0x40u, 0x80u, 0xc0u}) {
|
||||
for (const auto alphaOnMsb : {false, true}) {
|
||||
blocks.push_back(std::make_unique<Block>(allocation.Bytes() * samples));
|
||||
auto& source = *blocks.back();
|
||||
std::memset(source.data, 0x5a, source.bytes);
|
||||
blocks.push_back(std::make_unique<Block>(BlockBytes));
|
||||
auto& metadata = *blocks.back();
|
||||
std::memset(metadata.data, key, metadata.bytes);
|
||||
auto target = targetFor(source, samples, format);
|
||||
target.dccAddress = metadata.Address();
|
||||
target.dccPipeAligned = true;
|
||||
target.dccAlphaOnMsb = alphaOnMsb;
|
||||
Draw(device, target, false, Float32Export, Program(Color{0.25f, 0.5f, 0.75f, 0.125f}, Float32Export, 0u), false, true);
|
||||
blocks.push_back(std::make_unique<Block>(allocation.Bytes()));
|
||||
auto& resolved = *blocks.back();
|
||||
std::memset(resolved.data, 0xa5, resolved.bytes);
|
||||
AgcDriver::Graphics::ColorMetadataPass pass{};
|
||||
pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve;
|
||||
pass.source = target;
|
||||
pass.targets = {targetFor(resolved, 1u, format)};
|
||||
device.ColorMetadataPass(pass);
|
||||
const auto stored = ReadBack(device, resolved);
|
||||
const auto bytes = std::as_bytes(std::span(stored));
|
||||
std::array<std::byte, 4> expected{};
|
||||
const auto keys = key == 0x00u ? AgcDriver::Graphics::DccKeys::Clear0000 : key == 0x40u ? AgcDriver::Graphics::DccKeys::Clear0001 : key == 0x80u ? AgcDriver::Graphics::DccKeys::Clear1110 : AgcDriver::Graphics::DccKeys::Clear1111;
|
||||
Require(AgcDriver::Graphics::FillDccClear(format, keys, alphaOnMsb, expected), "the sRGB DCC expected texel was not encoded");
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
for (std::uint32_t channel = 0; channel < 4; ++channel) Require(bytes[layout.Offset(x, y) + channel] == expected[channel], "an sRGB DCC import or resolve changed an undrawn sample endpoint");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) {
|
||||
const ColorTargetLayout layout(Width, Height, ColorTileMode::RenderTarget, 8);
|
||||
const auto expectRange = [](std::string_view message, const char* plane, std::uint64_t address, std::size_t bytes, std::uint32_t samples) {
|
||||
@@ -1147,6 +1205,19 @@ int main(int argc, char** argv) {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (argc == 3 && std::string_view(argv[1]) == "--dcc-srgb-clear") {
|
||||
const auto samples = std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u;
|
||||
Require(samples != 0, "unknown sRGB DCC test sample count");
|
||||
try {
|
||||
DccSrgbClearTests(*device, samples);
|
||||
} catch (const std::runtime_error& error) {
|
||||
if (std::string_view(error.what()) != "AGC graphics: the device cannot render color with the target's sample count") throw;
|
||||
std::printf("skipped, the device lacks the requested multisample color attachments: %s\n", error.what());
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
std::cout << "Multisampled sRGB DCC clear tests passed\n";
|
||||
return 0;
|
||||
}
|
||||
if ((argc == 2 || argc == 3) && std::string_view(argv[1]) == "--dcc-fast-clear") {
|
||||
const auto samples = argc == 2 ? 0u : std::string_view(argv[2]) == "2" ? 2u : std::string_view(argv[2]) == "4" ? 4u : std::string_view(argv[2]) == "8" ? 8u : 0u;
|
||||
Require(argc == 2 || samples != 0, "unknown DCC test sample count");
|
||||
|
||||
Loaded 100 of 675 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user