chore: refresh native buffer alias guard after depth write fix

This commit is contained in:
Romans Pokrovskis committed 2026-10-28 16:11:13 +02:00
commit b2aad7239c
675 files changed
+27767 -3631

No files matched your search

+8
View File
@@ -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.
+111
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+14 -6
View File
@@ -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;
+17 -15
View File
@@ -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});
+64
View File
@@ -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()
+39 -10
View File
@@ -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) {
+71
View File
@@ -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;
}
}
+113 -6
View File
@@ -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(&registered.buffer, 1); });
expectFailure([&] { sceAgcAcbSetWorkloadsActive(&registered.buffer, 1, ids, 2); });
expectFailure([&] { sceAgcAcbSetWorkloadComplete(&registered.buffer, 1, 3); });
expectFailure([&] { sceAgcAcbSetWorkloadStreamInactive(&registered.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(&registered.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(&registered.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(&registered.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(&registered.buffer, 1, duplicate, 2) == nullptr, "ACB duplicate workload ids accepted");
check(sceAgcAcbSetWorkloadsActive(&registered.buffer, 1, outOfRange, 1) == nullptr, "ACB workload id 64 accepted");
check(sceAgcAcbSetWorkloadsActive(&registered.buffer, 1, top, 1) != nullptr, "ACB workload id 63 rejected");
check(sceAgcAcbSetWorkloadsActive(&registered.buffer, 1, ids, 0) == nullptr, "ACB workload count 0 accepted");
check(sceAgcAcbSetWorkloadsActive(&registered.buffer, 1, nullptr, 2) == nullptr, "ACB null workload ids accepted");
check(sceAgcAcbSetWorkloadsActive(&registered.buffer, 0, ids, 2) == nullptr, "ACB stream id 0 accepted");
check(sceAgcAcbSetWorkloadsActive(&registered.buffer, 32, ids, 2) == nullptr, "ACB stream id 32 accepted");
check(sceAgcAcbSetWorkloadComplete(&registered.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(&registered.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;
}
}
+22
View File
@@ -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;
}
}
+84 -5
View File
@@ -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, &region) != 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, &region, 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, &region, 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");
+40 -1
View File
@@ -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