Merge branch 'main' into #7417

Resolve docs/dev/debt/libc.md: keep main's _Throw_Cpp_error entry and this branch's call tracing entry.
This commit is contained in:
Dean Galvin committed 2026-10-21 06:33:59 +13:00
commit 143ed02750
723 files changed
+32965 -2820

No files matched your search

+3 -13
View File
@@ -9,6 +9,7 @@ on:
- core/relinker/**
- demo/web/**
- tools/build_web.py
- tools/package_release.py
- tools/nid_names.py
- tools/progress.py
- tools/progress_history.py
@@ -42,21 +43,10 @@ jobs:
with:
ref: main
fetch-depth: 0
- name: Build web converter
shell: bash
run: |
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk"
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.3
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm --target relinker_web --parallel 2
- name: Test web converter
run: python3 tools/test_web.py --build build-wasm
- name: Download latest release libraries
- name: Download matching release converter and libraries
env:
GITHUB_TOKEN: ${{ github.token }}
run: python3 tools/build_web.py --build build-wasm --libraries
run: python3 tools/build_web.py --build build-wasm --release
- run: python3 tools/progress.py build-wasm/web
- run: python3 tools/progress_history.py build-wasm/web
- uses: actions/upload-pages-artifact@v5
+1 -1
View File
@@ -16,7 +16,7 @@ concurrency:
jobs:
labels:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 30
permissions:
contents: read
pull-requests: write
+35 -3
View File
@@ -41,6 +41,7 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
submodules: recursive
- name: Point the WinLibs CMake at the CA bundle of Git for Windows
if: matrix.platform == 'windows'
@@ -86,7 +87,7 @@ jobs:
run: cmake --build build
- name: Run tests (Linux)
if: matrix.platform == 'linux'
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error -E '^precise_wait$'
env:
ANYPS5_REQUIRE_VULKAN: 1
GALLIUM_OVERRIDE_CPU_CAPS: avx
@@ -110,9 +111,38 @@ jobs:
if-no-files-found: error
compression-level: 0
web:
name: Build and test web converter
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
- name: Build web converter
shell: bash
run: |
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk"
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.3
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm --target relinker_web --parallel 2
- name: Test web converter
run: python3 tools/test_web.py --build build-wasm
- name: Package web converter
shell: bash
run: python3 tools/package_release.py --platform web --build build-wasm --output dist --version "$RELEASE_TAG" --revision "$(git rev-parse HEAD)"
- uses: actions/upload-artifact@v7
with:
name: release-web
path: dist/*
if-no-files-found: error
compression-level: 0
publish:
name: Publish release
needs: build
needs: [build, web]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
@@ -122,6 +152,8 @@ jobs:
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
- uses: actions/download-artifact@v8
with:
pattern: release-*
@@ -130,7 +162,7 @@ jobs:
- name: Check release files and collect documentation
shell: bash
run: |
for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG"; do
for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG" "web-$RELEASE_TAG.zip"; do
test -s "dist/$file"
done
python tools/package_release.py --docs docs/user --output dist
+26 -1
View File
@@ -44,6 +44,9 @@ if(BUILD_TESTING)
get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY)
foreach(runtimeName IN ITEMS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll)
if(NOT EXISTS "${testCompilerDirectory}/${runtimeName}")
if(ANYPS5_RELINKER_ONLY)
continue()
endif()
message(FATAL_ERROR "Cannot locate MinGW runtime: ${runtimeName}")
endif()
file(COPY "${testCompilerDirectory}/${runtimeName}" DESTINATION "${ANYPS5_TEST_OUTPUT_DIRECTORY}")
@@ -120,6 +123,10 @@ if(BUILD_TESTING)
core/shader/recompiler/IntermediateRepresentation/include)
add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests)
set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20)
add_test_executable(pad_command_file_tests core/libs/prx/libScePad/tests/InputCommandFile.cpp)
target_include_directories(pad_command_file_tests PRIVATE core/libs)
add_test(NAME pad_command_file COMMAND pad_command_file_tests)
set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20)
if(Python3_Interpreter_FOUND)
add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py
@@ -167,6 +174,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py)
add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py)
add_test(NAME conventions_git_errors COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_git.py)
add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py)
add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE}
@@ -175,6 +184,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_debt_length.py)
add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py)
add_test(NAME test_script_registration COMMAND ${Python3_EXECUTABLE}
${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 COMMAND ${Python3_EXECUTABLE}
@@ -184,7 +195,13 @@ if(BUILD_TESTING)
add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py)
add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedTests)
add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedPathTests)
add_test(NAME configure_seed_round_trips COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedRoundTripTests)
add_test(NAME configure_seed_parsing COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ChecksParsingTests)
add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
@@ -202,16 +219,24 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py)
add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE}
${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 nid_database_cache COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py)
add_test(NAME web_release COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_web_release.py)
set_tests_properties(web_release PROPERTIES TIMEOUT 20)
add_test(NAME web_library_assets COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_web.py)
set_tests_properties(web_library_assets PROPERTIES TIMEOUT 20)
add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py)
set_tests_properties(pad_input PROPERTIES TIMEOUT 20)
add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py)
set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20)
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE}
@@ -35,6 +35,10 @@ std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg);
std::optional<Image> Decode(std::span<const std::uint8_t> jpeg);
std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator);
std::optional<Image> DecodeScaled(std::span<const std::uint8_t> jpeg, std::uint32_t denominator);
} // namespace Decoder::Jpeg
#endif // DECODER_JPEG_HPP
+44 -6
View File
@@ -2,6 +2,7 @@
#include <climits>
#include <cstddef>
#include <memory>
#include <stdexcept>
#define STB_IMAGE_STATIC
@@ -19,8 +20,8 @@ constexpr std::uint32_t MAX_DIMENSION = 0xFFFF;
class Handle {
public:
Handle() : handle(tj3Init(TJINIT_COMPRESS)) {
if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed");
explicit Handle(int type) : handle(tj3Init(type)) {
if (!handle) throw std::runtime_error(type == TJINIT_COMPRESS ? "Jpeg::Encode: encoder initialization failed" : "Jpeg::DecodeScaled: decoder initialization failed");
}
~Handle() { tj3Destroy(handle); }
Handle(const Handle&) = delete;
@@ -63,7 +64,7 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
throw std::invalid_argument("Jpeg::Encode: invalid restart interval");
}
const int subsampling = toSubsampling(sampling);
Handle handle;
Handle handle(TJINIT_COMPRESS);
if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) {
throw std::runtime_error("Jpeg::Encode: encoder configuration failed");
}
@@ -80,6 +81,43 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
return {buffer.data, buffer.data + size};
}
std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator) {
if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) {
throw std::invalid_argument("Jpeg::ScaledDimension: the scale must be 1/1, 1/2, 1/4 or 1/8");
}
const tjscalingfactor factor{1, static_cast<int>(denominator)};
return static_cast<std::uint32_t>(TJSCALED(static_cast<std::uint64_t>(dimension), factor));
}
std::optional<Image> DecodeScaled(std::span<const std::uint8_t> jpeg, std::uint32_t denominator) {
if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) {
throw std::invalid_argument("Jpeg::DecodeScaled: the scale must be 1/1, 1/2, 1/4 or 1/8");
}
if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt;
Handle handle(TJINIT_DECOMPRESS);
if (tj3Set(handle, TJPARAM_STOPONWARNING, 1) < 0) throw std::runtime_error("Jpeg::DecodeScaled: decoder configuration failed");
if (tj3DecompressHeader(handle, jpeg.data(), jpeg.size()) < 0) return std::nullopt;
const int colorspace = tj3Get(handle, TJPARAM_COLORSPACE);
if (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) {
throw std::runtime_error("Jpeg::DecodeScaled: scaled decoding of a CMYK or YCCK image is not implemented");
}
const int width = tj3Get(handle, TJPARAM_JPEGWIDTH);
const int height = tj3Get(handle, TJPARAM_JPEGHEIGHT);
if (width <= 0 || height <= 0) return std::nullopt;
if (tj3SetScalingFactor(handle, tjscalingfactor{1, static_cast<int>(denominator)}) < 0) {
throw std::runtime_error("Jpeg::DecodeScaled: the decoder refused the scaling factor");
}
const std::uint32_t channels = colorspace == TJCS_GRAY ? 1 : 3;
Image image{ScaledDimension(static_cast<std::uint32_t>(width), denominator), ScaledDimension(static_cast<std::uint32_t>(height), denominator), channels, {}};
image.pixels.resize(static_cast<std::size_t>(image.width) * image.height * channels);
if (tj3Decompress8(handle, jpeg.data(), jpeg.size(), image.pixels.data(), 0, channels == 1 ? TJPF_GRAY : TJPF_RGB) < 0) {
return std::nullopt;
}
return image;
}
std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) {
if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt;
@@ -97,12 +135,12 @@ std::optional<Image> Decode(std::span<const std::uint8_t> jpeg) {
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* decoded = stbi_load_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels, 0);
const std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> decoded(
stbi_load_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels, 0), stbi_image_free);
if (!decoded) return std::nullopt;
Image image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels), {}};
image.pixels.assign(decoded, decoded + static_cast<std::size_t>(image.width) * image.height * image.channels);
stbi_image_free(decoded);
image.pixels.assign(decoded.get(), decoded.get() + static_cast<std::size_t>(image.width) * image.height * image.channels);
return image;
}
+68
View File
@@ -1,5 +1,7 @@
#include "Decoder/Jpeg.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
@@ -108,5 +110,71 @@ int main() {
const std::vector<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8};
Require(!Decoder::Jpeg::Decode(garbage).has_value());
Require(Decoder::Jpeg::ScaledDimension(7, 1) == 7 && Decoder::Jpeg::ScaledDimension(7, 2) == 4);
Require(Decoder::Jpeg::ScaledDimension(7, 4) == 2 && Decoder::Jpeg::ScaledDimension(7, 8) == 1);
Require(Decoder::Jpeg::ScaledDimension(32, 8) == 4 && Decoder::Jpeg::ScaledDimension(33, 8) == 5 && Decoder::Jpeg::ScaledDimension(1, 8) == 1);
Require(Decoder::Jpeg::ScaledDimension(0xFFFF, 2) == 0x8000);
for (const std::uint32_t denominator : {0u, 3u, 16u}) {
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::ScaledDimension(7, denominator); }));
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); }));
}
const auto boxError = [](const std::vector<std::uint8_t>& source, std::uint32_t sourceWidth, std::uint32_t sourceHeight,
std::uint32_t channels, const Decoder::Jpeg::Image& scaled, std::uint32_t denominator) {
long error = 0;
for (std::uint32_t y = 0; y < scaled.height; ++y) {
for (std::uint32_t x = 0; x < scaled.width; ++x) {
for (std::uint32_t c = 0; c < channels; ++c) {
long sum = 0;
long count = 0;
for (std::uint32_t sy = y * denominator; sy < std::min(sourceHeight, (y + 1) * denominator); ++sy) {
for (std::uint32_t sx = x * denominator; sx < std::min(sourceWidth, (x + 1) * denominator); ++sx) {
sum += source[(static_cast<std::size_t>(sy) * sourceWidth + sx) * channels + c];
++count;
}
}
const auto average = static_cast<std::uint8_t>((sum + count / 2) / count);
error += Difference(average, scaled.pixels[(static_cast<std::size_t>(y) * scaled.width + x) * channels + c]);
}
}
}
return error / static_cast<long>(static_cast<std::size_t>(scaled.width) * scaled.height * channels);
};
for (const std::uint32_t denominator : {1u, 2u, 4u, 8u}) {
const auto scaled = Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator);
Require(scaled.has_value() && scaled->channels == 3);
Require(scaled->width == width / denominator && scaled->height == height / denominator);
Require(scaled->pixels.size() == static_cast<std::size_t>(scaled->width) * scaled->height * 3);
Require(boxError(rgb, width, height, 3, *scaled, denominator) < 4);
const auto grayScaled = Decoder::Jpeg::DecodeScaled(grayJpeg, denominator);
Require(grayScaled.has_value() && grayScaled->channels == 1);
Require(grayScaled->width == width / denominator && grayScaled->height == height / denominator);
Require(boxError(gray, width, height, 1, *grayScaled, denominator) < 4);
}
std::vector<std::uint8_t> odd(31 * 23 * 3);
for (std::uint32_t y = 0; y < 23; ++y) {
for (std::uint32_t x = 0; x < 31; ++x) {
std::uint8_t* pixel = &odd[(y * 31 + x) * 3];
pixel[0] = static_cast<std::uint8_t>(x * 8);
pixel[1] = static_cast<std::uint8_t>(y * 11);
pixel[2] = static_cast<std::uint8_t>(200 - x * 2);
}
}
const auto oddJpeg = Decoder::Jpeg::Encode(odd, 31, 23, 3, 95, Decoder::Jpeg::Sampling::Yuv420);
for (const std::uint32_t denominator : {2u, 4u, 8u}) {
const auto scaled = Decoder::Jpeg::DecodeScaled(oddJpeg, denominator);
Require(scaled.has_value() && scaled->channels == 3);
Require(scaled->width == Decoder::Jpeg::ScaledDimension(31, denominator) && scaled->height == Decoder::Jpeg::ScaledDimension(23, denominator));
Require(boxError(odd, 31, 23, 3, *scaled, denominator) < 8);
}
Require(!Decoder::Jpeg::DecodeScaled({}, 2).has_value());
Require(!Decoder::Jpeg::DecodeScaled(garbage, 2).has_value());
const std::vector<std::uint8_t> truncated(rgbJpeg.begin(), rgbJpeg.begin() + static_cast<std::ptrdiff_t>(rgbJpeg.size() / 2));
Require(!Decoder::Jpeg::DecodeScaled(truncated, 2).has_value());
return 0;
}
+3 -3
View File
@@ -152,12 +152,12 @@ std::optional<Image> Decode(std::span<const std::uint8_t> png) {
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* decoded = stbi_load_from_memory(png.data(), static_cast<int>(png.size()), &width, &height, &channels, 4);
const std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> decoded(
stbi_load_from_memory(png.data(), static_cast<int>(png.size()), &width, &height, &channels, 4), stbi_image_free);
if (!decoded) return std::nullopt;
Image image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), {}};
image.pixels.assign(decoded, decoded + static_cast<std::size_t>(image.width) * image.height * 4);
stbi_image_free(decoded);
image.pixels.assign(decoded.get(), decoded.get() + static_cast<std::size_t>(image.width) * image.height * 4);
return image;
}
+36
View File
@@ -82,6 +82,9 @@ add_library(libc SHARED EXCLUDE_FROM_ALL)
set(LIBS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
add_subdirectory(${LIBC_DIR} ${CMAKE_CURRENT_BINARY_DIR}/prx/libc)
target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(WIN32)
target_compile_options(libc PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp")
endif()
configure_windows_unwind(libc)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(libc PRIVATE LINKER:-z,now)
@@ -139,6 +142,9 @@ foreach(LIB_DIR ${LIBS_DIRS})
configure_windows_unwind(${LIB_NAME})
endif()
if(WIN32)
target_compile_options(${LIB_NAME} PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp")
endif()
if(LIB_NAME STREQUAL "libkernel")
target_link_libraries(${LIB_NAME} PUBLIC libc)
else()
@@ -354,6 +360,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceVoice>")
endif()
add_test_executable(guest_move_tests tests/GuestMove.cpp)
target_include_directories(guest_move_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_move_tests PRIVATE libSceMove libc)
configure_windows_unwind(guest_move_tests)
add_test(NAME guest_move COMMAND guest_move_tests)
if(WIN32)
set_tests_properties(guest_move PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceMove>")
endif()
add_test_executable(guest_offline_imports_tests tests/GuestOfflineImports.cpp)
target_include_directories(guest_offline_imports_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_offline_imports_tests PRIVATE libSceHttp libSceNpEntitlementAccess libSceRudp libc)
@@ -398,6 +414,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceGameLiveStreaming>")
endif()
add_test_executable(guest_netctl_ap_tests tests/GuestNetCtlAp.cpp)
target_include_directories(guest_netctl_ap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_netctl_ap_tests PRIVATE libSceNetCtlAp libc)
configure_windows_unwind(guest_netctl_ap_tests)
add_test(NAME guest_netctl_ap COMMAND guest_netctl_ap_tests)
if(WIN32)
set_tests_properties(guest_netctl_ap PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNetCtlAp>")
endif()
add_test_executable(guest_netctl_ap_dialog_tests tests/GuestNetCtlApDialog.cpp)
target_include_directories(guest_netctl_ap_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_netctl_ap_dialog_tests PRIVATE libSceNetCtlApDialog libc)
@@ -458,6 +484,16 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
endif()
add_test_executable(guest_rudp_poll_tests tests/GuestRudpPoll.cpp)
target_include_directories(guest_rudp_poll_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_rudp_poll_tests PRIVATE libSceRudp libc)
configure_windows_unwind(guest_rudp_poll_tests)
add_test(NAME guest_rudp_poll COMMAND guest_rudp_poll_tests)
if(WIN32)
set_tests_properties(guest_rudp_poll PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceRudp>")
endif()
add_test_executable(guest_memory_pool_stats_tests tests/GuestMemoryPoolStats.cpp)
target_include_directories(guest_memory_pool_stats_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_memory_pool_stats_tests PRIVATE libSceSsl libSceHttp2 libSceNet libkernel libc)
+9 -2
View File
@@ -1155,8 +1155,8 @@ struct ImeKeycode {
std::uint64_t timestamp;
};
using ImeTextFilter = std::int32_t (*)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
using ImeExtKeyboardFilter = int (*)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
using ImeTextFilter = std::int32_t (APS5_VABI *)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
using ImeExtKeyboardFilter = int (APS5_VABI *)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
struct ImeExtendedParam {
std::uint32_t option;
@@ -1941,6 +1941,13 @@ struct SystemGestureTouchRecognizerInformation {
struct SystemGestureTouchEvent { std::uint8_t reserve[168]; };
struct SystemGestureTouchPadData {
std::int32_t pad_handle;
std::int32_t report_number;
const PadData* pad_data_buffer;
std::uint8_t reserve[8];
};
struct UserServiceLoginUserIdList { int user_id[4]; };
struct UserServiceRegisteredUserIdList { int user_id[16]; };
+57 -17
View File
@@ -1,54 +1,84 @@
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <atomic>
#include <mutex>
#include <unordered_set>
#include <unordered_map>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
// Audio co-processor batches complete immediately without producing output; audio is silent.
static constexpr int SCE_ACM_ERROR_INVALID_PARAMETER = static_cast<int>(0x80E30002);
static std::atomic<uint32_t> g_nextId{1};
namespace {
constexpr int SCE_ACM_ERROR_BAD_CONTEXT = static_cast<int>(0x81940002);
constexpr int SCE_ACM_ERROR_BAD_POINTER = static_cast<int>(0x81940006);
std::mutex g_mutex;
std::unordered_set<AcmContextId> g_contexts;
std::atomic<uint32_t> g_nextId{1};
}
extern "C" {
int APS5_VABI sceAcmBatchStartBuffer(AcmContextId context, const void* batch_commands, size_t batch_size, AcmBatchError* batch_error, AcmBatchId* batch) {
(void)context;
(void)batch_commands;
(void)batch_size;
(void)batch_error;
if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER;
*batch = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!batch) return SCE_ACM_ERROR_BAD_POINTER;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error));
*batch = id;
return 0;
}
int APS5_VABI sceAcmBatchStartBuffers(AcmContextId context, uint32_t batch_info_count, const AcmBatchInfo* const batch_info[], AcmBatchError* batch_error, AcmBatchId* batch) {
(void)context;
(void)batch_info_count;
(void)batch_info;
(void)batch_error;
if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER;
*batch = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!batch) return SCE_ACM_ERROR_BAD_POINTER;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error));
*batch = id;
return 0;
}
int APS5_VABI sceAcmBatchWait(AcmContextId context, AcmBatchId batch, uint32_t timeout) {
(void)context;
(void)batch;
(void)timeout;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
return 0;
}
int APS5_VABI sceAcmContextCreate(AcmContextId* context) {
if (!context) return SCE_ACM_ERROR_INVALID_PARAMETER;
*context = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!context) return SCE_ACM_ERROR_BAD_POINTER;
AcmContextId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(g_mutex);
g_contexts.insert(id);
}
*context = id;
return 0;
}
int APS5_VABI sceAcmContextDestroy(AcmContextId context) {
(void)context;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
auto it = g_contexts.find(context);
if (it == g_contexts.end()) return SCE_ACM_ERROR_BAD_CONTEXT;
g_contexts.erase(it);
return 0;
}
// Builds a convolution-reverb command into a batch; batches execute as no-ops, so nothing is encoded.
int APS5_VABI sceAcm_ConvReverb_SharedInput(void) {
return 0;
}
@@ -66,4 +96,14 @@ int APS5_VABI sceAcmBatchJobPriority(void) {
return 0;
}
int APS5_VABI sceAcm_ConvReverb_SharedIr() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAcm_IFFT() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
+1
View File
@@ -428,6 +428,7 @@ configure_windows_unwind(agc_shader_helpers_tests)
if(BUILD_TESTING)
set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests)
set_tests_properties(agc_shader_helpers PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
@@ -1,7 +1,9 @@
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
@@ -96,8 +98,8 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s
Require(count != 0 && count <= 0x3fffu && offset <= 0xffffu && count <= 0x10000u - offset, function, "invalid register range");
std::vector<std::uint32_t> snapshot;
if (values != nullptr) {
CheckAddress(reinterpret_cast<std::uintptr_t>(values), 4, function);
snapshot.assign(values, values + count);
snapshot.resize(count);
std::memcpy(snapshot.data(), values, static_cast<std::size_t>(count) * sizeof(std::uint32_t));
}
auto* packet = Allocate(buffer, count + 2u, function);
packet[0] = Header(opcode, count + 2u);
+8 -2
View File
@@ -474,8 +474,14 @@ void testRegisterRange() {
check(packet == storage.words.data() + 6 && storage.buffer.cursor_up == storage.words.data() + 12, "incorrect populated register range allocation");
check(storage.words == expected, "register values were not copied correctly");
const auto* misaligned = reinterpret_cast<const std::uint32_t*>(reinterpret_cast<const unsigned char*>(values.data()) + 1);
expectFailure([&] { sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 4); });
check(storage.words == expected && storage.buffer.cursor_up == storage.words.data() + 12, "misaligned register values modified command buffer");
packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 3);
expected[12] = 0xc0037600u;
expected[13] = 0x8c;
expected[14] = 0x02000000u;
expected[15] = 0x03000000u;
expected[16] = 0x04000000u;
check(packet == storage.words.data() + 12 && storage.buffer.cursor_up == storage.words.data() + 17, "incorrect misaligned register range allocation");
check(storage.words == expected, "misaligned register values were not read byte for byte");
}
void testPacketPayloadAddress() {
@@ -8,6 +8,8 @@
extern "C" {
std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer*, ShaderRegister);
std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t);
std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t);
std::uint32_t* APS5_VABI sceAgcCbSetUcRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t);
std::uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer*, ShaderRegister);
std::uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer*, ShaderRegister);
std::uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer*, ShaderRegister);
@@ -92,11 +94,37 @@ void testConfig() {
expectFailure([&] { sceAgcDcbSetCfRegisterDirect(nullptr, {0, 1}); });
expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), 0); });
expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0xffffu, values.data(), 2); });
const auto* misaligned = reinterpret_cast<const std::uint32_t*>(reinterpret_cast<const unsigned char*>(values.data()) + 1);
expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, misaligned, 1); });
check(storage.words == before && storage.buffer.cursor_up == reserved + 4, "invalid config register write modified buffer");
}
void testRangeFromAnyByte() {
const std::array writers{sceAgcCbSetShRegisterRangeDirect, sceAgcCbSetUcRegisterRangeDirect, sceAgcDcbSetCfRegisterRangeDirect};
const std::array headers{0xc0047600u, 0xc0047900u, 0xc0046800u};
alignas(16) std::array<unsigned char, 32> bytes{};
for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast<unsigned char>(0x10u + i);
struct Case {
std::size_t offset;
std::array<std::uint32_t, 4> values;
};
const std::array cases{
Case{1, {0x14131211u, 0x18171615u, 0x1c1b1a19u, 0x201f1e1du}},
Case{2, {0x15141312u, 0x19181716u, 0x1d1c1b1au, 0x21201f1eu}},
Case{3, {0x16151413u, 0x1a191817u, 0x1e1d1c1bu, 0x2221201fu}},
Case{13, {0x201f1e1du, 0x24232221u, 0x28272625u, 0x2c2b2a29u}},
};
for (std::size_t i = 0; i < writers.size(); ++i) {
for (const auto& entry : cases) {
Storage storage;
storage.words.fill(0xabcdef01u);
const auto* values = reinterpret_cast<const std::uint32_t*>(bytes.data() + entry.offset);
auto* packet = writers[i](&storage.buffer, 0x40u, values, 4);
check(packet == storage.words.data() && storage.buffer.cursor_up == packet + 6, "unaligned register range allocation mismatch");
check(packet[0] == headers[i] && packet[1] == 0x40u && std::equal(entry.values.begin(), entry.values.end(), packet + 2), "unaligned register values were not read byte for byte");
check(packet[6] == 0xabcdef01u, "unaligned register range overwrote the following word");
}
}
}
void testIndirect() {
const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect};
const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize};
@@ -210,6 +238,7 @@ int main() {
try {
testDirect();
testConfig();
testRangeFromAnyByte();
testIndirect();
testIndirectPlaceholder();
testDirectList();
+92 -7
View File
@@ -266,6 +266,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Screenshot.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp
@@ -358,6 +359,7 @@ add_shader_recompiler(shader_recompiler_agc_driver libc
target_link_libraries(libSceAgcDriver PRIVATE
shader_recompiler_agc_driver
decoder_png
SDL2-static
libc
libkernel
@@ -367,6 +369,7 @@ target_link_libraries(libSceAgcDriver PRIVATE
if(APS5_ENABLE_TRACY)
target_link_libraries(libSceAgcDriver PUBLIC TracyClient)
endif()
target_link_options(libSceAgcDriver PRIVATE LINKER:--exclude-libs,$<TARGET_FILE_NAME:decoder_png>)
if(BUILD_TESTING)
add_test_executable(agc_driver_draw_program_trace_tests
@@ -645,7 +648,9 @@ if(BUILD_TESTING)
if(WIN32)
add_test(NAME agc_write_tracking_metadata_commit COMMAND agc_write_tracking_tests --metadata-commit)
set_tests_properties(agc_write_tracking_metadata_commit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit PROPERTIES ENVIRONMENT_MODIFICATION
add_test(NAME agc_resident_direct_remap COMMAND agc_write_tracking_tests --resident-direct-remap)
set_tests_properties(agc_resident_direct_remap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20)
set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@@ -804,6 +809,19 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_spirv_failure_text_tests EXCLUDE_FROM_ALL tests/SpirvFailureText.cpp)
target_link_libraries(agc_spirv_failure_text_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_spirv_failure_text_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_spirv_failure_text_tests)
if(BUILD_TESTING)
set_target_properties(agc_spirv_failure_text_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_spirv_failure_text COMMAND agc_spirv_failure_text_tests)
set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
if(WIN32)
set_tests_properties(agc_spirv_failure_text PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_spirv_lds_atomic_lock_tests EXCLUDE_FROM_ALL tests/SpirvLdsAtomicLock.cpp)
target_link_libraries(agc_spirv_lds_atomic_lock_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_spirv_lds_atomic_lock_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -817,6 +835,19 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_spirv_subvector_loop_begin_tests EXCLUDE_FROM_ALL tests/SpirvSubvectorLoopBegin.cpp)
target_link_libraries(agc_spirv_subvector_loop_begin_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_spirv_subvector_loop_begin_tests)
if(BUILD_TESTING)
set_target_properties(agc_spirv_subvector_loop_begin_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_spirv_subvector_loop_begin COMMAND agc_spirv_subvector_loop_begin_tests)
set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
if(WIN32)
set_tests_properties(agc_spirv_subvector_loop_begin PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_layer_input_tests EXCLUDE_FROM_ALL tests/LayerInput.cpp)
target_link_libraries(agc_layer_input_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_layer_input_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -1106,6 +1137,12 @@ target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_reco
set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_formatted_buffer_byte_stores_tests)
add_test_executable(agc_mesh_output_budget_tests EXCLUDE_FROM_ALL tests/MeshOutputBudget.cpp)
target_include_directories(agc_mesh_output_budget_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_mesh_output_budget_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_mesh_output_budget_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_mesh_output_budget_tests)
add_test_executable(agc_shader_disk_cache_tests EXCLUDE_FROM_ALL tests/ShaderDiskCache.cpp)
target_include_directories(agc_shader_disk_cache_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_driver libc)
@@ -1118,6 +1155,12 @@ target_link_libraries(agc_placeholder_images_tests PRIVATE shader_recompiler_agc
set_target_properties(agc_placeholder_images_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_placeholder_images_tests)
add_test_executable(agc_screenshot_tests EXCLUDE_FROM_ALL tests/Screenshot.cpp Execution/src/Screenshot.cpp)
target_include_directories(agc_screenshot_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_screenshot_tests PRIVATE decoder_png SDL2-static libc)
set_target_properties(agc_screenshot_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_screenshot_tests)
add_test_executable(agc_shader_device_profile_tests EXCLUDE_FROM_ALL tests/ShaderDeviceProfile.cpp Execution/src/ShaderDeviceProfile.cpp)
target_include_directories(agc_shader_device_profile_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_shader_device_profile_tests PRIVATE shader_recompiler_agc_driver libc)
@@ -1400,6 +1443,12 @@ if(BUILD_TESTING)
endif()
add_test(NAME agc_recorder_read_list COMMAND agc_driver_recorder_tests --read-list-only)
set_tests_properties(agc_recorder_read_list PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_recorder_deferred_failure_report COMMAND agc_driver_recorder_tests --deferred-failure-report-only)
set_tests_properties(agc_recorder_deferred_failure_report PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_recorder_deferred_failure_report PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_space_pending_writes COMMAND agc_driver_recorder_tests --space-pending-writes-only)
set_tests_properties(agc_space_pending_writes PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
@@ -1836,10 +1885,11 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_undithered_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --undithered-alpha-to-coverage-state)
add_test(NAME agc_driver_depth_bounds_state COMMAND agc_driver_graphics_tests --depth-bounds-state)
add_test(NAME agc_driver_msaa_dcc_state COMMAND agc_driver_graphics_tests --multisample-dcc-state)
add_test(NAME agc_driver_unused_viewport_depth COMMAND agc_driver_graphics_tests --unused-viewport-depth)
set_tests_properties(agc_driver_unused_viewport_depth PROPERTIES TIMEOUT 30)
set_tests_properties(agc_driver_undithered_alpha_to_coverage_state PROPERTIES
set_tests_properties(agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_alpha_to_coverage_state COMMAND agc_driver_graphics_tests --alpha-to-coverage-state)
set_tests_properties(agc_driver_alpha_to_coverage_state PROPERTIES
@@ -1857,7 +1907,7 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_dithered_alpha_to_coverage PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_alpha_to_coverage_state agc_driver_undithered_alpha_to_coverage_state agc_driver_depth_bounds_state agc_driver_msaa_dcc_state agc_driver_unused_viewport_depth PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_dithered_alpha_to_coverage ${multisampledDccClearTests} PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
@@ -1937,6 +1987,7 @@ target_include_directories(agc_driver_buffer_descriptor_runtime_tests PRIVATE $<
target_include_directories(agc_driver_shutdown_threads_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
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_prepared_publication_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
@@ -1976,6 +2027,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_readonly_resummarize agc_driver_eop_interrupts PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_small_copy_sync COMMAND agc_driver_tests --small-copy-sync)
set_tests_properties(agc_driver_small_copy_sync PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_COPY_SYNC=1" SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_small_copy_sync PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests)
set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 20)
if(WIN32)
@@ -2019,7 +2076,7 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests)
foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_screenshot_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests)
if(agcTest STREQUAL "agc_driver_constant_store_tests" OR agcTest STREQUAL "agc_driver_flat_apertures_tests" OR agcTest STREQUAL "agc_driver_pipeline_optimized_readback_tests")
continue()
endif()
@@ -2053,6 +2110,7 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_draw_pipeline_trace_disabled PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_queued_raw_shader PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8" TIMEOUT 30)
add_test(NAME agc_driver_partially_resident_pending_stores COMMAND agc_driver_partially_resident_snapshot_tests --pending-stores)
set_tests_properties(agc_driver_multisample_readback PROPERTIES TIMEOUT 20)
foreach(agcReadbackCase IN ITEMS uint8 uint16 uint64 uint128 depth)
@@ -2071,6 +2129,17 @@ if(BUILD_TESTING)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240)
if(WIN32)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_shader_disk_cache_long_paths COMMAND agc_shader_disk_cache_tests --long-host-paths)
add_test(NAME agc_shader_disk_cache_host_stderr COMMAND agc_shader_disk_cache_tests --host-stderr)
set_tests_properties(agc_shader_disk_cache_host_stderr PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_shader_disk_cache_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_large_tracked_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --large-tracked)
set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(WIN32)
add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint)
@@ -2113,12 +2182,17 @@ if(BUILD_TESTING)
endif()
set_tests_properties(agc_sparse_buffer_holes PROPERTIES TIMEOUT 30)
add_test(NAME agc_pipeline_journal COMMAND agc_driver_graphics_tests --pipeline-journal)
add_test(NAME agc_pipeline_journal_long_paths COMMAND agc_driver_graphics_tests --pipeline-journal-long-paths)
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)
set_tests_properties(agc_pipeline_journal PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(agc_pipeline_library_clear_waits_for_build PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
set_tests_properties(agc_pipeline_store_lazy_buffer_pool PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
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 PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_sparse_buffer_holes PROPERTIES TIMEOUT 30)
@@ -2313,12 +2387,22 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample)
set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection-cpu)
set_tests_properties(agc_sampled_cache_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_driver_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection)
set_tests_properties(agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission)
add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized)
add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear)
set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_driver_resource_cache_replaced_texture COMMAND agc_driver_sampled_cache_budget_tests resource-replaced)
add_test(NAME agc_driver_resource_cache_lookup_changed COMMAND agc_driver_sampled_cache_budget_tests resource-lookup-changed)
set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
# The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex
@@ -2537,6 +2621,7 @@ if(AGC_BUILD_VISUAL_TEST)
target_link_libraries(agc_driver_visual_test PRIVATE
shader_recompiler_agc_driver
decoder_png
SDL2-static
libc
libkernel
@@ -3127,6 +3212,6 @@ endif()
if(BUILD_TESTING)
get_directory_property(agcRegisteredTests TESTS)
list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal|agc_driver_pipeline_journal)$")
list(FILTER agcRegisteredTests EXCLUDE REGEX "^(agc_pipeline_journal(_long_paths|_extended_paths)?|agc_driver_pipeline_journal)$")
set_property(TEST ${agcRegisteredTests} APPEND PROPERTY ENVIRONMENT "APS5_NO_PIPELINE_JOURNAL=1")
endif()
@@ -17,6 +17,7 @@ extern "C" void AgcDriverRaiseWorkerThreadPriority_nid_postfix(const char* role)
extern "C" void AgcDriverLockVulkanLoader_nid_postfix();
extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix();
extern "C" void AgcDriverShutdown_nid_postfix();
extern "C" void AgcDriverRequestScreenshot_nid_postfix();
extern "C" void AgcDriverSuspendPoint_nid_postfix();
extern "C" std::uint64_t AgcDriverDispatchesAdoptedAhead_nid_postfix();
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader);
@@ -11,6 +11,7 @@
#include <cstdint>
#include <list>
#include <memory>
#include <utility>
#include <vector>
#include "prx/libc/include/general/AtomicSharedPtr.hpp"
@@ -45,6 +46,9 @@ void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi
void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi);
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment);
void AppendProgramMemory(std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<DrawProgram>& programs);
std::vector<std::pair<std::size_t, std::size_t>> UserDataWords(const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, std::uint32_t location);
struct PreparedGraphicsStage {
std::shared_ptr<const ShaderSnapshot> snapshot;
PreparedShaders::Entry entry;
@@ -163,7 +163,7 @@ private:
std::shared_ptr<DrawDecode> decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context);
void resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile);
void lookupDraw(const Submission& submission, const std::shared_ptr<VulkanDevice>& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr<DrawEntry>& entry, std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, const std::shared_ptr<DrawEntry>& dataEntry, DrawDataCandidates& dataCandidates, std::vector<StageDataHit>& stageData);
std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected);
std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics);
static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads);
void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey);
static bool drawPrecheck();
@@ -30,7 +30,7 @@ enum TrySite { TryLabel = 0, TryFlush, TryReap, TryPoll, TryIdle, TryCapture, Tr
extern std::atomic<std::uint64_t> triesFailed[TrySites];
extern std::atomic<std::uint64_t> storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, dmaFills, copiesToHostMemory;
extern std::atomic<std::uint64_t> storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, dmaFills, copiesToHostMemory, copiesBehindPendingSources;
extern std::map<std::uint32_t, std::uint64_t> drainCounts;
@@ -10,6 +10,10 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <condition_variable>
#include <exception>
#include <functional>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
@@ -18,6 +22,7 @@
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>
@@ -267,6 +272,18 @@ private:
Clock::duration folded{};
};
struct TraceHost {
std::string cpu;
std::uint32_t threads = 0;
std::uint64_t memoryMiB = 0;
};
struct TraceTitle {
std::string id;
std::string name;
std::string contentVersion;
};
struct TraceSummary {
std::string commit;
std::optional<bool> dirty;
@@ -281,6 +298,135 @@ struct TraceSummary {
double gpuBusyMs = 0;
std::string trace;
std::optional<std::pair<double, double>> interval;
std::optional<TraceHost> host;
std::optional<TraceTitle> title;
};
struct TraceBenchInterval {
double startS = 0;
double endS = 0;
std::uint64_t firstPresent = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
double gpuBusyMs = 0;
std::vector<std::uint32_t> frameUs;
};
using TraceBenchIntervals = std::vector<std::shared_ptr<const TraceBenchInterval>>;
class TraceBench {
public:
static constexpr std::size_t MaximumIntervals = 240;
static void Keep(TraceBenchIntervals& kept, std::shared_ptr<const TraceBenchInterval> interval, std::uint64_t& dropped) {
kept.push_back(std::move(interval));
if (kept.size() <= MaximumIntervals) return;
const auto excess = kept.size() - MaximumIntervals;
kept.erase(kept.begin(), kept.begin() + static_cast<std::ptrdiff_t>(excess));
dropped += excess;
}
static bool Parse(const char* bench, bool summaryEvery) {
if (bench == nullptr) return false;
if (std::string_view(bench) != "1") throw std::invalid_argument("APS5_TRACE_BENCH=" + std::string(bench) + " is not 1");
if (!summaryEvery) throw std::invalid_argument("APS5_TRACE_BENCH=1 needs APS5_TRACE_SUMMARY_EVERY");
return true;
}
};
struct TraceBenchSnapshot {
TraceSummary summary;
TraceBenchIntervals intervals;
std::uint64_t dropped = 0;
};
class TraceBenchWriter {
public:
explicit TraceBenchWriter(std::function<void(const TraceBenchSnapshot&)> writer) : writer(std::move(writer)) {
if (!this->writer) throw std::invalid_argument("invalid benchmark writer");
worker = std::thread([this] { run(); });
}
TraceBenchWriter(const TraceBenchWriter&) = delete;
TraceBenchWriter& operator=(const TraceBenchWriter&) = delete;
~TraceBenchWriter() {
Stop();
}
void Stop() {
std::lock_guard stopLock(stopMutex);
{
std::lock_guard lock(mutex);
stopping = true;
}
changed.notify_one();
if (worker.joinable()) worker.join();
}
void Write(TraceBenchSnapshot snapshot) {
{
std::lock_guard lock(mutex);
if (failure) std::rethrow_exception(failure);
if (stopping) throw std::runtime_error("benchmark writer is stopping");
if (pending) ++superseded;
pending = std::move(snapshot);
}
changed.notify_one();
}
void Flush() {
std::unique_lock lock(mutex);
idle.wait(lock, [&] { return failure || (!pending && !writing); });
if (failure) std::rethrow_exception(failure);
}
std::uint64_t Superseded() {
std::lock_guard lock(mutex);
return superseded;
}
private:
void run() noexcept {
try {
for (;;) {
TraceBenchSnapshot snapshot;
{
std::unique_lock lock(mutex);
changed.wait(lock, [&] { return stopping || pending.has_value(); });
if (!pending) return;
snapshot = std::move(*pending);
pending.reset();
writing = true;
}
writer(snapshot);
{
std::lock_guard lock(mutex);
writing = false;
}
idle.notify_all();
}
} catch (...) {
{
std::lock_guard lock(mutex);
failure = std::current_exception();
writing = false;
}
idle.notify_all();
}
}
std::function<void(const TraceBenchSnapshot&)> writer;
std::mutex stopMutex;
std::mutex mutex;
std::condition_variable changed;
std::condition_variable idle;
std::optional<TraceBenchSnapshot> pending;
std::uint64_t superseded = 0;
bool writing = false;
bool stopping = false;
std::exception_ptr failure;
std::thread worker;
};
struct TraceGpuRange {
@@ -429,6 +575,7 @@ public:
AppendString(output, summary.driver);
output += ",\"os\":";
AppendString(output, summary.os);
AppendMachine(output, summary);
output += ",\"resolution\":";
AppendString(output, std::to_string(summary.width) + "x" + std::to_string(summary.height));
output += ",\"window\":{\"spec\":";
@@ -502,7 +649,71 @@ public:
return output;
}
static std::vector<std::uint32_t> FrameMicroseconds(std::span<const Clock::time_point> presents) {
std::vector<std::uint32_t> frames;
for (std::size_t index = 1; index < presents.size(); ++index) {
const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(presents[index] - presents[index - 1]).count();
frames.push_back(static_cast<std::uint32_t>(std::clamp<std::int64_t>(elapsed, 0, std::numeric_limits<std::uint32_t>::max())));
}
return frames;
}
static std::string Bench(const TraceSummary& summary, std::span<const std::shared_ptr<const TraceBenchInterval>> intervals, std::uint64_t intervalsDropped) {
std::string output = "{\"format\":\"aps5-bench/1\",\"commit\":";
AppendString(output, summary.commit);
output += ",\"dirty\":";
output += summary.dirty.has_value() ? (*summary.dirty ? "true" : "false") : "null";
output += ",\"env\":{";
for (std::size_t index = 0; index < summary.environment.size(); ++index) {
if (index != 0) output += ',';
AppendString(output, summary.environment[index].first);
output += ':';
AppendString(output, summary.environment[index].second);
}
output += "},\"gpu\":";
AppendString(output, summary.gpu);
output += ",\"driver\":";
AppendString(output, summary.driver);
output += ",\"os\":";
AppendString(output, summary.os);
AppendMachine(output, summary);
output += ",\"intervals_dropped\":" + std::to_string(intervalsDropped) + ",\"intervals\":[";
for (std::size_t index = 0; index < intervals.size(); ++index) {
const auto& interval = *intervals[index];
if (index != 0) output += ',';
output += "\n{\"start_s\":" + number(interval.startS) + ",\"end_s\":" + number(interval.endS);
output += ",\"first_present\":" + std::to_string(interval.firstPresent);
output += ",\"resolution\":";
AppendString(output, std::to_string(interval.width) + "x" + std::to_string(interval.height));
output += ",\"gpu_busy_ms\":" + number(interval.gpuBusyMs) + ",\"frame_us\":[";
for (std::size_t frame = 0; frame < interval.frameUs.size(); ++frame) {
if (frame != 0) output += ',';
output += std::to_string(interval.frameUs[frame]);
}
output += "]}";
}
output += "\n]}\n";
return output;
}
private:
static void AppendMachine(std::string& output, const TraceSummary& summary) {
if (summary.host) {
output += ",\"cpu\":";
AppendString(output, summary.host->cpu);
output += ",\"cpu_threads\":" + std::to_string(summary.host->threads);
output += ",\"memory_mib\":" + std::to_string(summary.host->memoryMiB);
}
if (summary.title) {
output += ",\"title_id\":";
AppendString(output, summary.title->id);
output += ",\"title\":";
AppendString(output, summary.title->name);
output += ",\"content_version\":";
AppendString(output, summary.title->contentVersion);
}
}
static double microseconds(Clock::duration elapsed) {
return std::chrono::duration<double, std::micro>(elapsed).count();
}
@@ -521,7 +732,7 @@ private:
class PerformanceTrace {
public:
static bool Configured() {
static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr;
static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr || std::getenv("APS5_TRACE_BENCH") != nullptr;
return configured;
}
@@ -89,7 +89,8 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue);
void Execute(std::span<const std::uint32_t> packet, QueueState& queue);
inline bool IndirectRegisterOpcode(std::uint32_t opcode) { return opcode == 0x63 || opcode == 0x64 || opcode == 0x9f; }
std::vector<std::uint32_t> ReadIndirectRegisters(std::span<const std::uint32_t> packet);
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue);
bool IndirectRegistersValid(std::span<const std::uint32_t> pairs);
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue, bool skipInvalid = false);
bool AccessesMemory(std::uint32_t header);
// Whether an ACQUIRE_MEM packet asks only for GPU cache actions (no CPU-visible memory
// synchronization): such a packet needs a pipeline barrier, not a device drain.
@@ -42,7 +42,7 @@ public:
{
std::lock_guard lock(mutex);
if (failure) std::rethrow_exception(failure);
if (stopping) throw std::runtime_error("profile output is stopping");
if (stopping) return false;
if (text.empty()) return true;
if (text.size() > capacity - bytes) {
++dropped;
@@ -69,6 +69,7 @@ public:
} else if (static_cast<std::size_t>(size) > capacity) {
std::lock_guard lock(mutex);
if (failure) std::rethrow_exception(failure);
if (stopping) return;
++dropped;
changed.notify_one();
} else {
@@ -0,0 +1,18 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <span>
#include <string_view>
namespace AgcDriver::Screenshot {
void Request();
bool Take();
std::filesystem::path Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span<const std::byte> bgra);
}
#endif
@@ -9,6 +9,7 @@ namespace AgcDriver {
class TraceFile {
public:
static void WriteGzip(const std::filesystem::path& path, std::string_view text);
static void ReplaceGzip(const std::filesystem::path& path, std::string_view text);
static void AppendLine(const std::filesystem::path& path, std::string_view line);
};
@@ -103,6 +103,7 @@ public:
// Pending-label table lookup and open-batch overlap test for WAIT_REG_MEM (see Recorder).
std::optional<Graphics::Recorder::LabelHit> PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, Graphics::Recorder::LabelRefusal* refusal = nullptr) const;
bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const;
void SealPassWriteOnly();
bool RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const;
bool StoresPendingOver(std::uint64_t address, std::size_t bytes) const;
// Fills [address, address + bytes) of host-imported guest memory with a repeating 16-byte pattern,
@@ -167,6 +168,7 @@ public:
};
using CopyWriterNote = std::function<void(std::span<const std::byte> value, std::uint64_t generation)>;
CopyOutcome CopyBuffer(std::uint64_t destination, std::uint64_t source, std::size_t bytes, std::size_t cpuMax, std::size_t gpuMax, std::size_t knownMax, std::uint64_t programAddress, std::uint32_t queue, const CopyWriterNote& noteWriter);
bool GpuCopyPossible(std::uint64_t destination, std::uint64_t source, std::size_t bytes) const;
// APS5_COPY_VERIFY=1 totals: CPU copies verified, whose source bytes changed over the sync (a
// producer the rule missed), whose destination bytes were overwritten (a store the rule
// missed), and whose destination an unsignaled batch read in place (the rule's own answer
@@ -314,6 +316,7 @@ public:
// `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);
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);
// A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after
@@ -339,6 +342,8 @@ public:
std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const;
VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const;
std::shared_ptr<ComputePipelineObjects> createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
bool completionStoresOver(std::uint64_t address, std::size_t bytes) const;
bool copyWaitsOnCompletions(std::uint64_t address, std::size_t bytes) const;
private:
VulkanDevice(const PresentationWindow* window, bool compileOnly);
@@ -358,6 +363,7 @@ private:
// registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks.
void recordDispatch(RecordedDispatch& record);
bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span<const std::byte> pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr<Graphics::StorageTexture>& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr, std::uint32_t residentLayer = 0, bool passthrough = false);
bool takeScreenshot(bool dumpFrame);
void recreateSwapchain(std::uint32_t width, std::uint32_t height, bool force);
void refreshSurfaceFormats();
void setHdrMetadata();
@@ -296,8 +296,13 @@ std::filesystem::path ComputePipelineCapture::Write(const std::filesystem::path&
CheckCaptureIo(error, "cannot enumerate capture directory");
while (iterator != std::filesystem::directory_iterator{}) {
const auto& entry = *iterator;
RequireCaptureCapacity(++count < 128, "capture directory reached 128 entries");
const auto entryStatus = CaptureStatus(entry.path());
if (std::filesystem::is_regular_file(entryStatus) && entry.path().filename() == ".DS_Store") {
iterator.increment(error);
CheckCaptureIo(error, "cannot advance capture directory");
continue;
}
RequireCaptureCapacity(++count < 128, "capture directory reached 128 entries");
RequireCapture(std::filesystem::is_directory(entryStatus) && !std::filesystem::is_symlink(entryStatus), "capture directory contains an unexpected entry");
const auto capture = entry.path() / "capture.aps5cp";
const auto captureStatus = CaptureStatus(capture);
@@ -359,7 +359,13 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
totalMs += elapsed;
if (profile && elapsed > 200) std::fprintf(stderr, "[gpu] compute shader 0x%llx recompile took %.0f ms (%zu SPIR-V words, %zu captured regions, total %.1f s)\n", static_cast<unsigned long long>(address), elapsed, compiledResult->spirv.size(), captured.size(), totalMs / 1000);
} catch (const std::exception& error) {
const auto dump = dumpShaders ? dumpRequest(address, request) : std::string{};
std::string dump;
if (dumpShaders) {
const auto regions = shaderMemory->Regions();
auto failed = request;
failed.context.memory = regions;
dump = dumpRequest(address, failed, "_failed");
}
std::string reason = error.what();
if (const auto newline = reason.find('\n'); newline != std::string::npos) reason.resize(newline);
@@ -384,8 +390,8 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
parked.keepVariant = keepVariant;
parked.attachVariant = attachVariant;
parked.shaderMemory = shaderMemory;
parked.captured = captured;
parked.liveWords = liveWords;
parked.captured = std::move(captured);
parked.liveWords = std::move(liveWords);
parked.capture = capture;
parked.dataHit = dataHit;
parked.cached = cached;
@@ -445,7 +451,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed);
const auto queueId = submission.queue;
itemCommits.fetch_add(1, std::memory_order_relaxed);
DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), groups, address, queueId, recipe, keepVariant, dataHit, epoch] {
DrawPipeline::Queue0().Enqueue([this, localDevice = std::move(localDevice), result = compiledResult, snapshots = std::move(snapshots), liveWords = std::move(liveWords), groups, address, queueId, recipe, keepVariant, dataHit, epoch] {
DrawPipeline::FollowEpoch(epoch);
GuestMemory::SetCurrentPacket(0x15, queueId);
const auto& compiledCommit = *result;
@@ -216,13 +216,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
phaseTiming.Phase(DrawRowDecode);
const auto locate = [&](std::uint32_t location) -> std::optional<std::pair<std::size_t, std::size_t>> {
if (location == 0x280u) return std::nullopt;
for (std::size_t i = 0; i < programs.size(); ++i) {
if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue;
const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr);
if (word < programs[i].userData.size()) return std::make_pair(i, static_cast<std::size_t>(word));
}
return std::nullopt;
const auto words = UserDataWords(programs, roles, location);
if (words.empty()) return std::nullopt;
return words.front();
};
if (drawParameters.indirect) {
auto& indirect = *drawParameters.indirect;
@@ -237,13 +233,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
}
std::vector<ShaderRecompiler::MemoryRegion> memory;
memory.reserve(2 * programs.size() + 32);
AppendProgramMemory(memory, programs);
std::vector<ShaderRecompiler::LinkedProgram> linked;
linked.reserve(programs.size());
for (std::size_t i = 0; i < programs.size(); ++i) {
const auto& program = programs[i];
for (const auto& region : program.memory) {
if (!region.bytes.empty()) memory.push_back(region);
}
linked.push_back({roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData});
}
timing.Mark("prepare");
@@ -380,6 +374,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
};
std::optional<Graphics::IndirectDrawPath> indirectCpu;
std::vector<std::uint32_t> pushOffsets(programs.size(), 0);
std::optional<ShaderRecompiler::MeshBarycentricLayout> meshBarycentrics;
std::vector<std::size_t> resultIndex(programs.size(), 0);
for (std::size_t i = 0; i < programs.size(); ++i) {
if (roles[i] == Role::GeometryBack) continue;
@@ -407,7 +402,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
break;
}
}
compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected));
compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[i] = compiledStages.back().get();
if (candidate != dataCandidates[i].end()) verifyDataStage(i, *candidate->first);
@@ -471,7 +466,21 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
phaseTiming.Phase(DrawRowRectList);
};
if (graphics.rectList) buildRectList();
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active && graphics.stages.path == Graphics::ShaderPath::Geometry) {
require(localDevice->Target().mesh.has_value(), "a pixel shader on the mesh path reads barycentrics, and this device has neither VK_KHR_fragment_shader_barycentric nor mesh shaders (VK_EXT_mesh_shader), so they cannot be provided or emulated");
require(!graphics.rectList && graphics.stages.mesh && programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics needs exactly one mesh program before it to emulate them");
meshBarycentrics = ShaderRecompiler::MeshBarycentricLayoutOf(*programResults[1]);
if (programResults[0]->meshBarycentrics != meshBarycentrics) {
const auto pushBytes = programResults[0]->pushConstants.size();
const auto exports = programResults[0]->parameterExports;
compiledStages.push_back(materializeDrawStage(0, pushOffsets[0], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[0] = compiledStages.back().get();
stages[0].program = programResults[0];
require(programResults[0]->pushConstants.size() == pushBytes && programResults[0]->parameterExports == exports, "pairing the mesh program with its pixel shader changed the push constants or the parameter exports the pixel shader was built for");
}
require(programResults[0]->meshBarycentrics == meshBarycentrics, "the mesh program was compiled for a different barycentric layout than its pixel shader");
} else if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it");
const auto geometryLimits = localDevice->GeometryLimits();
if (geometryLimits || !localDevice->Target().tessellation) {
@@ -529,9 +538,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
std::vector<Pm4::DrawArguments> records;
for (std::uint32_t record = 0; record < count; ++record) records.push_back(Pm4::ReadDrawArguments(indirect, record));
Graphics::CountIndirectDraw(*indirectCpu, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - readStart).count());
const auto baseVertexWord = locate(indirect.baseVertexLocation);
const auto startInstanceWord = locate(indirect.startInstanceLocation);
const auto drawIndexWord = indirect.drawIndexEnabled ? locate(indirect.drawIndexLocation) : std::nullopt;
const auto baseVertexWords = UserDataWords(programs, roles, indirect.baseVertexLocation);
const auto startInstanceWords = UserDataWords(programs, roles, indirect.startInstanceLocation);
const auto drawIndexWords = indirect.drawIndexEnabled ? UserDataWords(programs, roles, indirect.drawIndexLocation) : std::vector<std::pair<std::size_t, std::size_t>>{};
const auto drawable = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); };
std::size_t lastDrawable = records.size();
for (std::size_t record = 0; record < records.size(); ++record) {
@@ -542,14 +551,15 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
if (traceIndirect) std::fprintf(stderr, "[draw] record %u: count %u instances %u first %u vertexOffset %u startInstance %u\n", record, arguments.count, arguments.instances, arguments.firstVertexOrIndex, arguments.vertexOffset, arguments.firstInstance);
if (arguments.count == 0 || arguments.instances == 0) continue;
std::set<std::size_t> patched;
const auto patch = [&](const std::optional<std::pair<std::size_t, std::size_t>>& word, std::uint32_t value) {
if (!word) return;
writablePrograms[word->first].userData[word->second] = value;
patched.insert(word->first);
const auto patch = [&](const std::vector<std::pair<std::size_t, std::size_t>>& words, std::uint32_t value) {
for (const auto& [program, word] : words) {
writablePrograms[program].userData[word] = value;
patched.insert(program);
}
};
patch(baseVertexWord, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex);
patch(startInstanceWord, arguments.firstInstance);
patch(drawIndexWord, record);
patch(baseVertexWords, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex);
patch(startInstanceWords, arguments.firstInstance);
patch(drawIndexWords, record);
if (graphics.stages.tessellation) {
for (std::size_t i = 0; i < programs.size(); ++i) {
if (programs[i].binary.stage == Stage::TessellationControl || programs[i].binary.stage == Stage::TessellationEvaluation) patched.insert(i);
@@ -573,7 +583,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
auto& result = results[resultIndex[programIndex]];
const auto pushBytes = result.pushConstants.size();
decodeVertexInfo(programIndex);
result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected);
result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics);
if (!rejected.empty()) return DrawVerdict::Rejected;
require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout");
}
@@ -8,12 +8,13 @@
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <span>
#include <sstream>
#include <stdexcept>
namespace AgcDriver::DriverDetail {
std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected) {
std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics) {
PerformanceTimer timing("Shader.DrawStage");
using Stage = ShaderRecompiler::ShaderStage;
phaseTiming.Phase(DrawRowVectors);
@@ -24,7 +25,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)},
localDevice->Target(),
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : graphics.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}}
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt}
};
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request);
@@ -32,12 +33,14 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
auto& stageCapture = stageCaptures[i];
stageCapture.forgetSerial = GuestMemory::ForgetSerial();
stageCapture.pushOffset = pushOffset;
const auto named = [&](const std::exception& error) {
const auto named = [&](const std::exception& error, std::span<const ShaderRecompiler::MemoryRegion> regions) {
static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr;
std::string dump;
if (dumpShaders) {
try {
dump = dumpRequest(program.binary.codeAddress, request);
auto failed = request;
failed.context.memory = regions;
dump = dumpRequest(program.binary.codeAddress, failed, "_failed");
} catch (...) {}
}
std::ostringstream where;
@@ -51,7 +54,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
try {
return CaptureInvocation(shaderMemory, *program.snapshot, program.codeOffset, request, invocation);
} catch (const std::exception& error) {
throw named(error);
throw named(error, shaderMemory.Regions());
}
}();
@@ -99,7 +102,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
try {
stageCapture.compiled = invocation.Materialize(*capture);
} catch (const std::exception& error) {
throw named(error);
throw named(error, request.context.memory);
}
stageCapture.capture = capture;
timing.Mark("materialize");
@@ -46,11 +46,34 @@ void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32
const auto address = static_cast<std::uint64_t>(low) | (static_cast<std::uint64_t>(high) << 32u);
require(address != 0 || !pointerRequired, "merged shader user-data address is null");
if (address == 0) return;
GuestMemory::CheckRange(reinterpret_cast<const void*>(address), 8, 4);
if (pointerRequired) GuestMemory::CheckRange(reinterpret_cast<const void*>(address), 8, 4);
program.userData[0] = low;
program.userData[1] = high;
}
void AppendProgramMemory(std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<DrawProgram>& programs) {
for (const auto& program : programs) {
for (const auto& region : program.memory) {
if (region.bytes.empty()) continue;
const bool listed = std::any_of(memory.begin(), memory.end(), [&](const ShaderRecompiler::MemoryRegion& known) { return known.guestAddress == region.guestAddress && known.bytes.data() == region.bytes.data() && known.bytes.size() == region.bytes.size(); });
if (!listed) memory.push_back(region);
}
}
}
std::vector<std::pair<std::size_t, std::size_t>> UserDataWords(const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, std::uint32_t location) {
using Role = ShaderRecompiler::ProgramRole;
require(roles.size() == programs.size(), "draw program roles are incomplete");
std::vector<std::pair<std::size_t, std::size_t>> words;
if (location == 0x280u) return words;
for (std::size_t i = 0; i < programs.size(); ++i) {
if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue;
const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr);
if (word < programs[i].userData.size()) words.emplace_back(i, static_cast<std::size_t>(word));
}
return words;
}
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment) {
using Stage = ShaderRecompiler::ShaderStage;
using Role = ShaderRecompiler::ProgramRole;
@@ -95,11 +118,21 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
programs.push_back(prepare(programAddress(base), type, stage, resources, users));
roles.push_back(role);
};
const auto& graphics = product->state;
auto& graphics = product->state;
if (graphics.stages.path == Graphics::ShaderPath::Tessellation) {
if (!staticAbi || programAddress(0x148) != 0) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local);
append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull);
InitializeMerged(queue, programs.back(), 0x102, true, staticAbi);
const auto localAddress = programAddress(0x148);
const auto local = !staticAbi || localAddress != 0 ? ProgramSnapshot(registry, localAddress, {3, 5}) : nullptr;
const bool mergedHull = local != nullptr && !local->header.empty() && local->type == 3;
require(graphics.stages.tessellation.has_value(), "tessellation draw has no tessellation configuration");
graphics.stages.tessellation->mergedHull = mergedHull;
if (mergedHull) {
append(0x148, 3, Stage::Local, 0x10b, 0x10c, Role::Local);
append(0x148, 3, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull);
} else {
if (local != nullptr) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local);
append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull);
}
InitializeMerged(queue, programs.back(), 0x102, !mergedHull, staticAbi);
append(0x0c8, 2, Stage::TessellationEvaluation, 0x08b, 0x08c, Role::Domain);
} else if (graphics.stages.path == Graphics::ShaderPath::Geometry) {
const auto frontAddress = programAddress(0xc8);
@@ -28,6 +28,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
if (graphics.stages.tessellation) {
const auto& tess = *graphics.stages.tessellation;
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology, tess.patchesPerGroup, tess.primitiveGroupSize, static_cast<std::uint32_t>(tess.breakAtInstance)}) mix(value);
mix(tess.mergedHull);
}
mix(graphics.rectList);
mix(programs.size());
@@ -241,7 +241,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
const auto sameTess = [](const std::optional<ShaderRecompiler::TessellationConfiguration>& x, const std::optional<ShaderRecompiler::TessellationConfiguration>& y) {
if (x.has_value() != y.has_value()) return false;
if (!x) return true;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance && x->mergedHull == y->mergedHull;
};
if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false;
if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false;
@@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp"
#include <mutex>
namespace AgcDriver {
@@ -83,6 +84,10 @@ extern "C" void AgcDriverShutdown_nid_postfix() {
AgcDriver::Shutdown();
}
extern "C" void AgcDriverRequestScreenshot_nid_postfix() {
AgcDriver::Screenshot::Request();
}
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader) try {
AgcDriver::RegisterShader(shader);
} catch (const ProcessShutdown&) {
@@ -52,9 +52,9 @@ void PrintFailure(std::exception_ptr error) {
std::fflush(stderr);
}
constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull;
constexpr std::uint64_t DriverMemoryAddress = 0xfe0000000ull + GuestArena::GuestAddressOffset;
constexpr std::size_t DriverMemoryBytes = 2u << 20u;
constexpr std::uint64_t FsTableAddress = 0xfe0040000ull;
constexpr std::uint64_t FsTableAddress = DriverMemoryAddress + 0x40000ull;
constexpr std::uint64_t FsTableSlotBytes = 16u;
constexpr std::uint64_t FsTableSlots = 6u;
constexpr std::uint64_t FsRoutineRecords = 8u;
@@ -67,8 +67,9 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> DispatchWriteRanges(const S
bounded = false;
continue;
}
const auto resource = Graphics::DecodeTextureResource(std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8, 8));
if (resource.baseAddress == 0) continue;
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 8, 8);
if (Graphics::IsNullTextureResource(words)) continue;
const auto resource = Graphics::DecodeTextureResource(words);
const auto surfaceBytes = Graphics::DescribeSurface(resource).guestBytes;
ranges.emplace_back(resource.baseAddress, resource.baseAddress + (surfaceBytes != 0 ? surfaceBytes : UnknownImageBytes));
if (resource.dccAddress != 0) ranges.emplace_back(resource.dccAddress, resource.dccAddress + (surfaceBytes != 0 ? surfaceBytes / 256 + 1 : UnknownImageBytes));
@@ -11,9 +11,12 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <shared_mutex>
#include <utility>
namespace AgcDriver::DriverDetail {
@@ -69,6 +72,20 @@ bool keepsDrawPipeline(std::span<const std::uint32_t> packet, std::uint32_t head
}
}
bool sealsWriteOnly(std::span<const std::uint32_t> packet, std::uint32_t opcode) {
if (opcode == 0x58) return true;
if (opcode != 0x46 || packet.size() < 2) return false;
switch (packet[1] & 0x3fu) {
case 0x07: case 0x0f: case 0x10: case 0x16: case 0x2a: case 0x2c: case 0x2e: case 0x31: return true;
default: return false;
}
}
bool& drawnSinceSeal() {
static thread_local bool drawn = false;
return drawn;
}
}
void Driver::deliverGuestEopInterrupt(std::uint32_t queue) {
@@ -405,6 +422,7 @@ void Driver::execute(const Submission& submission) {
} else if (drawPacket) {
landPendingWaits(submission.queue);
packetCounters.CountDraw();
drawnSinceSeal() = true;
bool drawn = false;
timed(&WorkerProfile::drawMs, [&] {
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
@@ -447,10 +465,43 @@ void Driver::execute(const Submission& submission) {
Pm4::Execute(packet, queue);
} else if (sampleDump && !wroteOnGpu) {
dumpSampleCounters(Pm4::SampleDumpAddress(packet));
} else if (submission.queue == 0 && sealsWriteOnly(packet, opcode)) {
if (std::exchange(drawnSinceSeal(), false)) {
const auto seal = [this] {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (const auto localDevice = device.Load()) localDevice->SealPassWriteOnly();
};
if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Enqueue(seal, {});
else seal();
}
} else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) {
if (!wroteOnGpu) {
if (Pm4::IndirectRegisterOpcode(opcode)) Pm4::ExecuteIndirectRegisters(packet, submission.registerLists.at(cursor), queue);
else Pm4::Execute(packet, queue);
if (Pm4::IndirectRegisterOpcode(opcode)) {
const auto& listed = submission.registerLists.at(cursor);
if (Pm4::IndirectRegistersValid(listed)) {
Pm4::ExecuteIndirectRegisters(packet, listed, queue);
} else {
landPendingWaits(submission.queue);
recordQueuedLabelsBeforeRead(submission.queue);
const auto current = Pm4::ReadIndirectRegisters(packet);
static const bool skipInvalid = std::getenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS") != nullptr;
static std::atomic<bool> reported{false};
if (!reported.exchange(true)) {
std::uint32_t invalid = 0;
for (std::size_t i = 0; i < listed.size(); i += 2) {
if (!Pm4::IndirectRegistersValid(std::span(&listed[i], 1))) {
invalid = listed[i];
break;
}
}
std::fprintf(stderr, "[gpu] queue 0x%x %s at DWORD %zu: register list entry with offset dword 0x%08x read again where the packet executes%s\n", submission.queue, Pm4::Name(packet[0]).c_str(), cursor, invalid, skipInvalid ? "; invalid entries are skipped (APS5_SKIP_INVALID_INDIRECT_REGISTERS)" : "");
}
Pm4::ExecuteIndirectRegisters(packet, current, queue, skipInvalid);
}
} else {
Pm4::Execute(packet, queue);
}
if (opcode == 0x49 || opcode == 0x37) {
if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue);
}
@@ -11,6 +11,8 @@
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <limits>
@@ -114,7 +116,29 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
if (!drainAll && !cpuStores && (opcode == 0x40 || opcode == 0x50 || opcode == 0x83)) {
constexpr std::size_t gpuStoreLimit = 65536;
bool drained = true;
if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) {
const auto copy = opcode == 0x50 ? Pm4::DecodeMemoryCopy(packet) : std::nullopt;
const auto copyBehindPendingSource = [&] {
if (!copy.has_value() || copy->bytes > gpuStoreLimit || !GuestMemory::Accessible(reinterpret_cast<const void*>(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast<const void*>(copy->destination), copy->bytes, true)) return false;
const auto end = copy->source + copy->bytes;
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> range{{{copy->source, end}}};
const auto& queued = deferredLabels().labels;
const bool labelQueued = std::any_of(queued.begin(), queued.end(), [&](const DeferredLabel& label) { return label.address < end && copy->source < label.address + label.size; });
if (!labelQueued && !Graphics::Recorder::SnapshotWriteOverlaps(copy->source, copy->bytes) && !Graphics::StorageTexture::AnyPendingOverlaps(range) && !Graphics::AnyShadowedOverlaps(copy->source, copy->bytes)) return false;
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
const auto localDevice = device.Load();
if (localDevice == nullptr) return false;
recordDeferredLabels(localDevice.get(), submission.queue);
if (!localDevice->StoresPendingOver(copy->source, copy->bytes) || !localDevice->GpuCopyPossible(copy->destination, copy->source, copy->bytes)) return false;
const auto outcome = localDevice->CopyBuffer(copy->destination, copy->source, copy->bytes, 0, std::numeric_limits<std::size_t>::max(), 0, 0, submission.queue, [](std::span<const std::byte>, std::uint64_t) {});
if (outcome.path != 1 && outcome.path != 3) throw std::runtime_error("AGC driver: a DMA_DATA copy of up to 64 KiB that GpuCopyPossible accepted took copy path " + std::to_string(outcome.path));
return true;
};
if (copyBehindPendingSource()) {
++copiesBehindPendingSources;
wroteOnGpu = true;
drained = false;
} else if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) {
const auto bytes = store->Bytes();
if (bytes.empty()) {
@@ -147,7 +171,6 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
}
}
if (drained && opcode == 0x50) {
const auto copy = Pm4::DecodeMemoryCopy(packet);
if (copy.has_value() && copy->bytes > gpuStoreLimit && GuestMemory::Accessible(reinterpret_cast<const void*>(copy->source), copy->bytes) && GuestMemory::Accessible(reinterpret_cast<const void*>(copy->destination), copy->bytes, true)) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
@@ -2,6 +2,7 @@
#include "ControlFlow/GraphBuilder.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
@@ -32,6 +33,13 @@
#include <mutex>
#include <atomic>
#include <pthread.h>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <process.h>
#endif
#ifdef __GLIBC__
#include <malloc.h>
#endif
@@ -69,6 +77,11 @@ std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnosti
}
std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address) {
const auto drain = [](std::uint64_t first, std::size_t bytes) {
if (!DrawPipeline::Active()) return;
auto& pipeline = DrawPipeline::Queue0();
if (pipeline.Busy() && pipeline.Overlaps(first, bytes)) pipeline.Drain(DrawPipeline::DrainReason::Capture);
};
GuestMemory::CheckRange(reinterpret_cast<const void*>(address), sizeof(std::uint32_t), 256);
static std::mutex cacheMutex;
static std::list<std::shared_ptr<const ShaderSnapshot>> cache;
@@ -81,6 +94,7 @@ std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address) {
}
if (cached) {
const auto code = std::as_bytes(std::span(cached->code));
drain(address, code.size());
GuestMemory::FlushGpuWrites(address, code.size());
if (GuestMemory::CompareMapped(address, code) == GuestMemory::Compare::Equal) {
std::lock_guard lock(cacheMutex);
@@ -101,6 +115,7 @@ std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address) {
while (snapshot.code.size() < available) {
const auto previous = snapshot.code.size();
snapshot.code.resize(std::min(available, std::max<std::size_t>(64, previous * 2)));
drain(address + previous * sizeof(std::uint32_t), (snapshot.code.size() - previous) * sizeof(std::uint32_t));
GuestMemory::Read(address + previous * sizeof(std::uint32_t),
std::as_writable_bytes(std::span(snapshot.code).subspan(previous)), alignof(std::uint32_t));
try {
@@ -803,7 +818,7 @@ std::optional<ShaderRecompiler::ShaderFloatMode> DecodeFloatMode(const Registers
case 0: rsrc1 = 0x212; fp16OverflowBit = 26; break;
case 1: rsrc1 = 0x00a; fp16OverflowBit = 29; break;
case 2: case 4: case 6: rsrc1 = 0x08a; fp16OverflowBit = 31; break;
case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break;
case 3: case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break;
default: return std::nullopt;
}
const auto found = registers.find(rsrc1);
@@ -901,7 +916,7 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
case 1: stage = Stage::Fragment; programRegister = 0x008; resourceRegister = 0x00b; break;
case 2: stage = Stage::Vertex; programRegister = 0x0c8; resourceRegister = 0x08b; firstUser = 8; break;
case 4: stage = Stage::Mesh; programRegister = 0x0c8; resourceRegister = 0x08b; break;
case 5: return {};
case 3: case 5: return {};
case 6: stage = Stage::Mesh; programRegister = 0x088; resourceRegister = 0x08b; break;
case 7: stage = Stage::TessellationControl; programRegister = 0x108; resourceRegister = 0x10b; break;
default:
@@ -1190,7 +1205,7 @@ public:
}
void Close() {
std::vector<pthread_t> joined;
std::vector<Worker> joined;
{
std::unique_lock lock(mutex);
closed = true;
@@ -1198,16 +1213,45 @@ public:
joined.swap(workers);
}
ready.notify_all();
for (const auto worker : joined) {
if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr);
}
for (const auto& worker : joined) Join(worker);
}
private:
static constexpr std::size_t StackBytes = 64u << 20u;
#ifdef _WIN32
struct Worker {
HANDLE handle;
unsigned id;
};
static void Join(const Worker& worker) {
if (worker.id != GetCurrentThreadId()) WaitForSingleObject(worker.handle, INFINITE);
CloseHandle(worker.handle);
}
#else
using Worker = pthread_t;
static void Join(Worker worker) {
if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr);
}
#endif
explicit RegistrationPreparePool(unsigned count) {
#ifdef _WIN32
for (unsigned index = 0; index < count; ++index) {
unsigned id = 0;
const auto thread = _beginthreadex(nullptr, static_cast<unsigned>(StackBytes), &RegistrationPreparePool::WindowsMain, this, STACK_SIZE_PARAM_IS_A_RESERVATION, &id);
if (thread != 0) {
workers.push_back({reinterpret_cast<HANDLE>(thread), id});
++threads;
liveWorkers.fetch_add(1, std::memory_order_acq_rel);
}
}
#else
pthread_attr_t attributes;
pthread_attr_init(&attributes);
pthread_attr_setstacksize(&attributes, 64u << 20u);
pthread_attr_setstacksize(&attributes, StackBytes);
for (unsigned index = 0; index < count; ++index) {
pthread_t thread;
if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) {
@@ -1217,8 +1261,16 @@ private:
}
}
pthread_attr_destroy(&attributes);
#endif
}
#ifdef _WIN32
static unsigned __stdcall WindowsMain(void* argument) {
Main(argument);
return 0;
}
#endif
static void* Main(void* argument) {
auto& pool = *static_cast<RegistrationPreparePool*>(argument);
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8);
@@ -1263,7 +1315,7 @@ private:
std::condition_variable ready;
std::condition_variable idle;
std::deque<Task> tasks;
std::vector<pthread_t> workers;
std::vector<Worker> workers;
unsigned threads = 0;
inline static std::atomic<unsigned> liveWorkers{0};
unsigned running = 0;
@@ -1350,13 +1402,13 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
require(expected == decoded.roles, "graphics ABI linked programs do not match the stage routing");
std::vector<ShaderRecompiler::LinkedProgram> linked;
std::vector<ShaderRecompiler::MemoryRegion> memory;
AppendProgramMemory(memory, decoded.programs);
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
const auto& program = decoded.programs[index];
require(program.snapshot != nullptr && program.codeOffset < program.snapshot->code.size(), "graphics ABI has an invalid registered entry point");
const auto code = std::span(program.snapshot->code).subspan(program.codeOffset);
require(program.binary.code.data() == code.data() && program.binary.code.size() == code.size() && program.binary.codeAddress == program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t), "graphics ABI entry point differs from its snapshot");
linked.push_back({decoded.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData});
memory.insert(memory.end(), program.memory.begin(), program.memory.end());
}
std::vector<PreparedGraphicsStage> prepared;
std::uint32_t pushOffset = 0;
@@ -21,7 +21,7 @@ std::atomic<std::uint64_t> packetLockRecordUs{0}, packetLockSubmitUs{0};
std::atomic<std::uint64_t> triesFailed[TrySites] = {};
std::atomic<std::uint64_t> storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0};
std::atomic<std::uint64_t> storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0}, copiesBehindPendingSources{0};
std::map<std::uint32_t, std::uint64_t> drainCounts;
@@ -39,7 +39,7 @@ SubmissionCosts& submissionCosts(std::uint32_t queue) {
void reportSync() {
std::string report;
for (const auto& [code, count] : drainCounts) report += " " + (code == 0xffffu ? std::string("flip") : Pm4::Name(code << 8u)) + "=" + std::to_string(count);
AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source\n", static_cast<unsigned long long>(drainTotal), report.c_str(), static_cast<unsigned long long>(unlockedDrains.load()), static_cast<unsigned long long>(gpuLabels.load()), static_cast<unsigned long long>(completionLabels.load()), static_cast<unsigned long long>(noOpLabels.load()), static_cast<unsigned long long>(labelFallbacks[1].load()), static_cast<unsigned long long>(labelFallbacks[2].load()), static_cast<unsigned long long>(labelFallbacks[3].load()), static_cast<unsigned long long>(labelFallbacks[4].load()), static_cast<unsigned long long>(queuedLabels.load()), static_cast<unsigned long long>(labelGroups.load()), static_cast<unsigned long long>(immediateLabels.load()), static_cast<unsigned long long>(storesOnGpu.load()), static_cast<unsigned long long>(storesBehindCompletions.load()), static_cast<unsigned long long>(storesOnCpu.load()), static_cast<unsigned long long>(storesDrained.load()), static_cast<unsigned long long>(dmaFills.load()), static_cast<unsigned long long>(copiesToHostMemory.load()));
AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source; %llu copies of up to 64 KiB recorded on the GPU behind their source's pending stores\n", static_cast<unsigned long long>(drainTotal), report.c_str(), static_cast<unsigned long long>(unlockedDrains.load()), static_cast<unsigned long long>(gpuLabels.load()), static_cast<unsigned long long>(completionLabels.load()), static_cast<unsigned long long>(noOpLabels.load()), static_cast<unsigned long long>(labelFallbacks[1].load()), static_cast<unsigned long long>(labelFallbacks[2].load()), static_cast<unsigned long long>(labelFallbacks[3].load()), static_cast<unsigned long long>(labelFallbacks[4].load()), static_cast<unsigned long long>(queuedLabels.load()), static_cast<unsigned long long>(labelGroups.load()), static_cast<unsigned long long>(immediateLabels.load()), static_cast<unsigned long long>(storesOnGpu.load()), static_cast<unsigned long long>(storesBehindCompletions.load()), static_cast<unsigned long long>(storesOnCpu.load()), static_cast<unsigned long long>(storesDrained.load()), static_cast<unsigned long long>(dmaFills.load()), static_cast<unsigned long long>(copiesToHostMemory.load()), static_cast<unsigned long long>(copiesBehindPendingSources.load()));
}
void countLabelOutcome(int reason) {
@@ -35,6 +35,7 @@
#include <cstdint>
#include <fcntl.h>
#include <fstream>
#include <link.h>
#include <pthread.h>
#include <sstream>
#include <sys/ioctl.h>
@@ -494,6 +495,26 @@ void ForgetInaccessiblePages(std::uintptr_t address, std::size_t bytes) {
Pages().forgetInaccessible(address, bytes);
}
#ifdef __linux__
int mainImageExtent(dl_phdr_info* info, std::size_t, void* data) {
constexpr auto mask = static_cast<std::uintptr_t>(PageBytes - 1);
auto& span = *static_cast<PageSpan*>(data);
std::uintptr_t first = std::numeric_limits<std::uintptr_t>::max();
std::uintptr_t last = 0;
for (int index = 0; index < info->dlpi_phnum; ++index) {
const auto& header = info->dlpi_phdr[index];
if (header.p_type != PT_LOAD || header.p_memsz == 0) continue;
first = std::min(first, static_cast<std::uintptr_t>(info->dlpi_addr + header.p_vaddr) & ~mask);
last = std::max(last, static_cast<std::uintptr_t>(info->dlpi_addr + header.p_vaddr + header.p_memsz + mask) & ~mask);
}
if (first < last) {
span.base = first;
span.size = static_cast<std::size_t>(last - first);
}
return 1;
}
#endif
void PageStates::initialize() {
if (ready.load(std::memory_order_acquire)) return;
std::call_once(once, [&] {
@@ -516,6 +537,12 @@ void PageStates::initialize() {
image.size = cursor - start;
imageCached = image.allocate();
}
#elif defined(__linux__)
static const bool noImageCache = std::getenv("APS5_NO_IMAGE_PAGE_CACHE") != nullptr;
if (!noImageCache) {
dl_iterate_phdr(mainImageExtent, &image);
imageCached = image.allocate();
}
#endif
if (arenaCached || imageCached) {
GuestAllocations::GuestAllocationsSetInvalidator_nid_postfix(&ForgetPages);
@@ -1514,39 +1541,55 @@ bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function
const auto first = address & ~(page - 1);
const auto stop = (address + bytes + page - 1) & ~(page - 1);
const bool covered = tracker.covers(first, static_cast<std::size_t>(stop - first));
std::vector<std::byte> saved;
std::vector<std::uint32_t> slots;
if (covered && staysWatched) {
const auto length = static_cast<std::size_t>(stop - first);
CheckRange(reinterpret_cast<const void*>(first), length, 1, false);
const auto* source = reinterpret_cast<const std::byte*>(first);
slots.resize(length / page);
std::size_t kept = 0;
for (std::size_t index = 0; index < slots.size(); ++index) {
const bool zero = std::memcmp(source + index * page, ZeroPage.data(), page) == 0;
slots[index] = zero ? WindowCopy::Zero : static_cast<std::uint32_t>(kept++);
}
saved.reserve(kept * page);
for (std::size_t index = 0; index < slots.size(); ++index) {
if (slots[index] != WindowCopy::Zero) saved.insert(saved.end(), source + index * page, source + (index + 1) * page);
}
}
const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true);
bool imported = false;
{
const auto runImport = [&] {
#ifdef __APPLE__
const GuestArena::HostWrite open(reinterpret_cast<void*>(first), static_cast<std::size_t>(stop - first));
#endif
imported = import();
}
if (!imported) return false;
return import();
};
if (!staysWatched) {
const bool before = covered && walkWrites(tracker, first, stop, StampKind::Cpu, true);
if (!runImport()) return false;
if (!before || !walkWrites(tracker, first, stop, StampKind::ImportWindow, true)) unwatchLocked(tracker, first, static_cast<std::size_t>(stop - first));
unwatchLocked(tracker, address, bytes);
return true;
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> spans;
if (covered) spans.emplace_back(first, stop);
else {
for (auto begin = first; begin < stop;) {
const auto end = std::min<std::uint64_t>((begin / WriteBlockBytes + 1) * WriteBlockBytes, stop);
if (tracker.covers(begin, static_cast<std::size_t>(end - begin))) {
if (!spans.empty() && spans.back().second == begin) spans.back().second = end;
else spans.emplace_back(begin, end);
}
begin = end;
}
}
std::vector<std::uint32_t> slots(static_cast<std::size_t>((stop - first) / page), WindowCopy::Zero);
std::size_t kept = 0;
for (const auto& [begin, end] : spans) {
CheckRange(reinterpret_cast<const void*>(begin), static_cast<std::size_t>(end - begin), 1, false);
for (auto cursor = begin; cursor < end; cursor += page) {
if (std::memcmp(reinterpret_cast<const void*>(cursor), ZeroPage.data(), page) != 0) slots[static_cast<std::size_t>((cursor - first) / page)] = static_cast<std::uint32_t>(kept++);
}
}
std::vector<std::byte> saved;
saved.reserve(kept * page);
const auto* source = reinterpret_cast<const std::byte*>(first);
for (std::size_t index = 0; index < slots.size(); ++index) {
if (slots[index] != WindowCopy::Zero) saved.insert(saved.end(), source + index * page, source + (index + 1) * page);
}
std::erase_if(spans, [&](const auto& span) { return !walkWrites(tracker, span.first, span.second, StampKind::Cpu, true); });
if (!runImport()) return false;
const WindowCopy window{first, saved, slots};
if (!before || !walkWrites(tracker, first, stop, StampKind::Cpu, true, &window)) unwatchLocked(tracker, first, static_cast<std::size_t>(stop - first));
auto unwatchedFrom = first;
for (const auto& [begin, end] : spans) {
if (!walkWrites(tracker, begin, end, StampKind::Cpu, true, &window)) continue;
if (unwatchedFrom < begin) unwatchLocked(tracker, unwatchedFrom, static_cast<std::size_t>(begin - unwatchedFrom));
unwatchedFrom = end;
}
if (unwatchedFrom < stop) unwatchLocked(tracker, unwatchedFrom, static_cast<std::size_t>(stop - unwatchedFrom));
return true;
}
@@ -3,11 +3,15 @@
#include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp"
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
#include "prx/libSceAgcDriver/TraceRevision.hpp"
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include <array>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <limits>
#include <mutex>
#include <system_error>
#include <thread>
#ifdef _WIN32
#include <windows.h>
#else
@@ -15,6 +19,11 @@
#include <sys/utsname.h>
#include <unistd.h>
#endif
#if defined(_MSC_VER) && !defined(__clang__)
#include <intrin.h>
#elif defined(__x86_64__) || defined(__i386__)
#include <cpuid.h>
#endif
#ifndef _WIN32
extern char** environ;
@@ -46,6 +55,9 @@ struct Session {
double gpuBusyAtOpen = 0;
std::optional<TraceClockSample> gpuClock;
std::vector<Clock::time_point> presents;
bool bench = false;
TraceBenchIntervals benchIntervals;
std::uint64_t benchDropped = 0;
};
Session& session() {
@@ -63,6 +75,11 @@ std::unique_ptr<ProfileOutput>& summaryWriter() {
return output;
}
std::unique_ptr<TraceBenchWriter>& benchWriter() {
static std::unique_ptr<TraceBenchWriter> output;
return output;
}
std::filesystem::path traceDirectory() {
const char* directory = std::getenv("APS5_TRACE_DIR");
return std::filesystem::path(directory != nullptr ? directory : ".");
@@ -91,6 +108,64 @@ std::string operatingSystem() {
#endif
}
std::optional<std::string> processorName() {
std::array<std::uint32_t, 12> words{};
#if defined(_MSC_VER) && !defined(__clang__)
int registers[4]{};
__cpuid(registers, static_cast<int>(0x80000000u));
if (static_cast<std::uint32_t>(registers[0]) < 0x80000004u) return std::nullopt;
for (unsigned leaf = 0; leaf < 3; ++leaf) {
__cpuid(registers, static_cast<int>(0x80000002u + leaf));
std::memcpy(words.data() + leaf * 4, registers, sizeof registers);
}
#elif defined(__x86_64__) || defined(__i386__)
if (__get_cpuid_max(0x80000000u, nullptr) < 0x80000004u) return std::nullopt;
for (unsigned leaf = 0; leaf < 3; ++leaf) {
unsigned eax = 0, ebx = 0, ecx = 0, edx = 0;
if (__get_cpuid(0x80000002u + leaf, &eax, &ebx, &ecx, &edx) == 0) throw std::runtime_error("cpuid brand string leaf failed");
words[leaf * 4] = eax;
words[leaf * 4 + 1] = ebx;
words[leaf * 4 + 2] = ecx;
words[leaf * 4 + 3] = edx;
}
#else
return std::nullopt;
#endif
std::string name(reinterpret_cast<const char*>(words.data()), sizeof words);
name.resize(std::strlen(name.c_str()));
const auto first = name.find_first_not_of(' ');
if (first == std::string::npos) return std::nullopt;
return name.substr(first, name.find_last_not_of(' ') - first + 1);
}
std::uint64_t memoryMiB() {
#ifdef _WIN32
MEMORYSTATUSEX status{};
status.dwLength = sizeof(status);
if (::GlobalMemoryStatusEx(&status) == 0) throw std::system_error(static_cast<int>(::GetLastError()), std::system_category(), "GlobalMemoryStatusEx");
return status.ullTotalPhys / (1024 * 1024);
#else
const long pages = ::sysconf(_SC_PHYS_PAGES);
const long size = ::sysconf(_SC_PAGE_SIZE);
if (pages <= 0 || size <= 0) throw std::system_error(errno, std::generic_category(), "sysconf physical memory");
return static_cast<std::uint64_t>(pages) * static_cast<std::uint64_t>(size) / (1024 * 1024);
#endif
}
const std::optional<TraceHost>& host() {
static const auto value = []() -> std::optional<TraceHost> {
auto cpu = processorName();
if (!cpu) return std::nullopt;
return TraceHost{std::move(*cpu), std::thread::hardware_concurrency(), memoryMiB()};
}();
return value;
}
std::optional<TraceTitle> title() {
if (!HasAppParamJson_nid_no_patch()) return std::nullopt;
return TraceTitle{GetAppTitleId_nid_postfix().value, GetAppTitle_nid_postfix().value, GetAppContentVersion_nid_no_patch().value};
}
std::vector<std::pair<std::string, std::string>> environment() {
#ifdef _WIN32
char** entries = _environ;
@@ -120,6 +195,9 @@ TraceSummary identity(std::uint32_t width, std::uint32_t height, const VulkanDev
summary.os = operatingSystem();
summary.width = width;
summary.height = height;
summary.host = host();
static const auto running = title();
summary.title = running;
return summary;
}
@@ -185,6 +263,23 @@ void PerformanceTrace::FlushInterval(Clock::time_point now, std::uint32_t width,
summary.gpuBusyMs = VulkanDevice::PresentCounts().gpuBusyMs - state.intervalGpuBusy;
summary.interval = std::make_pair(seconds(state.intervalPresents.front() - state.first), seconds(now - state.first));
summaryWriter()->Write(TraceFormat::Summary(summary, snapshots, state.intervalPresents));
if (!state.bench) return;
TraceBench::Keep(state.benchIntervals, std::make_shared<const TraceBenchInterval>(TraceBenchInterval{summary.interval->first, summary.interval->second, summary.firstPresent, width, height, summary.gpuBusyMs, TraceFormat::FrameMicroseconds(state.intervalPresents)}), state.benchDropped);
if (!benchWriter()) {
const auto name = "aps5-bench-" + summary.commit + "-" + std::to_string(processId()) + ".json.gz";
benchWriter() = std::make_unique<TraceBenchWriter>([directory = traceDirectory(), name](const TraceBenchSnapshot& snapshot) {
try {
std::filesystem::create_directories(directory);
TraceFile::ReplaceGzip(directory / name, TraceFormat::Bench(snapshot.summary, snapshot.intervals, snapshot.dropped));
} catch (const std::exception& error) {
std::fprintf(stderr, "[trace] writing %s failed: %s\n", name.c_str(), error.what());
std::fflush(stderr);
throw;
}
});
ProfilePrint_nid_no_patch("[trace] benchmark frames go to %s, rewritten every interval\n", name.c_str());
}
benchWriter()->Write(TraceBenchSnapshot{std::move(summary), state.benchIntervals, state.benchDropped});
}
void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const VulkanDevice* device) {
@@ -195,6 +290,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const
if (!state.started) {
state.window = TraceWindow::Parse(std::getenv("APS5_TRACE_FRAMES"), std::getenv("APS5_TRACE_SECONDS"));
state.interval = TraceInterval::Parse(std::getenv("APS5_TRACE_SUMMARY_EVERY"));
state.bench = TraceBench::Parse(std::getenv("APS5_TRACE_BENCH"), state.interval.has_value());
state.first = now;
state.started = true;
}
@@ -287,6 +383,7 @@ void PerformanceTrace::Present(std::uint32_t width, std::uint32_t height, const
void PerformanceTrace::StopOutput() {
if (writer()) writer()->Stop();
if (summaryWriter()) summaryWriter()->Stop();
if (benchWriter()) benchWriter()->Stop();
}
}
@@ -943,15 +943,29 @@ void NoteDispatchRegisters(std::span<const std::uint32_t> packet, QueueState& qu
}
}
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue) {
bool IndirectRegistersValid(std::span<const std::uint32_t> pairs) {
for (std::size_t i = 0; i < pairs.size(); i += 2) {
if (pairs[i] == 0xffffffffu || (pairs[i] & ~0x70000000u) > 0xffffu) return false;
}
return true;
}
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue, bool skipInvalid) {
const auto opcode = (packet[0] >> 8u) & 0xffu;
require(packet.size() == 5 && IndirectRegisterOpcode(opcode), "expected indirect register packet");
require(pairs.size() == static_cast<std::size_t>(packet[4]) * 2, "indirect register list does not match its packet");
for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]);
for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
const auto usable = [&](std::uint32_t value) { return !skipInvalid || IndirectRegistersValid(std::span(&value, 1)); };
for (std::size_t i = 0; i < pairs.size(); i += 2) {
if (usable(pairs[i])) registerOffset(pairs[i]);
}
for (std::size_t i = 0; i < pairs.size(); i += 2) {
if (usable(pairs[i])) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
}
if (queue.savedContext.has_value() && TraceContextState()) {
std::fprintf(stderr, "[context] indirect 0x%x:", opcode);
for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
for (std::size_t i = 0; i < pairs.size(); i += 2) {
if (usable(pairs[i])) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
}
std::fprintf(stderr, "\n");
}
}
@@ -0,0 +1,77 @@
#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp"
#include "Decoder/Png.hpp"
#include "SDL_filesystem.h"
#include "SDL_error.h"
#include <atomic>
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
std::atomic<bool> requested{false};
std::filesystem::path directory() {
const char* configured = std::getenv("ANYPS5_SCREENSHOTS");
if (configured != nullptr && configured[0] != '\0') return configured;
const std::unique_ptr<char, decltype(&SDL_free)> basePath(SDL_GetBasePath(), SDL_free);
if (!basePath) throw std::runtime_error(std::string("Screenshot: cannot locate the executable: ") + SDL_GetError());
return std::filesystem::path(reinterpret_cast<const char8_t*>(basePath.get())) / "anyps5-screenshots";
}
std::string localTimestamp() {
const std::time_t now = std::time(nullptr);
std::tm local{};
#ifdef _WIN32
if (localtime_s(&local, &now) != 0) throw std::runtime_error("Screenshot: cannot read the local time");
#else
if (localtime_r(&now, &local) == nullptr) throw std::runtime_error("Screenshot: cannot read the local time");
#endif
char text[32];
if (std::strftime(text, sizeof(text), "%Y%m%d-%H%M%S", &local) == 0) throw std::runtime_error("Screenshot: cannot format the local time");
return text;
}
std::filesystem::path unusedPath(const std::filesystem::path& folder, const std::string& stem) {
for (int index = 0; index < 1000; ++index) {
const auto path = folder / (index == 0 ? stem + ".png" : stem + "-" + std::to_string(index) + ".png");
if (!std::filesystem::exists(path)) return path;
}
throw std::runtime_error("Screenshot: no unused file name for " + stem + " in " + folder.string());
}
}
void AgcDriver::Screenshot::Request() {
requested.store(true);
}
bool AgcDriver::Screenshot::Take() {
return requested.exchange(false);
}
std::filesystem::path AgcDriver::Screenshot::Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span<const std::byte> bgra) {
if (titleId.empty()) throw std::invalid_argument("Screenshot: empty title ID");
const auto pixelCount = static_cast<std::size_t>(width) * height;
if (width == 0 || height == 0 || bgra.size() != pixelCount * 4) throw std::invalid_argument("Screenshot: invalid BGRA8 frame");
std::vector<std::uint8_t> rgb(pixelCount * 3);
for (std::size_t pixel = 0; pixel < pixelCount; ++pixel) {
rgb[pixel * 3] = std::to_integer<std::uint8_t>(bgra[pixel * 4 + 2]);
rgb[pixel * 3 + 1] = std::to_integer<std::uint8_t>(bgra[pixel * 4 + 1]);
rgb[pixel * 3 + 2] = std::to_integer<std::uint8_t>(bgra[pixel * 4]);
}
const auto png = Decoder::Png::Encode(rgb, width, height, 3);
const auto folder = directory();
std::filesystem::create_directories(folder);
const auto path = unusedPath(folder, std::string(titleId) + "-" + localTimestamp());
std::ofstream file;
file.exceptions(std::ios::failbit | std::ios::badbit);
file.open(path, std::ios::binary);
file.write(reinterpret_cast<const char*>(png.data()), static_cast<std::streamsize>(png.size()));
file.close();
return path;
}
@@ -6,7 +6,11 @@
#include <fstream>
#include <stdexcept>
#include <string>
#include <system_error>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#endif
namespace AgcDriver {
@@ -50,6 +54,22 @@ void TraceFile::WriteGzip(const std::filesystem::path& path, std::string_view te
output.close();
}
void TraceFile::ReplaceGzip(const std::filesystem::path& path, std::string_view text) {
auto temporary = path;
temporary += ".tmp";
WriteGzip(temporary, text);
std::error_code error;
#ifdef _WIN32
if (::MoveFileExW(temporary.c_str(), path.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) == 0) error = std::error_code(static_cast<int>(::GetLastError()), std::system_category());
#else
std::filesystem::rename(temporary, path, error);
#endif
if (!error) return;
std::error_code removeError;
std::filesystem::remove(temporary, removeError);
throw std::system_error(error, "replacing " + path.string());
}
void TraceFile::AppendLine(const std::filesystem::path& path, std::string_view line) {
std::ofstream output;
output.exceptions(std::ios::badbit | std::ios::failbit);
@@ -24,6 +24,8 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceOverlay.hpp"
#include "prx/libSceAgcDriver/Execution/include/PresentationPass.hpp"
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp"
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
@@ -155,24 +157,30 @@ public:
if (worker.joinable()) worker.join();
}
void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, std::vector<std::byte> pixels) {
void Enqueue(int index, std::uint32_t width, std::uint32_t height, std::uint32_t scale, bool screenshot, std::vector<std::byte> pixels) {
std::lock_guard lock(mutex);
require(!stopping, "frame dump writer has stopped");
require(pixels.size() <= maxBytes - queuedBytes, "frame dump queue exceeded 64 MiB");
if (!frameDumpOutput.Enabled_nid_no_patch()) return;
if (!screenshot && !frameDumpOutput.Enabled_nid_no_patch()) return;
if (!worker.joinable()) worker = std::thread([this] { run(); });
const auto bytes = pixels.size();
pending.push_back({index, width, height, scale, std::move(pixels)});
pending.push_back({index, width, height, scale, screenshot, std::move(pixels)});
queuedBytes += bytes;
changed.notify_one();
}
bool Idle() {
std::lock_guard lock(mutex);
return queuedBytes == 0;
}
private:
struct Frame {
int index;
std::uint32_t width;
std::uint32_t height;
std::uint32_t scale;
bool screenshot;
std::vector<std::byte> pixels;
};
@@ -187,7 +195,8 @@ private:
batch.swap(pending);
}
for (const auto& frame : batch) {
WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale);
if (frame.screenshot) std::fprintf(stderr, "[gpu] screenshot saved: %s\n", Screenshot::Write(GetAppTitleId_nid_postfix().value, frame.width, frame.height, frame.pixels).string().c_str());
else WriteFrameBmp(frame.index, frame.width, frame.height, frame.pixels, frame.scale);
std::lock_guard lock(mutex);
queuedBytes -= frame.pixels.size();
}
@@ -449,6 +458,7 @@ struct VulkanDevice::State {
std::uint32_t dumpHeight = 0;
std::uint32_t dumpScale = 1;
bool dumpRecorded = false;
bool dumpScreenshot = false;
int dumpIndex = 0;
std::uint32_t imageIndex = 0;
bool inFlight = false;
@@ -480,6 +490,7 @@ struct VulkanDevice::State {
bool queuePending = false;
std::uint32_t queueIndex = 0;
int nextDumpIndex = 0;
bool nextDumpScreenshot = false;
FrameDumpWriter dumpWriter;
template<typename TFunction>
@@ -678,6 +689,11 @@ struct VulkanDevice::State {
require(slot.dumpBuffer != nullptr, "frame dump was not recorded");
const auto full = slot.dumpBuffer->Bytes();
const auto index = slot.dumpIndex;
if (slot.dumpScreenshot) {
require(full.size() == static_cast<std::size_t>(slot.dumpWidth) * slot.dumpHeight * 4, "invalid screenshot pixels");
dumpWriter.Enqueue(index, slot.dumpWidth, slot.dumpHeight, 1, true, std::vector<std::byte>(full.begin(), full.end()));
return;
}
const auto scale = slot.dumpScale;
require(scale != 0 && full.size() == static_cast<std::size_t>(slot.dumpWidth) * slot.dumpHeight * 4, "invalid frame dump pixels");
const auto width = (slot.dumpWidth + scale - 1) / scale;
@@ -688,7 +704,7 @@ struct VulkanDevice::State {
}
static const auto start = std::chrono::steady_clock::now();
std::fprintf(stderr, "[gpu] frame_%03d.bmp: %ux%u display read back on the GPU at %.1f s\n", index, slot.dumpWidth, slot.dumpHeight, std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count());
dumpWriter.Enqueue(index, width, height, 1, std::move(pixels));
dumpWriter.Enqueue(index, width, height, 1, false, std::move(pixels));
}
// Blocks on the fence, or polls it every 50 us with APS5_PRESENT_POLL_FENCE=1 (the difference
@@ -1485,7 +1501,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
VkPhysicalDeviceExtendedDynamicStateFeaturesEXT dynamicStateFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT};
VkPhysicalDeviceDynamicRenderingFeaturesKHR renderingFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR};
constexpr std::array<const char*, 6> libraryExtensions{VK_KHR_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_GRAPHICS_PIPELINE_LIBRARY_EXTENSION_NAME, VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME, VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_DEPTH_STENCIL_RESOLVE_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME};
if (std::getenv("APS5_NO_GPL") == nullptr && std::all_of(libraryExtensions.begin(), libraryExtensions.end(), hasExtension)) {
bool libraryDevice = false;
if (std::all_of(libraryExtensions.begin(), libraryExtensions.end(), hasExtension)) {
dynamicStateFeatures.pNext = &renderingFeatures;
libraryFeatures.pNext = &dynamicStateFeatures;
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &libraryFeatures};
@@ -1493,8 +1510,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
VkPhysicalDeviceGraphicsPipelineLibraryPropertiesEXT libraryProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_PROPERTIES_EXT};
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &libraryProperties};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
state->graphicsPipelineLibrary = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && renderingFeatures.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= Graphics::PipelinePushConstantBytes;
if (state->graphicsPipelineLibrary) deviceExtensions.insert(deviceExtensions.end(), libraryExtensions.begin(), libraryExtensions.end());
libraryDevice = libraryFeatures.graphicsPipelineLibrary == VK_TRUE && dynamicStateFeatures.extendedDynamicState == VK_TRUE && renderingFeatures.dynamicRendering == VK_TRUE && libraryProperties.graphicsPipelineLibraryFastLinking == VK_TRUE && properties.properties.limits.maxPushConstantsSize >= Graphics::PipelinePushConstantBytes;
if (libraryDevice) deviceExtensions.insert(deviceExtensions.end(), libraryExtensions.begin(), libraryExtensions.end());
state->graphicsPipelineLibrary = libraryDevice && std::getenv("APS5_NO_GPL") == nullptr;
if (state->graphicsPipelineLibrary && 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};
@@ -1720,7 +1738,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
depthClipFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &depthClipFeatures;
}
if (state->graphicsPipelineLibrary) {
if (libraryDevice) {
renderingFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &libraryFeatures;
}
@@ -2223,6 +2241,10 @@ bool VulkanDevice::OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) c
return state->recorder && state->recorder->OpenWriteOverlaps(address, bytes);
}
void VulkanDevice::SealPassWriteOnly() {
if (state->recorder) state->recorder->SealPassWriteOnly();
}
bool VulkanDevice::RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const {
if (!state->recorder) return true;
const auto& recorder = *state->recorder;
@@ -2518,44 +2540,63 @@ std::atomic<std::uint64_t> copiesAliased{0}, copyAliasCreated{0}, copyAliasNoSou
// source range must be exactly a live image's surface, and the destination range either exactly
// a live image of the same shape or none (one is made from the source's descriptor rebased). Under
// GuestMemory::GpuMutex. Debug aid: APS5_NO_COPY_ALIAS=1 keeps every such copy a transfer.
bool AliasCopy(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) {
bool CopyAliasDisabled() {
static const bool disabled = std::getenv("APS5_NO_COPY_ALIAS") != nullptr;
if (disabled) return false;
auto from = Graphics::StorageTexture::FindLive(source, bytes);
if (from == nullptr) {
return disabled;
}
struct AliasPlan {
std::shared_ptr<Graphics::StorageTexture> from;
std::shared_ptr<Graphics::StorageTexture> to;
const char* refusal = nullptr;
};
AliasPlan PlanAlias(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) {
AliasPlan plan;
plan.from = Graphics::StorageTexture::FindLive(source, bytes);
if (plan.from == nullptr) {
plan.refusal = "no source";
return plan;
}
plan.to = Graphics::StorageTexture::FindLive(destination, bytes);
plan.refusal = Graphics::StorageTexture::CopyRefusal(context, plan.to.get(), *plan.from, destination);
return plan;
}
bool AliasCopy(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) {
if (CopyAliasDisabled()) return false;
auto plan = PlanAlias(context, destination, source, bytes);
if (plan.from == nullptr) {
copyAliasNoSource.fetch_add(1, std::memory_order_relaxed);
return false;
}
auto to = Graphics::StorageTexture::FindLive(destination, bytes);
if (to != nullptr && !to->SameSurfaceShape(*from)) {
copyAliasShape.fetch_add(1, std::memory_order_relaxed);
if (plan.refusal != nullptr) {
if (plan.to != nullptr && !plan.to->SameSurfaceShape(*plan.from)) {
copyAliasShape.fetch_add(1, std::memory_order_relaxed);
} else {
static std::atomic<int> reported{0};
if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] image copy 0x%llx -> 0x%llx (0x%zx bytes) refused: %s\n", static_cast<unsigned long long>(source), static_cast<unsigned long long>(destination), bytes, plan.refusal);
copyAliasRefused.fetch_add(1, std::memory_order_relaxed);
}
return false;
}
if (to == nullptr) {
auto resource = from->Descriptor();
if (plan.to == nullptr) {
auto resource = plan.from->Descriptor();
resource.baseAddress = destination;
resource.dccAddress = 0;
try {
to = Graphics::CachedStorageSurface(context, resource);
} catch (const std::exception& error) {
static std::atomic<int> reported{0};
if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] no storage image for the copy destination 0x%llx: %s\n", static_cast<unsigned long long>(destination), error.what());
copyAliasRefused.fetch_add(1, std::memory_order_relaxed);
return false;
}
plan.to = Graphics::CachedStorageSurface(context, resource);
copyAliasCreated.fetch_add(1, std::memory_order_relaxed);
}
const char* refusal = nullptr;
if (!to->CopyFrom(*from, refusal)) {
static std::atomic<int> reported{0};
if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] image copy 0x%llx -> 0x%llx (0x%zx bytes) refused: %s\n", static_cast<unsigned long long>(source), static_cast<unsigned long long>(destination), bytes, refusal);
copyAliasRefused.fetch_add(1, std::memory_order_relaxed);
return false;
}
if (!plan.to->CopyFrom(*plan.from, refusal)) throw std::runtime_error(std::string("AGC graphics: an image copy its check allowed was refused: ") + refusal);
copiesAliased.fetch_add(1, std::memory_order_relaxed);
return true;
}
bool AliasCopyPossible(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) {
return !CopyAliasDisabled() && PlanAlias(context, destination, source, bytes).refusal == nullptr;
}
}
VulkanDevice::CopyVerification VulkanDevice::CopyVerifyCounts() {
@@ -2575,21 +2616,34 @@ VulkanDevice::CopyAliasing VulkanDevice::CopyAliasCounts() {
return {copiesAliased.load(std::memory_order_relaxed), copyAliasCreated.load(std::memory_order_relaxed), copyAliasNoSource.load(std::memory_order_relaxed), copyAliasShape.load(std::memory_order_relaxed), copyAliasRefused.load(std::memory_order_relaxed)};
}
bool VulkanDevice::completionStoresOver(std::uint64_t address, std::size_t bytes) const {
static const bool alwaysSync = std::getenv("APS5_COPY_SYNC") != nullptr;
if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true;
return Graphics::Recorder::PendingCompletionLabels() != 0 && state->recorder->CompletionLabelIn(address, bytes);
}
bool VulkanDevice::copyWaitsOnCompletions(std::uint64_t address, std::size_t bytes) const {
return state->recorder->PendingWriteOverlaps(address, bytes) && completionStoresOver(address, bytes);
}
bool VulkanDevice::GpuCopyPossible(std::uint64_t destination, std::uint64_t source, std::size_t bytes) const {
if (!state->recorder || bytes == 0 || copyWaitsOnCompletions(source, bytes) || copyWaitsOnCompletions(destination, bytes)) return false;
const auto context = graphicsContext();
if (AliasCopyPossible(context, destination, source, bytes)) return true;
return Graphics::HostImportFor(context, destination, bytes) != nullptr && Graphics::HostImportFor(context, source, bytes) != nullptr;
}
VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, std::uint64_t source, std::size_t bytes, std::size_t cpuMax, std::size_t gpuMax, std::size_t knownMax, std::uint64_t programAddress, std::uint32_t queue, const CopyWriterNote& noteWriter) {
CopyOutcome outcome{2, false, 0, CopyOutcome::None, false, false, 0, 0, 0, 0, false};
if (!state->recorder || bytes == 0) return outcome;
auto& recorder = *state->recorder;
// Finished batches were retired by the caller (Driver.cpp copyBuffer, on the graphics worker
// only, as WriteLabelOnGpu reaps), so their writes neither refuse the CPU copy nor force a wait.
static const bool alwaysSync = std::getenv("APS5_COPY_SYNC") != nullptr;
static const bool waitForSource = std::getenv("APS5_COPY_WAIT_SOURCE") != nullptr;
static const bool verify = std::getenv("APS5_COPY_VERIFY") != nullptr;
// A store a batch's completion makes on the CPU over the range (a copied buffer's write-back,
// a completion-deferred label): it lands only when that batch is reaped, whatever its fence.
const auto completionStoreOverlaps = [&](std::uint64_t address) {
if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true;
return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes);
};
const auto completionStoreOverlaps = [&](std::uint64_t address) { return completionStoresOver(address, bytes); };
// The CPU decision (see the header): pure queries, in the order that makes the first failing
// test the reason; a signaled writer of the source is fine (its in-place stores are final in
// host memory) as long as no completion store or label of it is still to land.
@@ -2667,8 +2721,8 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st
// before the transfer (or the device copy below) by its barrier; a store a batch's completion
// makes on the CPU must land before the copy reads the source or writes the destination, so
// the copy waits for that batch (SyncThrough) first.
const bool waitSource = recorder.PendingWriteOverlaps(source, bytes) && completionStoreOverlaps(source);
const bool waitDestination = recorder.PendingWriteOverlaps(destination, bytes) && completionStoreOverlaps(destination);
const bool waitSource = copyWaitsOnCompletions(source, bytes);
const bool waitDestination = copyWaitsOnCompletions(destination, bytes);
if (waitSource || waitDestination) {
outcome.synced = true;
const auto syncStart = std::chrono::steady_clock::now();
@@ -3028,6 +3082,13 @@ std::optional<std::array<std::byte, 4>> CompressedClearPixel(const DisplayBuffer
return DisplayBufferClearPixel(buffer, keys);
}
std::vector<std::byte> DisplayPixels(const DisplayBuffer& buffer, const std::optional<std::array<std::byte, 4>>& cleared) {
if (!cleared) return ReadDisplayBuffer(buffer);
std::vector<std::byte> pixels(static_cast<std::size_t>(buffer.width) * buffer.height * cleared->size());
for (std::size_t offset = 0; offset < pixels.size(); offset += cleared->size()) std::memcpy(pixels.data() + offset, cleared->data(), cleared->size());
return pixels;
}
// Debug aid: APS5_NO_RESIDENT_PRESENT=1 always presents through guest memory.
bool NoResidentPresent() {
static const bool no = std::getenv("APS5_NO_RESIDENT_PRESENT") != nullptr;
@@ -3079,10 +3140,14 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
dumpFrame = true;
}
}
if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame, false, nullptr, 0, passthrough)) {
const bool screenshot = takeScreenshot(dumpFrame);
auto shot = screenshot && passthrough ? ReadDisplayBuffer(buffer) : std::vector<std::byte>{};
if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame || (screenshot && !passthrough), false, nullptr, 0, passthrough)) {
if (dumpFrame) --dumps.dumped;
if (screenshot) Screenshot::Request();
return false;
}
if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot));
return true;
}
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
@@ -3128,9 +3193,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
bool dumpFrame = false;
if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast<std::uint64_t>(dumps.every) == 0) {
if (dumps.cpu || (passthrough && resident == nullptr)) {
auto full = cleared ? std::vector<std::byte>(static_cast<std::size_t>(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer);
for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size());
WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale());
WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, DisplayPixels(buffer, cleared), DumpScale());
} else {
state->nextDumpIndex = dumps.dumped++;
dumpFrame = true;
@@ -3141,6 +3204,9 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
lastReport = std::chrono::steady_clock::now();
AgcDriver::ProfilePrint_nid_no_patch("[flip] (cumulative): %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast<unsigned long long>(residentPresents), static_cast<unsigned long long>(refreshedPresents), static_cast<unsigned long long>(notPending), static_cast<unsigned long long>(unsuitable), static_cast<unsigned long long>(gpuDumps));
}
const bool screenshot = takeScreenshot(dumpFrame);
const bool cpuScreenshot = screenshot && passthrough && resident == nullptr;
auto shot = cpuScreenshot ? DisplayPixels(buffer, cleared) : std::vector<std::byte>{};
CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast<unsigned long long>(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast<unsigned long long>(resident ? resident->Generation() : 0));
if (passthrough && resident == nullptr) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat));
VkClearColorValue uniform{};
@@ -3150,14 +3216,21 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
const auto channel = [&](std::size_t index) { return static_cast<float>(std::to_integer<unsigned>((*cleared)[index])) / 255.0f; };
uniform = {{channel(2), channel(1), channel(0), channel(3)}};
}
if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr, layer, passthrough)) {
if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame || (screenshot && !cpuScreenshot), convert, cleared ? &uniform : nullptr, layer, passthrough)) {
// A dropped frame (swapchain out of date) keeps the dump numbering contiguous.
if (dumpFrame) --dumps.dumped;
if (screenshot) Screenshot::Request();
return false;
}
if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot));
return true;
}
bool VulkanDevice::takeScreenshot(bool dumpFrame) {
state->nextDumpScreenshot = !dumpFrame && state->dumpWriter.Idle() && Screenshot::Take();
return state->nextDumpScreenshot;
}
bool VulkanDevice::AcquireImage() {
PerformanceTimer timing("Vulkan.Acquire");
require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain");
@@ -3331,7 +3404,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu
else scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height);
}
if (dumpFrame) {
const auto scale = DumpScale();
const auto scale = state->nextDumpScreenshot ? 1 : DumpScale();
if (drawn || direct) {
const auto extent = PresentationPass::DumpExtent(width, height, scale);
slot.dumpWidth = extent.width;
@@ -3399,6 +3472,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu
slot.shared = shared;
// Only a submitted readback is written after the fence (a failed submit tears the device down).
slot.dumpRecorded = dumpFrame;
slot.dumpScreenshot = dumpFrame && state->nextDumpScreenshot;
slot.dumpIndex = state->nextDumpIndex;
slot.recorderSerial = batchesAtBlit;
slot.previousSerial = state->lastPresentSerial;
@@ -3799,6 +3873,10 @@ void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParamete
if (trace) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete");
}
bool VulkanDevice::DrawInputsInPlace() const {
return state->drawInputsInPlace;
}
namespace {
// The resource cache serves dispatches unless disabled; Revalidate proves a hit by texture identity,
@@ -79,6 +79,11 @@ struct DescriptorRejected : std::runtime_error {
using std::runtime_error::runtime_error;
};
struct BdaUnalignedAccess : std::runtime_error {
BdaUnalignedAccess(const std::string& message, std::uint32_t instruction) : std::runtime_error(message), instruction(instruction) {}
std::uint32_t instruction;
};
// Device entry points resolved once per device (VulkanDevice's State fills it after setup): the
// loader's vkGetDeviceProcAddr is a name lookup under its global mutex per call, paid at every
// record site otherwise. Null members (tests, APS5_NO_PROC_TABLE=1) resolve per call (Resolved).
@@ -246,6 +246,7 @@ struct MirrorStats {
std::uint64_t sparseBinds = 0;
};
MirrorStats MirrorCounters();
std::uint64_t SnapshotMismatches();
void ClearSnapshotCopies(VkDevice device);
std::vector<std::uint64_t> AddressTableKey(std::span<const std::pair<std::uint64_t, std::uint64_t>> writes, std::span<const ShaderRecompiler::BdaAbi::Range> own);
void ClearImageMirrors(VkDevice device);
@@ -70,6 +70,7 @@ void ClearMultisampleTargets(VkDevice device);
void InstallResidentImageReadGuard_nid_no_patch();
void UpdateResidentImageUnmapObserver_nid_no_patch();
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access);
void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access);
}
@@ -22,6 +22,7 @@ struct PassAccess {
std::span<const std::pair<VkImage, bool>> attachments;
bool anyReads = false;
bool anyWrites = false;
GuestRangeList writeOnly = {};
};
enum class PassHazard : std::uint8_t { None, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite };
@@ -32,6 +33,7 @@ public:
bool Overlaps(std::uint64_t begin, std::uint64_t end) const;
bool Empty() const { return ranges.empty(); }
std::size_t Size() const { return ranges.size(); }
GuestRangeList Ranges() const { return ranges; }
void Clear() { ranges.clear(); }
private:
@@ -43,6 +45,7 @@ public:
PassHazard Check(const PassAccess& access, bool samePass) const;
void Add(const PassAccess& access);
void BeginPass() { ++pass; }
void SealWriteOnly();
void Clear();
bool Empty() const;
@@ -55,6 +58,7 @@ private:
bool overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) const;
GuestRangeSet reads;
GuestRangeSet writes;
GuestRangeSet writeOnly;
std::vector<std::pair<VkImage, bool>> images;
std::vector<Attachment> attachments;
std::uint64_t pass = 0;
@@ -69,6 +69,7 @@ public:
};
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 = {});
void SealPassWriteOnly();
// DCC "uncompressed" key stores (DccMetadata.cpp StoreUncompressedOnGpu): queued on the open
// batch and recorded as one run (one barrier pair for every queued fill) at Submit, before a
// label store (RecordStore), or before a command that writes or reads a queued range (the
@@ -196,6 +197,7 @@ public:
std::optional<PendingWriteInfo> DescribePendingWrite(std::uint64_t address, std::size_t bytes) const;
// Ends and submits the open batch without waiting.
void Submit();
void HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value);
// Submits the open batch and returns the serial of the newest submitted batch (0 when nothing was
// ever submitted); WaitSerial(serial) then covers all recorded work.
std::uint64_t SubmitAndEpoch();
@@ -804,6 +806,8 @@ private:
Context context;
VkSemaphore timeline = VK_NULL_HANDLE;
VkSemaphore submitHold = VK_NULL_HANDLE;
std::uint64_t submitHoldValue = 0;
// Identity for a thread that released the mutex around a wait (syncThroughUnlocked): the
// recorder may have been torn down meanwhile, so it is looked up by this id, not by pointer.
std::uint64_t id = 0;
@@ -42,6 +42,7 @@ struct TextureCacheUse {
std::uint64_t storageBytes = 0;
};
TextureCacheUse TextureCacheUsage();
void OverrideCacheBudgetForTest(std::uint64_t bytes);
std::uint64_t SampledTextureCacheBudget(const Context& context);
std::shared_ptr<Texture> CachedSampledTexture(const Context& context, std::span<const std::uint32_t> words, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D);
bool SampledTexturesShareEntry(std::span<const std::uint32_t> first, std::span<const std::uint32_t> second);
@@ -233,6 +234,7 @@ public:
// The recorder's hazard tracker inputs (Recorder::NoteAccess): the written elements' guest
// ranges, the regions read in place, and the storage images with whether each is written.
const std::vector<std::pair<std::uint64_t, std::uint64_t>>& GpuWrites() const { return guestMemory.Writes(); }
const std::vector<std::pair<std::uint64_t, std::uint64_t>>& WriteOnlyRanges() const { return writeOnlyRanges; }
std::vector<std::pair<std::uint64_t, std::uint64_t>> InPlaceReads() const { return guestMemory.InPlaceReads(); }
void InPlaceReads(std::vector<std::pair<std::uint64_t, std::uint64_t>>& out) const { guestMemory.InPlaceReads(out); }
std::vector<std::pair<VkImage, bool>> StorageImages() const;
@@ -305,7 +307,7 @@ public:
bool DataWordsDiffer(const CompiledShader& shader) const;
// Why the fast proof of a Revalidate left the object to the full walk (the [rescache]
// revalidate line's reasons); Count: it did not.
enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Count };
enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count };
// Why a Pending failure was left to the full walk instead of the own-object refresh (T1, see
// refreshOwnObjects); Count: it was not.
enum class OwnRefreshFallback : std::size_t { Disabled, Snapshot, Keys, ForeignView, SurfaceKey, NotImported, Uncached, Rerun, Count };
@@ -343,6 +345,7 @@ private:
std::uint32_t format = 0;
int tileMode = 0;
std::uint64_t dccAddress = 0;
bool imageTable = false;
};
std::vector<DescribedRange> describedRanges;
struct Allocation {
@@ -353,6 +356,7 @@ private:
ShaderRecompiler::DescriptorRole role = ShaderRecompiler::DescriptorRole::ShaderData;
// Guest buffers: whether the shader may store to the range (see addGuestBuffer).
bool written = true;
bool writeOnly = false;
// Data buffers: the words the buffer holds once the recorded work ran (see RefreshData).
std::vector<std::uint32_t> dataWords;
std::uint32_t adjustment = 0;
@@ -588,6 +592,8 @@ private:
// Guest buffer elements bound read-only: each use of this object skips that many pending-write
// notes (counted in MarkGpuWrites for the [buffers] line).
std::size_t readOnlyBuffers = 0;
std::chrono::steady_clock::time_point lastFullWalk = std::chrono::steady_clock::now();
std::vector<std::pair<std::uint64_t, std::uint64_t>> writeOnlyRanges;
bool drawBuild = false;
std::uint64_t dispatchThreads = 0;
std::vector<std::shared_ptr<Texture>> textures;
@@ -339,6 +339,7 @@ public:
// may carry DCC metadata, and this surface must be host-imported (the write-back is GPU-direct).
// False, naming why, when the copy must be a transfer.
bool CopyFrom(StorageTexture& source, const char*& refusal);
static const char* CopyRefusal(const Context& context, const StorageTexture* destination, const StorageTexture& source, std::uint64_t address);
VkImage Image() const { return image; }
const GuestTextureResource& Descriptor() const { return descriptor; }
std::uint32_t ImageLayers() const { return geometry.imageLayers; }
@@ -76,6 +76,7 @@ struct SurfaceGeometry {
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor);
bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels);
GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed);
GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers);
}
@@ -244,10 +244,10 @@ void BdaResources::CheckFault() const {
ShaderRecompiler::BdaAbi::Fault report{};
std::memcpy(&report, fault->Bytes().data(), sizeof(report));
if (report.state == ShaderRecompiler::BdaAbi::FaultState::Empty) {
Require(static_cast<std::uint32_t>(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, "BDA fault record has data without publication");
Require(static_cast<std::uint32_t>(report.reason) == 0 && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, "BDA fault record has data without publication");
return;
}
Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.reserved == 0, "incomplete or invalid BDA fault record");
Require(report.state == ShaderRecompiler::BdaAbi::FaultState::Ready && report.otherFaults <= 1, "incomplete or invalid BDA fault record");
Require(report.reason != ShaderRecompiler::BdaAbi::FaultReason::InvalidRectangle, "rect-list requires finite nondegenerate axis-aligned positions with equal positive W");
if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::LoopLimit) {
// APS5_LOOP_GUARD: the shader left a loop that ran past the guard; the dispatch result is kept.
@@ -293,6 +293,10 @@ void BdaResources::CheckFault() const {
break;
}
}
if (report.reason == ShaderRecompiler::BdaAbi::FaultReason::Unaligned) {
if (report.otherFaults == 0) throw BdaUnalignedAccess(message.str(), report.instruction);
message << "; a fault of another kind was recorded in the same dispatch";
}
throw std::runtime_error(message.str());
}
@@ -102,7 +102,7 @@ DepthSamplingState prepareDepthSampling(const Context& context, const DepthTarge
if ((target.pipeBankXor & ~0xcf00u) != 0u) return refuse("unsupported native multisample depth XOR");
if (target.address == 0u || (target.address & 65535u) != 0u) return refuse("native multisample depth base is not aligned");
if (target.stencilAddress != 0u || target.htileStencil) return refuse("native multisample stencil import is not implemented");
if (target.htileAddress == 0u || (target.htileAddress & 255u) != 0u || !target.iterate256 || !target.htilePipeAligned || target.htileVrsEncoding != 0u) return refuse("native multisample depth initial import requires pipe-aligned ITERATE_256 HTILE without VRS");
if (target.htileAddress == 0u || (target.htileAddress & 255u) != 0u || !target.htilePipeAligned || target.htileVrsEncoding != 0u) return refuse("native multisample depth initial import requires pipe-aligned HTILE without VRS");
if (target.clearDepth != 0.0f && target.clearDepth != 1.0f) return refuse("native multisample depth initial clear value is not implemented");
state.initial = {{{target.address, static_cast<std::size_t>(MultisampledDepthBytes_nid_no_patch(target.extent, target.format == VK_FORMAT_D16_UNORM ? 2u : 4u, target.samples)), 0u}, {target.htileAddress, static_cast<std::size_t>(HtileMetadataBytes(target.extent)), 0u}}};
for (const auto& range : state.initial) {
@@ -1880,6 +1880,11 @@ void captureInputs(const Context& context, Recorder& recorder, VkCommandBuffer c
});
}
bool WriteOnlyPasses() {
static const bool enabled = std::getenv("APS5_NO_WRITE_ONLY_PASSES") == nullptr;
return enabled;
}
void recordDraw(const Context& context, const State& state, const Pm4::DrawParameters& draw, std::span<const CompiledShader> shaders, DrawInputs& inputs, RecordedDraw& record, DrawOutcome& outcome, DrawTimer& timer, double& ownWaitedMs) {
auto* recorder = record.recorder;
auto& resources = *record.resources;
@@ -1919,6 +1924,7 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
auto& passReads = ShaderRecompiler::ThreadOwned(passReadsSlot);
passReads.clear();
if (!addressBased) resources.DeviceReads(passReads);
const auto deviceReads = passReads.size();
if (gpuIndirect) {
passReads.emplace_back(args->arguments, args->arguments + args->RangeBytes());
if (args->countIndirect) passReads.emplace_back(args->countAddress, args->countAddress + 4);
@@ -1936,7 +1942,29 @@ void recordDraw(const Context& context, const State& state, const Pm4::DrawParam
const bool readOnlyBda = addressReadPasses && resources.BdaReadOnly();
outcome.addressDraw = addressBased;
const bool unknownWrites = ((addressBased || resources.UsesAddressTable()) && !readOnlyBda) || resources.HasCopiedWrites();
const PassAccess passAccess{passReads, resources.GpuWrites(), passImages, passAttachments, unknownMemory, unknownWrites};
GuestRangeList passWrites = resources.GpuWrites();
GuestRangeList passWriteOnly{};
thread_local std::vector<std::pair<std::uint64_t, std::uint64_t>>* hazardWritesSlot = nullptr;
if (const auto& writeOnly = resources.WriteOnlyRanges(); !writeOnly.empty() && !unknownMemory && WriteOnlyPasses()) {
const auto listed = [&](const std::pair<std::uint64_t, std::uint64_t>& range, bool aligned) {
return std::any_of(writeOnly.begin(), writeOnly.end(), [&](const auto& only) {
if (!aligned) return only == range;
const auto begin = only.first & ~std::uint64_t{3};
const auto end = begin == only.first ? only.second : (only.second + 3) & ~std::uint64_t{3};
return range.first == begin && range.second == end;
});
};
const auto ownReads = passReads.begin() + static_cast<std::ptrdiff_t>(deviceReads);
passReads.erase(std::remove_if(passReads.begin(), ownReads, [&](const auto& range) { return listed(range, true); }), ownReads);
auto& hazardWrites = ShaderRecompiler::ThreadOwned(hazardWritesSlot);
hazardWrites.clear();
for (const auto& range : resources.GpuWrites()) {
if (!listed(range, false)) hazardWrites.push_back(range);
}
passWrites = hazardWrites;
passWriteOnly = writeOnly;
}
const PassAccess passAccess{passReads, passWrites, passImages, passAttachments, unknownMemory, unknownWrites, passWriteOnly};
const bool fenced = state.depth.has_value() || !record.proxies.empty();
const auto start = recorder->StartDrawPass(passKey, capture || readsTarget || gpuIndirect || meshIndirect, fenced, passAccess);
for (const auto& target : record.multisampleTargets) KeepMultisampleTarget_nid_no_patch(target);
@@ -2103,6 +2131,10 @@ void Draw(const Context& context, const State& state, const Pm4::DrawParameters&
Require(!draw.indirect && !state.primitiveRestart && !state.rectList && !state.quadList && !state.stages.mesh && !state.stages.tessellation && state.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, "stencil replay supports direct triangle lists without restart, mesh or tessellation");
Require(!state.depthBiasPerFace && !ShadersWriteMemory(shaders), "stencil replay refuses face splits and shader memory writes");
Require(std::ranges::all_of(shaders, [](const CompiledShader& shader) { return shader.program != nullptr && shader.program->stencilReplaySafe && (shader.stage == ShaderRecompiler::ShaderStage::Vertex || shader.stage == ShaderRecompiler::ShaderStage::Fragment); }), "stencil replay refuses stages with side effects, clocks, subgroup dependencies or primitive ID inputs");
if (stencilReferences.size() == 256u) {
static std::once_flag unbounded;
std::call_once(unbounded, [] { APS5_LOG_CHARS_OUT("Stencil replay: a draw replays all 256 stencil reference values because its pixel shader's export is not bounded, recording 256 passes per triangle and instance"); });
}
for (const auto& shader : shaders) {
if (shader.stage != ShaderRecompiler::ShaderStage::Vertex) continue;
Require((shader.program->hostSubgroupSize == 1u || shader.program->hostSubgroupSize == context.subgroup.subgroupSize) && shader.program->spirv.size() >= 5u && shader.program->spirv[1] >= 0x00010000u && shader.program->spirv[1] < 0x00010600u, "stencil replay vertex subgroup contract differs from the physical device");
@@ -170,6 +170,21 @@ struct ImportRefusal {
std::uint64_t budget;
};
struct EvictingRefusal {
std::uint64_t base;
std::uint64_t bytes;
GuestAllocations::Mapped ranges;
std::uint64_t epoch;
std::uint64_t liveBytes;
std::uint64_t budget;
std::uint64_t room;
std::uint64_t window;
std::uint64_t floor;
std::uint64_t ready;
std::uint64_t readyHeld;
bool joined;
};
#ifdef _WIN32
constexpr std::size_t settleBatchCount = 4;
constexpr std::uint32_t settleBatchCopies = 256;
@@ -225,6 +240,7 @@ struct HostImports {
std::map<std::uint64_t, std::uint32_t> sparseRunJoins;
std::map<std::pair<std::uint64_t, std::uint64_t>, ImportFailure> failedBlocks;
std::map<std::pair<std::uint64_t, std::uint64_t>, ImportRefusal> refusals;
std::map<std::pair<std::uint64_t, std::uint64_t>, EvictingRefusal> evictingRefusals;
// Registry generation the imports were last reconciled with.
std::uint64_t refreshedGeneration = 0;
// Bumped whenever an import is dropped, so HostImport pointers taken under the lock earlier can be
@@ -1425,9 +1441,14 @@ bool pointerHeld(const HostImports& state, const HostImport& entry) {
return effectiveUse(state, entry) >= state.buildFloor;
}
constexpr std::uint64_t ProtectedUses = 256;
bool evictable(const HostImports& state, const HostImport& entry) {
constexpr std::uint64_t protectedUses = 256;
return !pointerHeld(state, entry) && effectiveUse(state, entry) + protectedUses < state.useCounter;
return !pointerHeld(state, entry) && effectiveUse(state, entry) + ProtectedUses < state.useCounter;
}
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);
}
std::uint64_t retireLeastRecent(const Context& context, HostImports& state, const GuestAllocations::Lease& lease, std::uint64_t wanted) {
@@ -1564,10 +1585,16 @@ const HostImport* findImport(HostImports& state, std::uint64_t begin, std::uint6
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).
std::uint64_t evictableBytes(const HostImports& state, std::uint64_t begin, std::uint64_t end) {
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) {
if ((entry.base + entry.bytes <= begin || entry.base >= end) && evictable(state, entry)) bytes += entry.pinnedBytes;
if (entry.base + entry.bytes > begin && entry.base < end) continue;
if (evictable(state, entry)) {
bytes += entry.pinnedBytes;
continue;
}
auto& horizon = pointerHeld(state, entry) ? readyHeld : ready;
horizon = std::min(horizon, effectiveUse(state, entry) + ProtectedUses + 1);
}
return bytes;
}
@@ -1657,6 +1684,19 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
failures.erase(failed);
}
dropTrimmed(state, base, base + bytes);
const auto budget = effectiveBudget(context, state);
const auto containing = std::find_if(run.ranges.begin(), run.ranges.end(), [address](const auto& range) { return address >= range->address && address < range->address + range->bytes; });
if (const auto remembered = state.evictingRefusals.find({base, bytes}); evict && remembered != state.evictingRefusals.end()) {
if (remembered->second.joined == joined && evictingRefusalCurrent(state, remembered->second, budget) && sameRun(remembered->second, run)) {
++counts.refused;
counts.refusedBytes += bytes;
++counts.remembered;
const auto room = remembered->second.room - std::min(remembered->second.room, remembered->second.budget - budget);
if (stepDown && containing != run.ranges.end() && room >= (*containing)->bytes) return importAllocation(context, state, address, lease, requester, end, evict, std::min(bytes / 2, room));
return nullptr;
}
state.evictingRefusals.erase(remembered);
}
auto first = state.imports.lower_bound(base);
if (first != state.imports.begin() && std::prev(first)->second.base + std::prev(first)->second.bytes > base) --first;
std::uint64_t replaced = 0;
@@ -1672,9 +1712,10 @@ const HostImport* importRunAt(const Context& context, HostImports& state, Import
if (held(it->second)) return stepDown ? importAllocation(context, state, address, lease, requester, end, evict, bytes / 2) : nullptr;
replaced += it->second.pinnedBytes;
}
const auto budget = effectiveBudget(context, state);
const auto live = state.liveBytes - replaced;
const auto evictable = evict && live + bytes > budget ? evictableBytes(state, base, base + bytes) : 0;
auto ready = std::numeric_limits<std::uint64_t>::max();
auto readyHeld = std::numeric_limits<std::uint64_t>::max();
const auto evictable = evict && live + bytes > budget ? evictableBytes(state, base, base + bytes, ready, readyHeld) : 0;
if (live + bytes > budget + evictable) {
// A refused import turns every later use of the range into CPU copies, so say so.
static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60);
@@ -1696,8 +1737,8 @@ 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 containing = std::find_if(run.ranges.begin(), run.ranges.end(), [address](const auto& range) { return address >= range->address && address < range->address + range->bytes; });
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 (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});
return nullptr;
@@ -1939,6 +1980,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
state.failedJoins.clear();
state.failedBlocks.clear();
state.refusals.clear();
state.evictingRefusals.clear();
state.ceiling = 0;
state.allocationFailures = 0;
state.allocationFailed = false;
@@ -1989,6 +2031,7 @@ void refreshImports(const Context& context, HostImports& state, const GuestAlloc
for (auto it = state.failedJoins.begin(); it != state.failedJoins.end();) it = sameRun(it->second, joinedRun(lease, it->second.base, it->second.base + it->second.bytes)) ? std::next(it) : state.failedJoins.erase(it);
for (auto it = state.failedBlocks.begin(); it != state.failedBlocks.end();) it = sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.failedBlocks.erase(it);
for (auto it = state.refusals.begin(); it != state.refusals.end();) it = stillRefused(state, &it->second, budget) && sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.refusals.erase(it);
for (auto it = state.evictingRefusals.begin(); it != state.evictingRefusals.end();) it = evictingRefusalCurrent(state, it->second, budget) && sameRun(it->second, exactRun(lease, it->first.first, it->first.second)) ? std::next(it) : state.evictingRefusals.erase(it);
}
const HostImport* peekImport(const HostImports& state, std::uint64_t begin, std::uint64_t end) {
@@ -2797,6 +2840,7 @@ LeaseState& Leases() {
struct SnapshotStats {
std::atomic<std::uint64_t> checked{0};
std::atomic<std::uint64_t> skipped{0};
std::atomic<std::uint64_t> mismatched{0};
};
SnapshotStats& Snapshots() {
@@ -3029,6 +3073,10 @@ MirrorStats MirrorCounters() {
return stats;
}
std::uint64_t SnapshotMismatches() {
return Snapshots().mismatched.load(std::memory_order_relaxed);
}
std::vector<std::uint64_t> AddressTableKey(std::span<const std::pair<std::uint64_t, std::uint64_t>> writes, std::span<const ShaderRecompiler::BdaAbi::Range> own) {
std::vector<std::uint64_t> key;
key.reserve(writes.size() * 2u + 1u + own.size() * 4u);
@@ -3097,6 +3145,7 @@ void ClearHostImports(VkDevice device) {
state.failedJoins.clear();
state.failedBlocks.clear();
state.refusals.clear();
state.evictingRefusals.clear();
state.ceiling = 0;
state.allocationFailures = 0;
state.allocationFailed = false;
@@ -4080,10 +4129,9 @@ void GuestBufferMemory::AddSnapshot(const GuestMemorySnapshot& snapshot) {
static const bool fatal = std::getenv("APS5_SNAPSHOT_CHECK_FATAL") != nullptr;
if (!live || fatal) throw std::runtime_error(text);
static std::mutex reportMutex;
static std::uint64_t mismatches = 0;
static auto lastReport = std::chrono::steady_clock::now() - std::chrono::seconds(60);
std::lock_guard lock(reportMutex);
++mismatches;
const auto mismatches = Snapshots().mismatched.fetch_add(1, std::memory_order_relaxed) + 1;
const auto now = std::chrono::steady_clock::now();
if (now - lastReport >= std::chrono::seconds(10)) {
lastReport = now;
@@ -203,8 +203,8 @@ GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words)
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 != 0) {
Require(false, "guest cube texture descriptor does not contain a multiple of 6 array slices" +
if (depth - baseArray + 1u < 6u) {
Require(false, "guest cube texture descriptor holds fewer than 6 array slices" +
describe() + " (base array " + std::to_string(baseArray) + ")");
}
break;
@@ -36,10 +36,22 @@ std::uint32_t MultisampledBlockLog2(ColorTileMode mode) {
}
}
std::uint64_t collectBlocks(std::uint64_t address, std::size_t bytes) {
constexpr std::uint64_t block = 65536;
const auto first = address & ~(block - 1);
const auto stop = (address + bytes + block - 1) & ~(block - 1);
const auto generation = GuestMemory::CollectWritesUncached(first, static_cast<std::size_t>(stop - first));
return generation != 0 ? generation : GuestMemory::CollectWritesUncached(address, bytes);
}
bool sameTarget(const ColorTarget& a, const ColorTarget& b) {
return a.surfaceAddress == b.surfaceAddress && a.format == b.format && a.extent.width == b.extent.width && a.extent.height == b.extent.height && a.samples == b.samples;
}
bool recreated(const ColorTarget& resident, const ColorTarget& use) {
return use.dccAddress != 0 && use.pipeBankXor != resident.pipeBankXor && use.address == resident.address && use.dccAddress == resident.dccAddress && use.dccControl == resident.dccControl && use.dccAlphaOnMsb == resident.dccAlphaOnMsb && use.dccPipeAligned == resident.dccPipeAligned && use.cmaskAddress == resident.cmaskAddress && use.cmaskBytes == resident.cmaskBytes && use.tileMode == resident.tileMode && use.elementBytes == resident.elementBytes;
}
VkClearColorValue fastClearColor(const ColorTarget& target) {
Require(target.elementBytes != 0 && target.elementBytes <= sizeof(target.clearWords) && 16u % target.elementBytes == 0, "the fast clear of a multisampled target over 64-bit texels is not modeled");
std::array<std::byte, 16> texels{};
@@ -215,6 +227,24 @@ public:
});
}
void Recreate(const ColorTarget& use) {
SettleStorageWrites();
target = use;
cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes);
colorFill = MultisampledColorFill(use);
fromTexture = false;
dccRanges.clear();
metadataValid = std::make_shared<std::atomic<bool>>(true);
const auto initial = initialDccClear(use);
recordCommands(context, [&](VkCommandBuffer commands, Recorder* recorder) {
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT | VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
const VkImageSubresourceRange range{VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
context.Resolved(&DeviceFunctions::cmdClearColorImage, "vkCmdClearColorImage")(commands, image, VK_IMAGE_LAYOUT_GENERAL, &initial, 1, &range);
RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT);
if (recorder != nullptr) Recorder::CountBarriers(Recorder::CommandClass::Draw, 2);
});
}
void SettleStorageWrites() {
if (!storageWritten) return;
storageWritten = false;
@@ -365,7 +395,7 @@ private:
std::vector<GuestMemory::UnchangedQuery> metadata(dccRanges.begin() + 1, dccRanges.end());
for (auto& range : metadata) {
if (!GuestMemory::Watched(range.address, range.bytes)) return false;
range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes);
range.generation = collectBlocks(range.address, range.bytes);
if (range.generation == 0) return false;
}
clear = MultisampledDccClear(use, std::vector<std::byte>(dccRanges[1].bytes, std::byte{*keys}), std::vector<std::byte>(use.cmaskBytes, std::byte{cmaskKeys}));
@@ -386,7 +416,7 @@ private:
for (auto& range : dccRanges) {
GuestMemory::CheckRange(reinterpret_cast<const void*>(range.address), range.bytes, 256);
Require(GuestMemory::Watched(range.address, range.bytes), "initial multisampled DCC contents require watched guest memory");
range.generation = GuestMemory::CollectWritesUncached(range.address, range.bytes);
range.generation = collectBlocks(range.address, range.bytes);
Require(range.generation != 0, "initial multisampled DCC contents have no write generation");
}
for (std::size_t first = 0; first < dccRanges.size(); ++first) {
@@ -529,6 +559,17 @@ MultisampleTarget& acquire(const Context& context, const ColorTarget& use) {
for (const auto& target : targets()) {
if (!target->active->load(std::memory_order_acquire)) continue;
if (target->context.device != context.device || !sameTarget(target->target, use)) continue;
if (recreated(target->target, use)) {
{
std::unique_lock lock(guardedMutex());
std::erase_if(guarded(), [&](const auto& range) { return range.active == target->active; });
anyGuarded().store(!guarded().empty(), std::memory_order_release);
}
target->Recreate(use);
guard(context, use, target->active, target->metadataValid);
target->ApplyFastClear(use);
return *target;
}
if (target->fromTexture && use.cmaskAddress != 0) {
target->fromTexture = false;
target->cmask = MultisampledCmask(use.cmaskAddress, use.cmaskBytes);
@@ -959,6 +1000,10 @@ void ClearMultisampleTargets(VkDevice device) {
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access) {
RequireNoResidentDepthRead_nid_no_patch(address, bytes);
RequireNoMultisampleColorTarget(address, bytes, access);
}
void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access) {
if (bytes == 0 || !anyGuarded().load(std::memory_order_acquire)) return;
std::shared_lock lock(guardedMutex());
for (const auto& range : guarded()) {
@@ -34,15 +34,16 @@ bool PassHazards::overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) c
PassHazard PassHazards::Check(const PassAccess& access, bool samePass) const {
const auto touchesMemory = [](const PassAccess& use) {
const auto nonEmpty = [](GuestRangeList ranges) { return std::any_of(ranges.begin(), ranges.end(), [](const auto& range) { return range.first < range.second; }); };
return use.anyReads || use.anyWrites || nonEmpty(use.reads) || nonEmpty(use.writes);
return use.anyReads || use.anyWrites || nonEmpty(use.reads) || nonEmpty(use.writes) || nonEmpty(use.writeOnly);
};
const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty();
const bool earlierTouches = anyReads || anyWrites || !reads.Empty() || !writes.Empty() || !writeOnly.Empty();
if ((access.anyWrites && earlierTouches) || (anyWrites && touchesMemory(access))) return PassHazard::UnknownWrite;
const bool writesRanges = std::any_of(access.writes.begin(), access.writes.end(), [](const auto& range) { return range.first < range.second; });
if ((access.anyReads && !writes.Empty()) || (anyReads && writesRanges)) return PassHazard::UnknownRead;
if (overlapsAny(writes, access.reads)) return PassHazard::ReadAfterWrite;
if (overlapsAny(writes, access.writes)) return PassHazard::WriteAfterWrite;
if (overlapsAny(reads, access.writes)) return PassHazard::WriteAfterRead;
const auto nonEmpty = [](GuestRangeList ranges) { return std::any_of(ranges.begin(), ranges.end(), [](const auto& range) { return range.first < range.second; }); };
const bool writesRanges = nonEmpty(access.writes) || nonEmpty(access.writeOnly);
if ((access.anyReads && (!writes.Empty() || !writeOnly.Empty())) || (anyReads && writesRanges)) return PassHazard::UnknownRead;
if (overlapsAny(writes, access.reads) || overlapsAny(writeOnly, access.reads)) return PassHazard::ReadAfterWrite;
if (overlapsAny(writes, access.writes) || overlapsAny(writeOnly, access.writes) || overlapsAny(writes, access.writeOnly)) return PassHazard::WriteAfterWrite;
if (overlapsAny(reads, access.writes) || overlapsAny(reads, access.writeOnly)) return PassHazard::WriteAfterRead;
const auto conflicts = [](bool earlier, bool later) { return earlier || later; };
for (const auto& [image, written] : access.images) {
if (image == VK_NULL_HANDLE) continue;
@@ -60,6 +61,7 @@ PassHazard PassHazards::Check(const PassAccess& access, bool samePass) const {
void PassHazards::Add(const PassAccess& access) {
for (const auto& [begin, end] : access.reads) reads.Insert(begin, end);
for (const auto& [begin, end] : access.writes) writes.Insert(begin, end);
for (const auto& [begin, end] : access.writeOnly) writeOnly.Insert(begin, end);
for (const auto& [image, written] : access.images) {
if (image == VK_NULL_HANDLE) continue;
const auto it = std::find_if(images.begin(), images.end(), [&](const auto& seen) { return seen.first == image; });
@@ -76,16 +78,22 @@ void PassHazards::Add(const PassAccess& access) {
anyWrites = anyWrites || access.anyWrites;
}
void PassHazards::SealWriteOnly() {
for (const auto& [begin, end] : writeOnly.Ranges()) writes.Insert(begin, end);
writeOnly.Clear();
}
void PassHazards::Clear() {
reads.Clear();
writes.Clear();
writeOnly.Clear();
images.clear();
attachments.clear();
anyReads = anyWrites = false;
}
bool PassHazards::Empty() const {
return reads.Empty() && writes.Empty() && images.empty() && attachments.empty() && !anyReads && !anyWrites;
return reads.Empty() && writes.Empty() && writeOnly.Empty() && images.empty() && attachments.empty() && !anyReads && !anyWrites;
}
}
@@ -224,6 +224,11 @@ bool libraryKeys(PipelineLibraryKeys& keys, const Context& context, const State&
}
}
}
if (resources.split) {
const auto unused = 3u & ~(keys.preRasterizationSets | keys.fragmentSets);
keys.preRasterizationSets |= unused & 1u;
keys.fragmentSets |= unused & 2u;
}
for (const auto& binding : input.bindings) {
appendKey(keys.vertexInput, binding.binding);
appendKey(keys.vertexInput, binding.stride);
@@ -1,5 +1,6 @@
#include "prx/libSceAgcDriver/Graphics/include/PipelineCache.hpp"
#include "ShaderCacheDirectory.hpp"
#include "ShaderDiskCache.hpp"
#include <algorithm>
#include <chrono>
#include <cstdio>
@@ -45,9 +46,11 @@ PipelineCache::PipelineCache(const Context& context, const VkPhysicalDevicePrope
std::snprintf(digits, sizeof(digits), "%02x", static_cast<unsigned>(byte));
uuid += digits;
}
char version[17];
std::snprintf(version, sizeof(version), "%016llx", static_cast<unsigned long long>(ShaderRecompiler::ShaderDiskCache::SourceVersion()));
char name[160];
std::snprintf(name, sizeof(name), "vk-pipelines-%04x-%04x-%08x-%s.bin", properties.vendorID, properties.deviceID, properties.driverVersion, uuid.c_str());
path = directory / name;
path = directory / version / name;
}
std::vector<std::byte> initialData;
if (!path.empty()) {
@@ -564,8 +564,15 @@ private:
Journal() {
const char* off = std::getenv("APS5_NO_PIPELINE_JOURNAL");
if ((off != nullptr && std::strcmp(off, "0") != 0) || !ShaderRecompiler::ShaderDiskCache::Enabled()) return;
path = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal";
lockPath = ShaderRecompiler::ShaderDiskCache::EntryDirectory() / "pipelines.journal.lock";
auto directory = ShaderRecompiler::ShaderDiskCache::EntryDirectory();
#ifdef _WIN32
std::error_code error;
const auto native = ShaderRecompiler::NativeCachePath_nid_no_patch(directory, error);
if (error) throw std::filesystem::filesystem_error("cannot resolve the pipeline journal directory", directory, error);
directory = native;
#endif
path = directory / "pipelines.journal";
lockPath = directory / "pipelines.journal.lock";
maxBytes = configured("APS5_PIPELINE_JOURNAL_MAX_MIB", 1024u) << 20u;
std::thread([this] { run(); }).detach();
}
@@ -32,6 +32,7 @@
#include <string>
#include <thread>
#include <tuple>
#include <unordered_map>
#include "prx/libc/include/general/AtomicSharedPtr.hpp"
namespace AgcDriver::Graphics {
@@ -1419,6 +1420,10 @@ void Recorder::LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool
open->passHazards.Add(access);
}
void Recorder::SealPassWriteOnly() {
if (open != nullptr) open->passHazards.SealWriteOnly();
}
void Recorder::endOpenRenderPass(bool barrier) {
auto& pass = open->renderPass;
if (open->segmentActive) endSegment(*open);
@@ -3396,6 +3401,11 @@ bool Recorder::writtenBackSince(std::uint64_t sequence, std::uint64_t begin, std
return false;
}
void Recorder::HoldNextSubmit(VkSemaphore semaphore, std::uint64_t value) {
submitHold = semaphore;
submitHoldValue = value;
}
void Recorder::Submit() {
PerformanceTimer frameTiming("Recorder.Submit");
// The work count is cleared even when nothing is open: the driver counts a dispatch after its
@@ -3445,13 +3455,23 @@ void Recorder::Submit() {
const std::uint64_t serial = submissions + 1;
CaptureTrace::Log("submit batch=%llu reads=%zu writes=%zu", static_cast<unsigned long long>(serial), batch->reads.size(), batch->writes.size());
VkTimelineSemaphoreSubmitInfoKHR timelineInfo{VK_STRUCTURE_TYPE_TIMELINE_SEMAPHORE_SUBMIT_INFO_KHR};
timelineInfo.signalSemaphoreValueCount = 1;
timelineInfo.pSignalSemaphoreValues = &serial;
const VkSemaphore hold = std::exchange(submitHold, VK_NULL_HANDLE);
const std::uint64_t holdValue = submitHoldValue;
VkPipelineStageFlags holdStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
if (hold != VK_NULL_HANDLE) {
submission.waitSemaphoreCount = 1;
submission.pWaitSemaphores = &hold;
submission.pWaitDstStageMask = &holdStage;
timelineInfo.waitSemaphoreValueCount = 1;
timelineInfo.pWaitSemaphoreValues = &holdValue;
}
if (timeline != VK_NULL_HANDLE) {
submission.pNext = &timelineInfo;
timelineInfo.signalSemaphoreValueCount = 1;
timelineInfo.pSignalSemaphoreValues = &serial;
submission.signalSemaphoreCount = 1;
submission.pSignalSemaphores = &timeline;
}
if (hold != VK_NULL_HANDLE || timeline != VK_NULL_HANDLE) submission.pNext = &timelineInfo;
const auto submitStart = std::chrono::steady_clock::now();
const auto result = function(queueSubmit, "vkQueueSubmit")(context.queue, 1, &submission, batch->fence);
if (result == VK_ERROR_DEVICE_LOST) {
@@ -3954,6 +3974,16 @@ void Recorder::finish(std::unique_ptr<Batch> batch, bool wait, int source, bool
for (auto& action : batch->completions) {
try {
action();
} catch (const BdaUnalignedAccess& error) {
static std::mutex failuresMutex;
static std::unordered_map<std::uint32_t, std::uint64_t> failures;
constexpr std::uint64_t kDeferredFailureFirst = 8, kDeferredFailureEvery = 1024;
const auto count = [&] {
std::lock_guard lock(failuresMutex);
return ++failures[error.instruction];
}();
if (count <= kDeferredFailureFirst) std::fprintf(stderr, "[gpu] deferred write-back failed: %s\n", error.what());
else if (count % kDeferredFailureEvery == 0) std::fprintf(stderr, "[gpu] deferred write-back failed with an unaligned BDA access at pc 0x%x %llu times so far; latest: %s\n", error.instruction, static_cast<unsigned long long>(count), error.what());
} catch (const std::exception& error) {
AgcDriver::LogCoalescedStderr_nid_no_patch("[gpu] deferred write-back failed: %s\n", error.what());
}
@@ -19,6 +19,7 @@
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <stdexcept>
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
@@ -119,6 +120,7 @@ struct CachedTexture {
// The untrackedKey the snapshot was last read or compared equal at, for a surface outside the
// write watch; zero when none was taken.
std::array<std::uint64_t, 4> untracked{};
std::uint64_t surfaceBytes = 0;
};
// Entries in use order (front = most recent) with a hash index by key: a lookup is O(1) and the
@@ -274,16 +276,17 @@ bool admitTexture(TextureCache& cache, const TextureKey& key, std::uint64_t inco
cache.firstUses.clear();
cache.firstUsesPresent = present;
}
const bool firstUse = incoming != 0 && present != 0 && !cache.firstUses.contains(key);
const bool scan = incoming != 0 && present != 0;
const bool firstUse = scan && !cache.firstUses.contains(key);
const auto ceiling = budget + budget / 4u;
while (!cache.entries.empty() && cache.bytes + incoming > budget) {
const auto back = std::prev(cache.entries.end());
if (firstUse && back->present + 1u >= present) {
const bool over = cache.bytes + incoming > ceiling;
if (scan && !over && (firstUse ? back->present + 1u >= present : back->present == present)) {
cache.firstUses.insert(key);
++cache.counts.uncached;
return false;
}
const bool over = cache.bytes + incoming > ceiling;
const bool keep = back->accounted == 0 || (!over && back->texture.use_count() > 1);
if (keep && cache.passed < cache.entries.size()) {
cache.entries.splice(cache.entries.begin(), cache.entries, back);
@@ -306,7 +309,7 @@ void reportCacheCounts(TextureCache& cache, std::uint64_t budget) {
if (now - cache.countsReported < std::chrono::seconds(10)) return;
cache.countsReported = now;
const auto counts = std::exchange(cache.counts, {});
AgcDriver::ProfilePrint_nid_no_patch("[texture-cache] (10 s): %llu textures cached, %llu used uncached (first use this frame, the least recently used entry used this frame or the last), %llu evicted, %llu held ones passed over, %llu found changed; %llu MiB in %zu entries, budget %llu MiB, frame %llu\n", static_cast<unsigned long long>(counts.inserted), static_cast<unsigned long long>(counts.uncached), static_cast<unsigned long long>(counts.evicted), static_cast<unsigned long long>(counts.passed), static_cast<unsigned long long>(counts.stale), static_cast<unsigned long long>(cache.bytes >> 20u), cache.entries.size(), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(Recorder::Presents()));
AgcDriver::ProfilePrint_nid_no_patch("[texture-cache] (10 s): %llu textures cached, %llu used uncached (first use this frame with the least recently used entry used this frame or the last, or a later use with it used this frame), %llu evicted, %llu held ones passed over, %llu found changed; %llu MiB in %zu entries, budget %llu MiB, frame %llu\n", static_cast<unsigned long long>(counts.inserted), static_cast<unsigned long long>(counts.uncached), static_cast<unsigned long long>(counts.evicted), static_cast<unsigned long long>(counts.passed), static_cast<unsigned long long>(counts.stale), static_cast<unsigned long long>(cache.bytes >> 20u), cache.entries.size(), static_cast<unsigned long long>(budget >> 20u), static_cast<unsigned long long>(Recorder::Presents()));
}
struct ImageMemoEntry {
@@ -551,7 +554,10 @@ std::optional<std::uint32_t> largestDeviceLocalHeap(const VkPhysicalDeviceMemory
return heap;
}
std::atomic<std::uint64_t> budgetOverride{0};
std::uint64_t forcedCacheBudget() {
if (const auto overridden = budgetOverride.load(std::memory_order_relaxed); overridden != 0) return overridden;
static const std::uint64_t forced = [] {
const char* text = std::getenv("APS5_TEXTURE_CACHE_MIB");
return text != nullptr ? std::strtoull(text, nullptr, 10) << 20u : 0u;
@@ -658,7 +664,9 @@ void reportGuestAllocation(const GuestTextureResource& resource, std::uint64_t g
}
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& resource, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) {
std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& requested, VkComponentMapping components, std::uint64_t guestBytes = 0, bool depthCompare = false, VkImageViewType multisampleViewType = VK_IMAGE_VIEW_TYPE_2D) {
const auto resource = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers);
if (resource.baseAddress != requested.baseAddress) guestBytes = 0;
if (resource.dimension == TextureDimension::k2DMsaa) {
Require(!depthCompare, "native multisampled depth comparison sampling is not implemented");
if (auto depth = DepthSurfaceTexture(context, words, resource, components, multisampleViewType)) {
@@ -857,18 +865,19 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
if (profile) LookupOutcomes::Add(*keys != DccKeys::Uncompressed ? LookupOutcomes::SampledHitClearedView : LookupOutcomes::SampledHitView, start);
return it->texture;
}
} else if (source == nullptr && it->bytes.size() == guestBytes && it->keys == *keys) {
} else if (source == nullptr && it->surfaceBytes == guestBytes && it->keys == *keys) {
// Unwritten pages need no comparison; partially resident textures compare only committed
// pages. The compare goes through the flush hook: it waits for recorded work over the
// surface (counted, and named for the [hooksync] line).
const auto equalsCommitted = [&] {
if (it->bytes.empty()) return false;
if (Recorder::SnapshotWriteOverlaps(address, bytes)) counters.pendingReads.fetch_add(1, std::memory_order_relaxed);
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::TextureCompare);
return GuestMemory::EqualsCommitted(address, it->bytes);
};
const auto unchanged = [&] {
if (it->committed != committed) return false;
return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, it->bytes.size()) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, it->bytes.size(), it->generation);
return committed.has_value() ? !Recorder::SnapshotWriteOverlaps(address, static_cast<std::size_t>(it->surfaceBytes)) && GuestMemory::UnchangedCommittedSince(*committed, it->generation) : GuestMemory::UnchangedSince(address, static_cast<std::size_t>(it->surfaceBytes), it->generation);
};
const auto kept = [&] {
if (untracked[0] == 0) return false;
@@ -892,6 +901,7 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
}
CachedTexture entry{key, address, std::vector<std::byte>(source != nullptr ? 0u : bytes), nullptr, *keys, snapshotGeneration};
entry.committed = std::move(committed);
entry.surfaceBytes = bytes;
if (source != nullptr) {
entry.source = source;
entry.sourceVersion = source->Version();
@@ -926,6 +936,8 @@ std::shared_ptr<Texture> cachedTexture(const Context& context, std::span<const s
}
entry.texture = std::make_shared<Texture>(context, *context.detiler, resource, components, entry.bytes, depthCompare);
entry.accounted = entry.texture->AllocationBytes();
constexpr std::size_t SnapshotCompareBytes = 256u << 10u;
if (entry.bytes.size() > SnapshotCompareBytes && (entry.generation != 0 || entry.untracked[0] != 0)) std::vector<std::byte>().swap(entry.bytes);
counters.snapshots.fetch_add(1, std::memory_order_relaxed);
}
const auto budget = sampledBudget(context, cache);
@@ -1095,7 +1107,9 @@ static WrittenStorage WrittenStorageBytes(const GuestTextureResource& viewed, st
}
// `guestBytes` is the surface size when the caller described the surface already (0: described here).
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& viewed, std::uint32_t mip, std::uint64_t guestBytes, bool written) {
std::shared_ptr<StorageTexture> cachedStorageTexture(const Context& context, std::span<const std::uint32_t> words, const GuestTextureResource& requested, std::uint32_t mip, std::uint64_t guestBytes, bool written) {
const auto viewed = ArrayLayerWindow(requested, context.limits.maxImageArrayLayers);
if (viewed.baseAddress != requested.baseAddress) guestBytes = 0;
Require(viewed.dimension != TextureDimension::k2DMsaa, "storage image access to a multisampled texture is not implemented");
RequireNoMultisampleTarget(viewed.baseAddress, guestBytes != 0 ? static_cast<std::size_t>(guestBytes) : 1u, "a storage image access");
if (auto depth = DepthSurfaceStorage(context, words, viewed, mip)) return depth;
@@ -1193,7 +1207,7 @@ std::uint64_t StorageTextureBudget(const VkPhysicalDeviceMemoryProperties& memor
if (reported == nullptr || !heap || reported->heapBudget[*heap] == 0) return TextureCacheBudget(memory);
const auto budget = reported->heapBudget[*heap];
const auto usage = reported->heapUsage[*heap];
const auto reserved = usage - std::min<std::uint64_t>(usage, sampledBytes + storageBytes) + std::max<std::uint64_t>(2048ull << 20u, sampledBytes) + budget / 8u;
const auto reserved = usage - std::min<std::uint64_t>(usage, sampledBytes + storageBytes) + sampledBytes + budget / 8u;
const auto cap = std::min<std::uint64_t>(TextureCacheBudget(memory), budget / 4u);
return std::max<std::uint64_t>(512ull << 20u, std::min<std::uint64_t>(cap, budget > reserved ? budget - reserved : 0u));
}
@@ -1220,6 +1234,10 @@ std::uint64_t StorageTextureCacheBudget(const Context& context) {
return storageBudget(context);
}
void OverrideCacheBudgetForTest(std::uint64_t bytes) {
budgetOverride.store(bytes, std::memory_order_relaxed);
}
TextureCacheUse TextureCacheUsage() {
TextureCacheUse use;
{
@@ -1948,6 +1966,8 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
const bool read = element < binding.bufferRead.size() && binding.bufferRead[element];
const std::uint32_t extent = element < binding.bufferExtent.size() ? binding.bufferExtent[element] : 0u;
const auto index = addGuestBuffer(std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * 4, 4), target, indexAddress, indexBytes, written, atomic, read, extent);
const bool classified = element < binding.bufferWritten.size() && element < binding.bufferRead.size() && element < binding.bufferAtomic.size();
allocations[index].writeOnly = classified && allocations[index].guest && allocations[index].written && binding.bufferWritten[element] && !read && !atomic;
const auto& push = shader.program->pushConstants;
if (!push.empty()) {
const auto position = shader.program->memoryOffsetDword * 4u + element;
@@ -2143,6 +2163,14 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
Check(context.Function<PFN_vkAllocateDescriptorSets>("vkAllocateDescriptorSets")(context.device, &allocation, &_set), "vkAllocateDescriptorSets");
}
}
writeOnlyRanges.clear();
for (const auto& item : allocations) {
if (!item.writeOnly || item.size == 0) continue;
const auto begin = item.address;
const auto end = item.address + item.size;
const bool shared = std::any_of(allocations.begin(), allocations.end(), [&](const Allocation& other) { return !other.writeOnly && other.size != 0 && other.address < end && begin < other.address + other.size; });
if (!shared) writeOnlyRanges.emplace_back(begin, end);
}
timing.descriptorsMs = phase(BuildPhase::Descriptors);
if (BuildProfiled()) {
timing.prepareMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - stageStart).count();
@@ -2697,7 +2725,7 @@ namespace {
// of the reused ones, fast = every image proved current from stamps, full = the lookups were repeated.
// Why the fast path left an object to the full walk (the "fast-fail by reason" counts).
using FastFail = ShaderResources::FastFail;
constexpr std::array<const char*, static_cast<std::size_t>(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface"};
constexpr std::array<const char*, static_cast<std::size_t>(FastFail::Count)> FastFailNames{"no record", "collect", "pending image", "evicted image", "memory changed", "keys", "cleared view", "storage keys", "depth surface", "replaced texture", "audit"};
using OwnRefreshFallback = ShaderResources::OwnRefreshFallback;
constexpr std::array<const char*, static_cast<std::size_t>(OwnRefreshFallback::Count)> OwnRefreshFallbackNames{"disabled", "snapshot texture", "cleared view", "foreign view", "surface key", "not imported", "uncached", "re-run failed"};
@@ -2835,6 +2863,19 @@ bool EpochRevalidate() {
return enabled;
}
std::chrono::steady_clock::duration TemplateAuditInterval() {
static const auto interval = [] {
const char* text = std::getenv("APS5_TEMPLATE_AUDIT_MS");
return std::chrono::milliseconds(text != nullptr ? std::strtoull(text, nullptr, 10) : 1000ull);
}();
return interval;
}
bool TemplateAuditDue(std::chrono::steady_clock::time_point last) {
const auto interval = TemplateAuditInterval();
return interval.count() != 0 && std::chrono::steady_clock::now() - last >= interval;
}
}
// Turns the lookups' records into this object's per-texture validation records (see
@@ -2913,6 +2954,7 @@ bool ShaderResources::fastRevalidate(std::uint64_t serialBefore, std::span<const
if (!DepthSurfaceHolds(context, surface.resource, textures[i].get())) return fail(FastFail::Depth);
continue;
}
if (textures[i]->Released()) return fail(FastFail::Replaced);
const auto address = surface.resource.baseAddress;
const auto bytes = static_cast<std::size_t>(surface.bytes);
surface.collected = GuestMemory::CollectWrites(address, bytes);
@@ -3039,6 +3081,7 @@ bool ShaderResources::fastRevalidateEach() {
if (!DepthSurfaceHolds(context, surface.resource, textures[i].get())) return false;
continue;
}
if (textures[i]->Released()) return false;
const auto address = surface.resource.baseAddress;
const auto bytes = static_cast<std::size_t>(surface.bytes);
surface.collected = GuestMemory::CollectWrites(address, bytes);
@@ -3216,6 +3259,7 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR
const auto start = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
for (const auto& region : directRegions) CompletePendingCpuWrites(context, region.begin, static_cast<std::size_t>(region.end - region.begin));
for (const auto& range : describedRanges) {
if (!range.imageTable) RequireNoMultisampleColorTarget(range.address, static_cast<std::size_t>(range.bytes), "a cached image access");
CompletePendingCpuWrites(context, range.address, static_cast<std::size_t>(range.bytes));
if (range.dccAddress != 0) CompletePendingCpuWrites(context, range.dccAddress, DccKeyBytes(range.bytes));
}
@@ -3238,6 +3282,7 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR
// they must hand back the very objects the set's views belong to. The build appended them stage
// by stage, binding by binding, so the walk repeats that order.
const auto fullWalk = [&] {
lastFullWalk = std::chrono::steady_clock::now();
lookupLog().clear();
BuildLookups lookups;
std::size_t textureIndex = 0;
@@ -3343,6 +3388,11 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR
}
if (fallback != OwnRefreshFallback::Count) countOwnRefreshFallback(fallback);
}
if (fast && TemplateAuditDue(lastFullWalk)) {
fast = false;
ownRefreshed = false;
reason = FastFail::Audit;
}
if (report != nullptr) report->path = !fast ? ProofPath::Full : ownRefreshed ? ProofPath::OwnRefreshed : ProofPath::Fast;
if (!fast) {
countFullWalk(reason);
@@ -3350,7 +3400,7 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR
const auto failure = [&] {
switch (reason) {
case FastFail::Pending: return ProofFailure::Pending;
case FastFail::Evicted: return ProofFailure::Evicted;
case FastFail::Evicted: case FastFail::Replaced: return ProofFailure::Evicted;
case FastFail::Changed: return ProofFailure::Changed;
case FastFail::Keys: case FastFail::ClearedView: case FastFail::StorageKeys: return ProofFailure::Keys;
default: return ProofFailure::Other;
@@ -4474,6 +4524,7 @@ std::shared_ptr<Texture> ShaderResources::fastTexture(const ImageRecord& record)
// between the stages marks them uncompressed).
const auto address = record.resource.baseAddress;
const auto bytes = static_cast<std::size_t>(record.guestBytes);
RequireNoMultisampleColorTarget(address, bytes != 0 ? bytes : 1u, "a texture read");
if (DepthSurfaceAt(address)) return nullptr;
if (GuestMemory::CollectWrites(address, bytes) == 0) return nullptr;
if (PendingStorageOverlaps(address, bytes, record.source.get())) return nullptr;
@@ -4812,7 +4863,7 @@ void ShaderResources::resolveImageTables() {
const auto place = [&](std::uint32_t element, std::shared_ptr<Texture> texture, const GuestTextureResource& resource, std::uint64_t guestBytes, bool firstLayer) {
textures.push_back(std::move(texture));
textureFirstLayer.push_back(firstLayer);
describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress, true});
table.elementTextures[element] = textures.size() - 1u;
item->imageAllocations[static_cast<std::size_t>(table.elementBase) + element] = textures.size() - 1u;
};
@@ -4931,7 +4982,7 @@ void ShaderResources::resolveImageTables() {
storageWritten.push_back(true);
storageAtomic.push_back(false);
storageAtomic64.push_back(false);
describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress, true});
bindings[storageBinding].imageAllocations[absolute] = storageTextures.size() - 1u;
continue;
}
@@ -5477,13 +5528,20 @@ bool SkipWriteBack() {
}
void ShaderResources::WriteBack() {
WriteBackBuffers();
if (SkipWriteBack()) return;
for (std::size_t index = 0; index < storageTextures.size(); ++index) {
if (!storageWritten[index] || storageTextures[index] == nullptr) continue;
storageTextures[index]->MarkDirty();
static_cast<void>(DepthSurfaceStoreBack(context, *storageTextures[index]));
std::exception_ptr unaligned;
try {
WriteBackBuffers();
} catch (const BdaUnalignedAccess&) {
unaligned = std::current_exception();
}
if (!SkipWriteBack()) {
for (std::size_t index = 0; index < storageTextures.size(); ++index) {
if (!storageWritten[index] || storageTextures[index] == nullptr) continue;
storageTextures[index]->MarkDirty();
static_cast<void>(DepthSurfaceStoreBack(context, *storageTextures[index]));
}
}
if (unaligned) std::rethrow_exception(unaligned);
}
void ShaderResources::MarkGpuWrites(Recorder& recorder) {
@@ -5529,9 +5587,16 @@ void ShaderResources::CheckFaults() const {
}
void ShaderResources::WriteBackBuffers() {
if (bda) bda->CheckFault();
if (SkipWriteBack()) return;
guestMemory.WriteBack();
std::exception_ptr unaligned;
if (bda) {
try {
bda->CheckFault();
} catch (const BdaUnalignedAccess&) {
unaligned = std::current_exception();
}
}
if (!SkipWriteBack()) guestMemory.WriteBack();
if (unaligned) std::rethrow_exception(unaligned);
}
bool ShaderResources::ReadsOverlap(std::uint64_t address, std::size_t bytes) const {
@@ -51,6 +51,8 @@ struct Module {
std::uint32_t primitiveIndices = 0;
bool fragmentBarycentric = false;
bool layerOutput = false;
bool meshCapability = false;
std::uint32_t primitiveInputs = 0;
std::map<std::uint32_t, std::vector<std::uint32_t>> modes;
const std::vector<std::uint32_t>& Type(std::uint32_t id) const {
@@ -75,17 +77,17 @@ struct Module {
throw std::runtime_error("AGC graphics: unsupported SPIR-V interface type");
}
std::uint32_t AddLocations(std::map<std::uint32_t, std::string>& locations, std::uint32_t location, std::uint32_t typeId, bool patch, std::uint32_t depth = 0) const {
std::uint32_t AddLocations(std::map<std::uint32_t, std::string>& locations, std::uint32_t location, std::uint32_t typeId, bool patch, bool primitive = false, std::uint32_t depth = 0) const {
Require(depth < 8 && location < 256, "graphics interface exceeds supported locations");
const auto& type = Type(typeId);
if ((type[0] & 0xffffu) == spv::OpTypeArray) {
Require(type.size() == 4, "malformed interface array");
const auto count = constants.find(type[3]);
Require(count != constants.end() && count->second != 0 && count->second <= 256, "invalid interface array length");
for (std::uint32_t i = 0; i < count->second; ++i) location = AddLocations(locations, location, type[2], patch, depth + 1);
for (std::uint32_t i = 0; i < count->second; ++i) location = AddLocations(locations, location, type[2], patch, primitive, depth + 1);
return location;
}
Require(locations.emplace(location | (patch ? 0x10000u : 0u), std::string(patch ? "patch:" : "vertex:") + Signature(typeId)).second, "duplicate shader interface location");
Require(locations.emplace(location | (patch ? 0x10000u : 0u), std::string(patch ? "patch:" : primitive ? "primitive:" : "vertex:") + Signature(typeId)).second, "duplicate shader interface location");
return location + 1;
}
@@ -154,7 +156,7 @@ struct Module {
}
};
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation) {
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics, bool geometryShader, bool sampleRateShading, bool bufferInt64Atomics, bool shaderClipDistance, bool shaderCullDistance, bool viewportIndexLayer, bool runtimeDescriptorArray, bool demoteToHelperInvocation, bool shaderTerminateInvocation, std::span<const std::uint32_t> primitiveLocations = {}) {
using Stage = ShaderRecompiler::ShaderStage;
Require(compiled.program != nullptr, "missing compiled shader");
const auto& shader = *compiled.program;
@@ -238,8 +240,9 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
(control || evaluation) &&
capability == spv::CapabilityTessellation;
if (fragment && capability == spv::CapabilityMeshShadingEXT) module.meshCapability = true;
const bool isMeshCapability =
mesh &&
(mesh || (fragment && !primitiveLocations.empty())) &&
capability == spv::CapabilityMeshShadingEXT;
const bool isGeometryCapability =
@@ -342,7 +345,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
terminateExtension = true;
break;
}
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (viewportIndexLayer && extension == "SPV_EXT_shader_viewport_index_layer") || (fragment && demoteToHelperInvocation && extension == "SPV_EXT_demote_to_helper_invocation") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (fragment && !primitiveLocations.empty() && extension == "SPV_EXT_mesh_shader") || ((descriptorIndexing || runtimeDescriptorArray) && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
break;
}
case spv::OpTerminateInvocation:
@@ -529,10 +532,13 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
module.secondSource = module.Signature(typeId);
} else if (decoration.location) {
Require(!vertexArray || (outer[0] & 0xffffu) == spv::OpTypeArray, "per-vertex interface lacks a control-point or mesh-output dimension");
Require(!decoration.perPrimitive, "per-primitive user outputs are unsupported");
Require(!decoration.perPrimitive || ((mesh && output) || (fragment && input)), "per-primitive user interface is unsupported for this stage");
Require(!decoration.perPrimitive || !decoration.patch, "a per-primitive interface cannot be a patch");
Require(!decoration.perPrimitive || std::ranges::find(primitiveLocations, *decoration.location) != primitiveLocations.end(), "per-primitive user interface at a location the barycentric emulation does not reserve");
Require(!decoration.builtin, "shader input cannot have both location and built-in decorations");
auto& locations = variable.storage == spv::StorageClassInput ? module.inputs : module.outputs;
module.AddLocations(locations, *decoration.location, typeId, decoration.patch);
module.AddLocations(locations, *decoration.location, typeId, decoration.patch, decoration.perPrimitive);
if (decoration.perPrimitive && fragment) module.primitiveInputs++;
} else if (decoration.builtin) {
module.Builtin(*decoration.builtin, typeId, variable.storage, stage, subgroup);
} else {
@@ -619,6 +625,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
Require(push == (!shader.pushConstants.empty() || shader.readsDepthRemapPush || mesh || (control && !rectListControl && !barycentricPatches)), "recompiler push constant metadata disagrees with SPIR-V");
for (const auto id : module.interface) Require(module.variables.contains(id), "entry point interface contains an unknown variable");
if (mesh) Require(module.primitiveIndices == state.stages.mesh->outputPrimitiveSize, "mesh shader does not export primitive indices");
if (fragment) Require(module.primitiveInputs == 0 || !primitiveLocations.empty(), "per-primitive pixel inputs need the barycentric emulation layout");
if (fragment) Require(!module.meshCapability || module.primitiveInputs != 0, "a pixel shader declares mesh shading without a per-primitive input");
if (stage == Stage::Vertex || mesh || evaluation || geometry) Require(module.position, "vertex shader does not export position");
return module;
}
@@ -657,13 +665,25 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
}
const std::array<Stage, 4> tessStages{Stage::Local, Stage::TessellationControl, Stage::TessellationEvaluation, Stage::Fragment};
static_cast<void>(AssemblePushConstants(shaders));
std::vector<std::uint32_t> primitiveLocations;
if (mesh && !shaders.empty() && shaders.back().program != nullptr && shaders.back().program->barycentricEmulation.active) {
const auto& last = *shaders.back().program;
const auto layout = ShaderRecompiler::LayoutBarycentricEmulation(last.fragmentParameters, last.barycentricEmulation, true);
if (layout.positionsLocation != ShaderRecompiler::BarycentricEmulationLayout::NoLocation) {
for (std::uint32_t corner = 0; corner < 3u; ++corner) primitiveLocations.push_back(layout.positionsLocation + corner);
for (const auto& [guest, relocated] : layout.perVertexLocations) {
static_cast<void>(guest);
for (std::uint32_t corner = 0; corner < 3u; ++corner) primitiveLocations.push_back(relocated + corner);
}
}
}
Module previous;
for (std::size_t i = 0; i < shaders.size(); ++i) {
const auto expected = state.rectList || patches ? (i == 0 ? Stage::Vertex : tessStages[i]) : tessellation ? tessStages[i] : geometry && i == 1 ? Stage::Geometry : i == shaders.size() - 1 ? Stage::Fragment : state.stages.path == ShaderPath::Geometry ? Stage::Mesh : Stage::Vertex;
Require(shaders[i].program != nullptr, "missing compiled shader");
Require(shaders[i].stage == expected, "graphics stage order disagrees");
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == ShaderRecompiler::RuntimeAbi::DescriptorSet || (shaders[i].stage == ShaderRecompiler::ShaderStage::Fragment && binding.descriptorSet == ShaderRecompiler::RuntimeAbi::FragmentDescriptorSet), "graphics resource uses a descriptor set other than its stage's");
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation);
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics, geometryShader, sampleRateShading, bufferInt64Atomics, shaderClipDistance, shaderCullDistance, viewportIndexLayer, runtimeDescriptorArray, demoteToHelperInvocation, shaderTerminateInvocation, primitiveLocations);
if (i != 0) {
for (const auto& [location, signature] : current.inputs) {
const auto output = previous.outputs.find(location);
@@ -690,7 +710,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
const auto color = std::find_if(state.colors.begin(), state.colors.end(), [&](const ColorTarget& target) { return target.exportIndex == location; });
const bool uintExport = color != state.colors.end() && color->uintExport;
const bool packed = color != state.colors.end() && color->packing != ShaderRecompiler::ColorExportPacking::None;
Require(signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments");
Require(color == state.colors.end() || signature == (uintExport || packed ? "vertex:u32x4" : "vertex:f32x4"), packed ? "fragment shader must export uint4 words to its packed color attachments" : uintExport ? "fragment shader must export uint4 colors to its unsigned integer attachments" : "fragment shader must export float4 colors to its attachments");
locations.insert(location);
}
Require((!state.dualSourceBlend && state.reversedBlend == 0) || previous.secondSource == "f32x4", "dual-source blending needs a float4 second color at location 0 index 1");
@@ -53,6 +53,12 @@ float readFloat(const Registers& registers, std::uint32_t offset) {
return value;
}
float readDepthBound(const Registers& registers, std::uint32_t offset) {
const auto value = std::bit_cast<float>(read(registers, offset));
if (std::isnan(value)) Require(false, "NaN depth bound at DWORD " + std::to_string(offset));
return value;
}
std::string zeroMessage(std::uint32_t offset, std::uint32_t value, const char* name) {
char detail[64];
std::snprintf(detail, sizeof(detail), " (register 0x%x = 0x%08x)", offset, value);
@@ -361,8 +367,8 @@ void decodeDepth(const Registers& cx, std::uint32_t depthControl, bool testValEx
result.depthBoundsTest = (depthControl & 8u) != 0;
if (result.depthBoundsTest) {
Require(zFormat != 0, "depth bounds without a depth plane");
result.minDepthBounds = readFloat(cx, 0x008);
result.maxDepthBounds = readFloat(cx, 0x009);
result.minDepthBounds = readDepthBound(cx, 0x008);
result.maxDepthBounds = readDepthBound(cx, 0x009);
}
if (depthClear) {
result.depthTest = true;
@@ -1337,9 +1343,8 @@ State DecodeState(const QueueState& queue, bool vertexWorkgroup) {
} else {
const auto screenBottomRight = read(cx, 0xd);
APS5_LOG_OUT_DEBUG("No color target, screen BR register=0x%x", screenBottomRight);
result.renderExtent = {screenBottomRight & 0xffffu, screenBottomRight >> 16u};
result.renderExtent = {std::max(screenBottomRight & 0xffffu, 1u), std::max(screenBottomRight >> 16u, 1u)};
APS5_LOG_OUT_DEBUG("Render extent from screen=%ux%u", result.renderExtent.width, result.renderExtent.height);
Require(result.renderExtent.width != 0 && result.renderExtent.height != 0, "empty framebuffer extent for a draw without color writes");
}
result.quadSampleMask = decodeQuadSampleMask(cx, result.samples);
result.pixelEarlyZ = !PixelProgramSkipped(queue) && PixelEarlyZ(read(cx, 0x203));
@@ -335,6 +335,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
imageInfo.format = vkFormat;
imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth};
imageInfo.mipLevels = descriptor.mipCount;
Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented");
imageInfo.arrayLayers = geometry.imageLayers;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
@@ -525,7 +526,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray;
if (descriptor.dimension == TextureDimension::kCube) {
Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices");
Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices");
}
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
@@ -632,7 +633,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray;
if (descriptor.dimension == TextureDimension::kCube) {
Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices");
Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices");
}
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
@@ -747,7 +748,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
const auto viewLayerCount = geometry.imageLayers - descriptor.baseArray;
if (descriptor.dimension == TextureDimension::kCube) {
Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices");
Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices");
}
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = image;
@@ -1014,7 +1015,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
const auto viewLevelCount = std::min(descriptor.lastLevel, descriptor.mipCount - 1u) - descriptor.baseLevel + 1u;
const auto viewLayerCount = std::min(geometry.imageLayers, source->ImageLayers()) - descriptor.baseArray;
if (descriptor.dimension == TextureDimension::kCube) {
Require(viewLayerCount % 6u == 0, "guest cube texture view does not contain a multiple of 6 array slices");
Require(viewLayerCount >= 6u, "guest cube texture view holds fewer than 6 array slices");
}
VkImageViewCreateInfo viewInfo{VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO};
viewInfo.image = source->Image();
@@ -1390,6 +1391,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
imageInfo.format = vkFormat;
imageInfo.extent = {descriptor.width, descriptor.height, geometry.imageDepth};
imageInfo.mipLevels = descriptor.mipCount;
Require(geometry.imageLayers <= context.limits.maxImageArrayLayers, "a texture array with more layers than maxImageArrayLayers is not implemented");
imageInfo.arrayLayers = geometry.imageLayers;
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
@@ -4688,28 +4690,19 @@ bool StorageTexture::SameSurfaceShape(const StorageTexture& other) const {
return guestBytes == other.guestBytes && mine.width == theirs.width && mine.height == theirs.height && mine.depthOrLastArray == theirs.depthOrLastArray && mine.baseArray == theirs.baseArray && mine.mipCount == theirs.mipCount && mine.tileMode == theirs.tileMode && mine.dimension == theirs.dimension && mine.format == theirs.format && storageFormat == other.storageFormat && geometry.imageLayers == other.geometry.imageLayers && geometry.imageDepth == other.geometry.imageDepth;
}
const char* StorageTexture::CopyRefusal(const Context& context, const StorageTexture* destination, const StorageTexture& source, std::uint64_t address) {
if (destination != nullptr && (destination == &source || !destination->SameSurfaceShape(source))) return "shape";
if (source.descriptor.dccAddress != 0 || (destination != nullptr && destination->descriptor.dccAddress != 0)) return "keys";
if (source.released || !source.Cached() || (destination != nullptr && (destination->released || !destination->Cached()))) return "released";
if (HostImportFor(context, address, static_cast<std::size_t>(source.guestBytes)) == nullptr) return "not imported";
if (Recorder::Active() == nullptr) return "no recorder";
return nullptr;
}
bool StorageTexture::CopyFrom(StorageTexture& source, const char*& refusal) {
if (&source == this || !SameSurfaceShape(source)) {
refusal = "shape";
return false;
}
if (descriptor.dccAddress != 0 || source.descriptor.dccAddress != 0) {
refusal = "keys";
return false;
}
if (released || source.released || !Cached() || !source.Cached()) {
refusal = "released";
return false;
}
if (HostImportFor(context, descriptor.baseAddress, static_cast<std::size_t>(guestBytes)) == nullptr) {
refusal = "not imported";
return false;
}
refusal = CopyRefusal(context, this, source, descriptor.baseAddress);
if (refusal != nullptr) return false;
auto* recorder = Recorder::Active();
if (recorder == nullptr) {
refusal = "no recorder";
return false;
}
// The source image must hold the surface: a stale one uploads first (and other images'
// results pending over its memory land first, as the transfer's flush would have them).
source.Refresh();
@@ -486,6 +486,25 @@ GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) {
return window;
}
GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers) {
const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube;
if (viewed.depthOrLastArray < maxArrayLayers || !layered) return viewed;
Require(viewed.dimension != TextureDimension::kCube, "an image with more layers than maxImageArrayLayers that is not a 1D or 2D array is not implemented");
Require(viewed.baseArray <= viewed.depthOrLastArray, "an image view's first array slice lies past its last");
Require(viewed.depthOrLastArray - viewed.baseArray < maxArrayLayers, "an image view of more array slices than maxImageArrayLayers is not implemented");
if (const auto xorMode = XorSwizzleMode(viewed.tileMode); xorMode != 0u) {
const auto* equation = FindTextureSwizzleEquation(xorMode, BytesPerElement(viewed.format));
Require(equation != nullptr && std::none_of(equation->bits.begin(), equation->bits.end(), [](std::uint32_t bit) { return (bit & 0xff000000u) != 0; }), "an image view past maxImageArrayLayers of a swizzle that depends on the slice is not implemented");
}
const auto geometry = DescribeSurface(viewed);
Require(!geometry.thick && geometry.layers == viewed.depthOrLastArray + 1u, "an array past maxImageArrayLayers must be a thin array");
auto window = viewed;
window.baseAddress = viewed.baseAddress + geometry.GuestLayerOffset(viewed.baseArray);
window.depthOrLastArray = viewed.depthOrLastArray - viewed.baseArray;
window.baseArray = 0;
return window;
}
std::vector<TileMipLayout> ComputeElementMipLayout(TextureTileMode tileMode, std::uint32_t bytesPerElement, std::uint32_t width, std::uint32_t height, std::uint32_t mipCount) {
Require(width != 0 && height != 0 && mipCount != 0 && mipCount <= 16u, "invalid surface mip chain");
if (tileMode == TextureTileMode::kLinear) return ComputeLinearMipLayout(bytesPerElement, 1u, 1u, width, height, mipCount);
@@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Resources.hpp"
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstring>
#include <initializer_list>
#include <limits>
@@ -15,6 +16,12 @@
namespace {
std::string Hex(std::uint64_t value) {
char text[24];
std::snprintf(text, sizeof(text), "0x%llx", static_cast<unsigned long long>(value));
return text;
}
using namespace AgcDriver::Graphics;
class Pipeline {
@@ -293,7 +300,7 @@ static void RunBdaAnchorTests(const Context& context) {
const auto expected = run(programs.plain, index, anchor, batch);
const auto actual = run(programs.anchored, index, anchor, batch);
const bool faultsMatch = programs.reads.lanes == 1u ? std::memcmp(&expected.fault, &actual.fault, sizeof(expected.fault)) == 0 : expected.fault.state == actual.fault.state;
Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords");
Require(faultsMatch && expected.values == actual.values, "an anchored BDA read differs from the same read through the lookups at anchor " + Hex(anchor) + " + " + std::to_string(batch[0]) + " + (" + std::to_string(read.immediate) + "), " + std::to_string(read.bits) + " bits, " + std::to_string(read.dwords) + " dwords");
}
}
}
@@ -307,6 +314,9 @@ static void RunBdaAnchorTests(const Context& context) {
for (std::uint32_t lane = 0; lane < 32u; ++lane) lows.push_back(lane * 0x24u);
std::vector<std::uint32_t> strided;
for (std::uint32_t lane = 0; lane < lanes; ++lane) strided.push_back(lane * 16u);
auto halfword = build({{{-1, 16, 1, 1}}, false, false, false, lanes});
compare(*halfword, {guest + 0x100}, {1u});
compare(*halfword, {guest + 0x100}, {0u, 1u});
auto continuing = build({reads, false, false, false, lanes});
compare(*continuing, anchors, lows);
compare(*continuing, {guest + 0x400}, strided);
@@ -326,11 +336,11 @@ static void RunBdaAnchorTests(const Context& context) {
};
const auto loads = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low, std::initializer_list<std::uint32_t> values) {
const auto outcome = run(*direct, read, anchor, {low});
Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read));
Require(outcome.fault.state == Abi::FaultState::Empty && std::equal(values.begin(), values.end(), outcome.values.begin()), "a read the anchor holds did not load directly at anchor " + Hex(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read));
};
const auto misses = [&](std::uint32_t read, std::uint64_t anchor, std::uint32_t low) {
const auto outcome = run(*direct, read, anchor, {low});
Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor 0x" + std::to_string(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read));
Require(outcome.fault.state == Abi::FaultState::Empty && outcome.values[0] == BdaAnchoredReadSentinel, "a read the anchor does not hold did not take the lookup path at anchor " + Hex(anchor) + " + " + std::to_string(low) + ", read " + std::to_string(read));
};
loads(2, guest + 0x100, 0, {dword(words, 0)});
loads(2, guest + 0x100, 0xc, {dword(words, 0xc)});
@@ -511,6 +521,20 @@ void RunBdaExecutionTests(const Context& context) {
}
ranges[0].permissions = 0;
run(guest, 8, 0, Abi::FaultReason::Permission);
const auto sequence = [&](std::initializer_list<Abi::FaultReason> reasons, Abi::FaultReason recorded, std::uint32_t otherFaults) {
const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0};
std::memcpy(table.Bytes().data(), &header, sizeof(header));
std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges));
std::memset(fault.Bytes().data(), 0, fault.Bytes().size());
Pipeline pipeline(context, MakeBdaFaultSequenceTestShader(reasons), {&table, &fault, &output});
pipeline.Run(1);
Abi::Fault report{};
std::memcpy(&report, fault.Bytes().data(), sizeof(report));
Require(report.state == Abi::FaultState::Ready && report.reason == recorded && report.otherFaults == otherFaults, "BDA faults of one dispatch were not recorded with the first reason and the sticky other-fault flag");
};
sequence({Abi::FaultReason::Unaligned, Abi::FaultReason::Unaligned}, Abi::FaultReason::Unaligned, 0);
sequence({Abi::FaultReason::Unaligned, Abi::FaultReason::Unmapped}, Abi::FaultReason::Unaligned, 1);
sequence({Abi::FaultReason::Unmapped, Abi::FaultReason::Unaligned}, Abi::FaultReason::Unmapped, 1);
RunBdaDwordWriteTests(context);
RunBdaAnchorTests(context);
}
@@ -544,9 +568,9 @@ void RunBdaStoreExecutionTests(const Context& context) {
Abi::Fault report{};
std::memcpy(&report, fault.Bytes().data(), sizeof(report));
if (reason == Abi::FaultReason{}) {
Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.reserved == 0, label + "mapped store published a fault");
Require(report.state == Abi::FaultState::Empty && report.reason == Abi::FaultReason{} && report.address == 0 && report.bytes == 0 && report.stage == 0 && report.instruction == 0 && report.otherFaults == 0, label + "mapped store published a fault");
} else {
Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast<std::uint32_t>(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.reserved == 0, label + "incorrect first fault metadata");
Require(report.state == Abi::FaultState::Ready && report.reason == reason && report.address == faultAddress && report.bytes == faultBytes && report.stage == static_cast<std::uint32_t>(ShaderRecompiler::IrShaderStage::Compute) && report.instruction == 0x1234 && report.otherFaults == 1, label + "incorrect first fault metadata");
}
std::array<std::uint32_t, Abi::FaultBufferBytes / sizeof(std::uint32_t)> words{};
std::memcpy(words.data(), fault.Bytes().data(), sizeof(words));
@@ -1372,6 +1372,212 @@ void importRefusalMemoryTests(const Context& context) {
::operator delete(block, std::align_val_t{piece});
}
void evictingRefusalMemoryTests(const Context& context) {
constexpr std::size_t piece = 65536;
constexpr std::size_t filled = 32;
constexpr std::size_t count = filled + 3;
auto importing = context;
importing.hostImportAlignment = piece;
auto* block = static_cast<std::uint8_t*>(::operator new(2 * count * piece, std::align_val_t{piece}));
const auto at = [&](std::size_t index) { return block + 2 * index * piece; };
const auto address = [&](std::size_t index) { return reinterpret_cast<std::uintptr_t>(at(index)); };
ClearHostImports(importing.device);
{
GuestAllocations::Mutation mutation;
for (std::size_t index = 0; index < count; ++index) mutation.Add(at(index), piece, true, true, true);
}
const auto budget = HostImportLiveBytes() + filled * piece;
SetHostImportBudget(budget);
for (std::size_t index = 0; index < filled; ++index) Require(HostImportFor(importing, address(index), 16) != nullptr, "a piece inside the import budget was not imported");
Require(HostImportLiveBytes() == budget, "the pieces did not fill the import budget");
const auto counted = [] { return HostImportCounters().requesters[static_cast<std::size_t>(ImportRequester::Access)]; };
auto before = counted();
for (int request = 0; request < 8; ++request) Require(HostImportFor(importing, address(filled), 16) == nullptr, "a piece over a full budget with nothing evictable was imported");
auto after = counted();
Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 evicting requests for one refused piece at a full budget with nothing evictable scanned the imports " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once");
SetHostImportBudget(budget - piece);
before = counted();
Require(HostImportFor(importing, address(filled), 16) == nullptr && counted().remembered == before.remembered + 1, "a remembered evicting refusal ended when the budget was lowered");
SetHostImportBudget(budget);
{
GuestAllocations::Mutation mutation;
mutation.Remove(at(0));
}
before = counted();
Require(HostImportFor(importing, address(filled), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after an import retired");
static_cast<void>(HostImportFor(importing, address(filled + 1), 16));
before = counted();
Require(HostImportFor(importing, address(filled + 1), 16) == nullptr && counted().remembered == before.remembered + 1, "a second evicting request for a refused piece was not answered from memory");
SetHostImportBudget(budget + piece);
before = counted();
Require(HostImportFor(importing, address(filled + 1), 16) != nullptr && counted().remembered == before.remembered, "an evicting request was answered from its remembered refusal after the budget was raised");
static_cast<void>(HostImportFor(importing, address(filled + 2), 16));
{
GuestBufferMemory upload(importing);
upload.Upload(false);
upload.WriteBack();
}
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");
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");
{
GuestAllocations::Mutation mutation;
for (std::size_t index = 1; index < count; ++index) mutation.Remove(at(index));
}
ClearHostImports(importing.device);
SetHostImportBudget(0);
::operator delete(block, std::align_val_t{piece});
}
void evictingRefusalAgingTests(const Context& context) {
constexpr std::size_t piece = 65536;
constexpr std::size_t filled = 16;
auto importing = context;
importing.hostImportAlignment = piece;
auto* block = static_cast<std::uint8_t*>(::operator new(2 * (filled + 1) * piece, std::align_val_t{piece}));
const auto at = [&](std::size_t index) { return block + 2 * index * piece; };
const auto address = [&](std::size_t index) { return reinterpret_cast<std::uintptr_t>(at(index)); };
ClearHostImports(importing.device);
{
GuestAllocations::Mutation mutation;
for (std::size_t index = 0; index <= filled; ++index) mutation.Add(at(index), piece, true, true, true);
}
SetHostImportBudget(HostImportLiveBytes() + filled * piece);
for (std::size_t index = 0; index < filled; ++index) Require(HostImportFor(importing, address(index), 16) != nullptr, "a piece inside the import budget was not imported");
{
GuestBufferMemory upload(importing);
upload.Upload(false);
upload.WriteBack();
}
const auto counted = [] { return HostImportCounters().requesters[static_cast<std::size_t>(ImportRequester::Access)]; };
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");
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");
{
GuestAllocations::Mutation mutation;
for (std::size_t index = 0; index <= filled; ++index) mutation.Remove(at(index));
}
ClearHostImports(importing.device);
SetHostImportBudget(0);
::operator delete(block, std::align_val_t{piece});
}
void evictingRefusalRangeTests(const Context& context) {
PieceWindow window(context);
constexpr auto piece = PieceWindow::Piece;
constexpr auto fillers = PieceWindow::Window / 2;
window.Add(0, 2 * piece);
window.Add(2 * piece, piece);
window.Add(3 * piece, piece);
for (std::size_t filler = 0; filler < 4; ++filler) {
window.Add(fillers + 2 * filler * piece, piece);
Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported");
}
SetHostImportBudget(HostImportLiveBytes() + piece);
Require(HostImportFor(window.importing, window.Address(0), 16) == nullptr, "a 128 KiB range in a 256 KiB block over the budget was imported");
Require(HostImportFor(window.importing, window.Address(2 * piece), 16) != nullptr && window.Block(2 * piece, piece), "a 64 KiB range of the same 256 KiB block was answered from the 128 KiB range's refusal instead of stepping down to its own import");
}
void evictingRefusalBlockRangesTests(const Context& context) {
PieceWindow window(context);
constexpr auto piece = PieceWindow::Piece;
constexpr auto fillers = PieceWindow::Window / 2;
window.AddPieces(0, 4 * piece);
for (std::size_t filler = 0; filler < 4; ++filler) {
window.Add(fillers + 2 * filler * piece, piece);
Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported");
}
SetHostImportBudget(HostImportLiveBytes());
const auto counted = [] { return HostImportCounters().requesters[static_cast<std::size_t>(ImportRequester::Access)]; };
const auto before = counted();
for (int request = 0; request < 8; ++request) Require(HostImportFor(window.importing, window.Address(static_cast<std::size_t>(request % 2) * piece), 16) == nullptr, "a 64 KiB range of a block over a full budget with nothing evictable was imported");
const auto after = counted();
Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 evicting requests alternating between two 64 KiB ranges of one refused 256 KiB block scanned the imports " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once");
SetHostImportBudget(HostImportLiveBytes() + piece);
Require(HostImportFor(window.importing, window.Address(piece), 16) != nullptr && window.Block(piece, piece), "a 64 KiB range of the block was answered from the full budget's refusal instead of stepping down to its own import once the budget had room for it");
}
void evictingRefusalJoinTests(const Context& context) {
PieceWindow window(context);
constexpr auto piece = PieceWindow::Piece;
constexpr auto fillers = PieceWindow::Window / 2;
window.AddPieces(0, 3 * piece);
for (std::size_t filler = 0; filler < 4; ++filler) {
window.Add(fillers + 2 * filler * piece, piece);
Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported");
}
SetHostImportBudget(HostImportLiveBytes());
const auto upload = [&] {
GuestBufferMemory plain(window.importing);
plain.AddReadable(window.Address(0), 3 * piece);
plain.Upload(false);
plain.WriteBack();
};
const auto counted = [] { return HostImportCounters().requesters[static_cast<std::size_t>(ImportRequester::Join)]; };
const auto before = counted();
for (int request = 0; request < 8; ++request) upload();
const auto after = counted();
Require(!HostImportCovers(window.importing, window.Address(0), 3 * piece), "a join of three 64 KiB ranges over a full budget with nothing evictable was imported");
Require(after.refused - before.refused == 8 && after.remembered - before.remembered == 7, "8 uploads bound across three refused ranges scanned the imports for their join " + std::to_string(8 - (after.remembered - before.remembered)) + " times, not once");
SetHostImportBudget(HostImportLiveBytes() + 3 * piece);
upload();
Require(HostImportCovers(window.importing, window.Address(0), 3 * piece) && counted().made == after.made + 1, "the join was answered from the full budget's refusal after the budget grew");
}
void evictingRefusalStepDownTests(const Context& context) {
PieceWindow window(context);
constexpr auto piece = PieceWindow::Piece;
constexpr auto fillers = PieceWindow::Window / 2;
window.AddPieces(0, 4 * piece);
for (std::size_t filler = 0; filler < 4; ++filler) {
window.Add(fillers + 2 * filler * piece, piece);
Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported");
}
SetHostImportBudget(HostImportLiveBytes() + 2 * piece);
{
GuestBufferMemory leased(window.importing);
leased.AcquireRegistered();
leased.Upload(true);
leased.WriteBack();
}
Require(window.Block(0, 2 * piece) && window.Serial(2 * piece) == 0, "the address-based build did not step down from the refused 256 KiB block to the 128 KiB block that fits");
{
GuestBufferMemory upload(window.importing);
upload.Upload(false);
upload.WriteBack();
}
const auto counted = [] { return HostImportCounters().requesters[static_cast<std::size_t>(ImportRequester::Access)]; };
const auto before = counted();
for (int request = 0; request < 8; ++request) Require(HostImportFor(window.importing, window.Address(2 * piece), 16) == nullptr, "the rest of a block over a full budget with nothing evictable was imported");
const auto after = counted();
const auto scans = (after.refused - before.refused) - (after.remembered - before.remembered);
Require(after.refused - before.refused == 16 && scans == 1, "8 evicting requests that step down from a refused block scanned the imports " + std::to_string(scans) + " times, not once");
SetHostImportBudget(HostImportLiveBytes() + piece);
Require(HostImportFor(window.importing, window.Address(2 * piece), 16) != nullptr && window.Block(2 * piece, piece), "a 64 KiB range was answered from a block's refusal remembered before the budget grew instead of stepping down to its own import");
}
void evictingRefusalWindowTests(const Context& context) {
PieceWindow window(context);
constexpr auto piece = PieceWindow::Piece;
constexpr auto fillers = PieceWindow::Window / 2;
SetHostImportWindow(0);
window.Add(0, piece);
window.Add(piece, piece);
for (std::size_t filler = 0; filler < 4; ++filler) {
window.Add(fillers + 2 * filler * piece, piece);
Require(HostImportFor(window.importing, window.Address(fillers + 2 * filler * piece), 16) != nullptr, "a filler piece inside the import budget was not imported");
}
SetHostImportBudget(HostImportLiveBytes() + piece);
Require(HostImportFor(window.importing, window.Address(0), 16) == nullptr, "two adjacent 64 KiB ranges over the budget were imported without an import window");
SetHostImportWindow(PieceWindow::Window);
Require(HostImportFor(window.importing, window.Address(0), 16) != nullptr && window.Block(0, piece), "a 64 KiB range was answered from a refusal made without the import window instead of stepping down once the window was set");
}
void importDeviceBudgetTests() {
constexpr std::uint64_t MiB = std::uint64_t{1} << 20u;
constexpr std::uint64_t GiB = std::uint64_t{1} << 30u;
@@ -3032,6 +3238,27 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
reject([&] { resources.WriteBack(); }, "rect-list requires");
Require(guest[0] == 123, "failed GPU command published writes");
}
{
const auto fail = [&](ShaderRecompiler::BdaAbi::FaultReason reason, std::uint32_t word, std::uint32_t value, std::uint32_t otherFaults = 0) {
ShaderResources resources(context, compiled);
std::memcpy(access.bytes(access.descriptor(6).buffer).data() + word * sizeof(std::uint32_t), &value, sizeof(value));
const ShaderRecompiler::BdaAbi::Fault report{ShaderRecompiler::BdaAbi::FaultState::Ready, reason, 0x7fff99880002ULL, 4, 0, 0x5c8, otherFaults};
std::memcpy(access.bytes(access.descriptor(5).buffer).data(), &report, sizeof(report));
try {
resources.WriteBack();
} catch (const BdaUnalignedAccess& error) {
Require(std::string(error.what()).find("BDA access failed") != std::string::npos, "an unaligned BDA access did not report the failed access");
return true;
} catch (const std::runtime_error& error) {
Require(std::string(error.what()).find("BDA access failed") != std::string::npos, "a BDA fault did not report the failed access");
return false;
}
throw std::runtime_error("a recorded BDA fault was not reported");
};
Require(fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 2, 777) && guest[2] == 777, "an unaligned BDA access lost the stores of the dispatch's other invocations");
Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unmapped, 3, 888) && guest[3] == 0, "a failed GPU command published writes");
Require(!fail(ShaderRecompiler::BdaAbi::FaultReason::Unaligned, 5, 999, 1) && guest[5] == 0, "an unaligned BDA access that hid a fault of another kind published writes");
}
{
auto aligned = context;
aligned.limits.minStorageBufferOffsetAlignment = 16;
@@ -3153,6 +3380,13 @@ void RunBdaResourceTests(const Context& context, const BdaTestAccess& access) {
importBlockFailureTests(context, access);
importBlockBudgetTests(context);
importRefusalMemoryTests(context);
evictingRefusalMemoryTests(context);
evictingRefusalAgingTests(context);
evictingRefusalRangeTests(context);
evictingRefusalWindowTests(context);
evictingRefusalBlockRangesTests(context);
evictingRefusalJoinTests(context);
evictingRefusalStepDownTests(context);
importDeviceBudgetTests();
importCeilingTests(context, access, false);
importCeilingTests(context, access, true);
@@ -134,6 +134,14 @@ std::vector<std::uint32_t> MakeBdaWriteTestShader(std::uint64_t address, std::ui
return shader;
}
std::vector<std::uint32_t> MakeBdaFaultSequenceTestShader(std::initializer_list<ShaderRecompiler::BdaAbi::FaultReason> reasons) {
using namespace ShaderRecompiler;
return MakeShader(0u, false, true, [&](SpirvValueEmitContext& ctx, const IrValue&, std::uint32_t base) {
for (const auto reason : reasons) RecordBdaFault(ctx.state, base, ConstantU32(ctx.state, 4u), ConstantU32(ctx.state, 0x1234u), reason);
return std::vector<std::uint32_t>{ConstantU32(ctx.state, 1u)};
});
}
std::vector<std::uint32_t> MakeBdaAnchoredReadTestShader(const BdaAnchoredReadProgram& reads, bool anchored) {
using namespace ShaderRecompiler;
IrProgram program;
@@ -2,12 +2,15 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_BDASHADER_HPP
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "BdaAbi.hpp"
#include <cstdint>
#include <initializer_list>
#include <vector>
std::vector<std::uint32_t> MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset = 0);
std::vector<std::uint32_t> MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops);
std::vector<std::uint32_t> MakeBdaSpanReadTestShader(std::uint64_t address, std::uint32_t offset, std::uint32_t extracted, bool coherent, bool stops);
std::vector<std::uint32_t> MakeBdaFaultSequenceTestShader(std::initializer_list<ShaderRecompiler::BdaAbi::FaultReason> reasons);
std::vector<std::uint32_t> MakeBdaDwordWriteTestShader(std::uint64_t address, std::uint32_t dwords, const std::uint32_t* values);
std::vector<std::uint32_t> MakeBdaStoreTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent);
std::vector<std::uint32_t> MakeBdaWriteTestShader(std::uint64_t address, std::uint32_t value, std::uint32_t bits, bool coherent);
@@ -260,8 +260,17 @@ void CheckOptionalInvalid() {
Reject([&] { capture.WriteOptional(directory.path); }, "optional capture swallowed a non-directory existing record");
Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(path.parent_path()) == 1u, "non-directory record disabled optional capture or was changed");
Directory unexpected;
const auto finder = unexpected.path / ".DS_Store";
std::ofstream(finder, std::ios::binary).put('x');
capture.specialization[0].value = 1u;
Require(!capture.Write(unexpected.path).empty(), "strict capture rejected a Finder metadata file");
capture.specialization[0].value = 2u;
Require(!capture.WriteOptional(unexpected.path).empty() && AgcDriver::ComputePipelineCapture::OptionalEnabled(), "optional capture rejected a Finder metadata file");
Require(std::filesystem::file_size(finder) == 1u, "Finder metadata file was changed");
capture.specialization[0].value = 3u;
const auto foreign = unexpected.path / "foreign";
std::ofstream(foreign, std::ios::binary).put('x');
RejectWithMessage([&] { capture.Write(unexpected.path); }, "capture directory contains an unexpected entry");
Reject([&] { capture.WriteOptional(unexpected.path); }, "optional capture swallowed an unexpected directory entry");
Require(AgcDriver::ComputePipelineCapture::OptionalEnabled() && std::filesystem::file_size(foreign) == 1u, "unexpected entry disabled optional capture or was changed");
}
@@ -29,6 +29,122 @@ bool foldsTo(std::uint32_t word, bool expected, bool maskedShift) {
return result->HasImmediate() && result->ImmediateBool() == expected;
}
bool masksFold(std::uint32_t inner, std::uint32_t outer) {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane});
auto& first = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(inner)});
auto& second = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&first, &builder.Constant(outer)});
static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&first}));
auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&second});
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
ConstantFolder().Fold(program);
const IrValue* result = user.Argument(0)->Resolve();
if ((inner & ~outer) == 0u) return result == &first;
return result->Opcode() == IrOpcode::BitwiseAnd32 && result->Argument(0)->Resolve() == &sum && result->Argument(1)->Resolve()->ImmediateU32() == outer;
}
bool mergedHalfMask() {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane});
auto& low = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)});
auto& high = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&lane, &builder.Constant(16u)});
auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &high});
auto& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&merged, &builder.Constant(0x7fffu)});
static_cast<void>(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&low}));
auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&masked});
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
ConstantFolder().Fold(program);
const IrValue* result = user.Argument(0)->Resolve();
return result->Opcode() == IrOpcode::BitwiseAnd32 && result->Argument(0)->Resolve() == &sum;
}
bool halfExtractions() {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
auto& low = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(3u), &lane});
auto& high = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(5u), &lane});
auto& lowHalf = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&low, &builder.Constant(0xffffu)});
auto& highHalf = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)});
auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&lowHalf, &highHalf});
auto& top = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&merged, &builder.Constant(16u)});
auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(16u), &builder.Constant(16u)});
auto& bottom = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(0u), &builder.Constant(16u)});
auto& topUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&top});
auto& fieldUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&field});
auto& bottomUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&bottom});
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
ConstantFolder().Fold(program);
const IrValue* topResult = topUser.Argument(0)->Resolve();
const IrValue* fieldResult = fieldUser.Argument(0)->Resolve();
const IrValue* bottomResult = bottomUser.Argument(0)->Resolve();
const bool topMasked = topResult->Opcode() == IrOpcode::BitwiseAnd32 && topResult->Argument(0)->Resolve() == &high && topResult->Argument(1)->Resolve()->ImmediateU32() == 0xffffu;
const bool fieldThrough = fieldResult->Opcode() == IrOpcode::BitFieldUExtract && fieldResult->Argument(0)->Resolve() == &high && fieldResult->Argument(1)->Resolve()->ImmediateU32() == 0u;
const bool bottomThrough = bottomResult->Opcode() == IrOpcode::BitFieldUExtract && bottomResult->Argument(0)->Resolve() == &low;
return topMasked && fieldThrough && bottomThrough;
}
bool halvesRejoin() {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
auto& word = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(7u), &lane});
auto& low = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(0u), &builder.Constant(16u)});
auto& high = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(16u), &builder.Constant(16u)});
auto& raised = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)});
auto& joined = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &raised});
auto& shifted = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&word, &builder.Constant(16u)});
auto& back = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&shifted, &builder.Constant(16u), &builder.Constant(16u)});
auto& joinedUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&joined});
auto& backUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&back});
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
ConstantFolder().Fold(program);
const IrValue* rejoined = joinedUser.Argument(0)->Resolve();
const IrValue* field = backUser.Argument(0)->Resolve();
const bool fieldBack = field->Opcode() == IrOpcode::BitFieldUExtract && field->Argument(0)->Resolve() == &word && field->Argument(1)->Resolve()->ImmediateU32() == 0u;
return rejoined == &word && fieldBack;
}
bool factorFieldKept(bool multiplied) {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
auto& sum = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(1u), &lane});
auto& masked = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&sum, &builder.Constant(0xffffu)});
auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&masked, &builder.Constant(0u), &builder.Constant(24u)});
auto& used = multiplied ? builder.IMul(field, field) : field;
auto& user = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&used});
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
ConstantFolder().Fold(program);
const IrValue* result = user.Argument(0)->Resolve();
if (!multiplied) return result == &masked;
return result->Opcode() == IrOpcode::IMul32 && result->Argument(0)->Resolve() == &field && field.Opcode() == IrOpcode::BitFieldUExtract && field.Argument(0)->Resolve() == &masked;
}
bool reversesTo(std::uint32_t word, std::uint32_t expected) {
IrProgram program;
auto& block = program.CreateBlock();
@@ -118,6 +234,14 @@ int main() {
passed &= run("a reversed high bit must fold to bit zero", reversesTo(0x80000000u, 0x00000001u));
passed &= run("a reversed mixed word must fold to its mirror image", reversesTo(0x12345678u, 0x1e6a2c48u));
passed &= run("a reversed descriptor dword must fold to its mirror image", reversesTo(0xdeadbeefu, 0xf77db57bu));
passed &= run("a repeated mask folds into the first one", masksFold(0xffffu, 0xffffu));
passed &= run("a narrower mask reads through the wider one", masksFold(0xffffu, 0x7fffu));
passed &= run("a wider mask after a narrower one is dropped", masksFold(0x00ffu, 0xffffu));
passed &= run("a mask under a merge of the halves reads through it", mergedHalfMask());
passed &= run("half extractions read the half that was merged in", halfExtractions());
passed &= run("the two halves of one word rejoin into the word", halvesRejoin());
passed &= run("a 24-bit field read of a value masked to 16 bits folds into the value", factorFieldKept(false));
passed &= run("a 24-bit field read that a multiply uses is kept, even for a factor masked to 16 bits", factorFieldKept(true));
return passed ? 0 : 1;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
@@ -471,6 +471,31 @@ void verifyLongBranch(const std::string& name, std::span<const std::uint32_t> co
require(source != graph.blocks.end() && source->terminator.kind == TerminatorKind::Branch && graph.FindBlock(source->terminator.trueBlock).startProgramCounter == target, name + ": the jump does not branch to " + std::to_string(target));
}
void verifyScheduledJumpTable() {
const std::vector<std::uint32_t> code{
0x83888208u, 0x7e020280u, 0x8f088308u, 0x7e040280u, 0xbe8c1f00u, 0x800cff0cu, 0x00000048u,
0x820d800du, 0xf4040386u, 0x10000000u, 0xbf8cc07fu, 0xbe901f00u, 0x80100e10u, 0x82110f11u,
0xbe802010u, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, 0xe0700000u,
0x80000100u, 0xbf810000u, 0x0000000cu, 0u, 0x00000014u, 0u, 0x0000001cu, 0u,
};
verifyJumpTable("scheduled two-dword table", code, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu});
static_cast<void>(recompile("scheduled two-dword table", code, 32u));
static_cast<void>(recompile("scheduled two-dword table", code, 64u));
auto vcc = patched(code, 0, {0x83ea826au});
vcc = patched(std::move(vcc), 2, {0x8f6a836au});
vcc = patched(std::move(vcc), 9, {0xd4000000u});
verifyJumpTable("scheduled two-dword table through vcc", vcc, 0x20u, {0xcu, 0x14u, 0x1cu}, {0x3cu, 0x44u, 0x4cu});
static_cast<void>(recompile("scheduled two-dword table through vcc", vcc, 32u));
static_cast<void>(recompile("scheduled two-dword table through vcc", vcc, 64u));
const std::vector<std::vector<std::uint32_t>> refused{
patched(code, 1, {0xbe880380u}),
patched(code, 3, {0xbe880380u}),
patched(code, 1, {0xbf820001u}),
patched(code, 3, {0xbf850000u}),
};
for (const auto& rejected : refused) require(refusal(rejected).find("unsupported dynamic s_setpc_b64") != std::string::npos, "a rewritten or conditional two-dword table index was accepted");
}
void verifyNullSwappc() {
const std::array<std::uint32_t, 2> jump{0xbefd210cu, 0xbf810000u};
const auto swappc = RdnaInstructionDecoder{}.Decode(jump).instructions.front();
@@ -1369,6 +1394,7 @@ done:
}
try {
verifyNullSwappc();
verifyScheduledJumpTable();
VerifyUserDataTail();
VerifyConstantHighTail();
verifyLargeLoopExitFallback();
+134 -13
View File
@@ -12,6 +12,7 @@
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "ShaderDiskCache.hpp"
#include "ShaderCacheDirectory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
@@ -51,6 +52,7 @@
#include <random>
#include <initializer_list>
#include <iostream>
#include <limits>
#include <map>
#include <optional>
#include <set>
@@ -1673,7 +1675,12 @@ void DisabledColorTests() {
Require(!state.hasColorTarget && state.color.address == 0 && state.color.bytes == 0, "disabled color writes accessed a color surface");
Require(state.renderExtent.width == 64 && state.renderExtent.height == 4, "attachment-free framebuffer lost screen scissor extent");
queue.context[0xd] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "empty framebuffer extent");
const auto emptyScreen = AgcDriver::Graphics::DecodeState(queue);
Require(emptyScreen.renderExtent.width == 1 && emptyScreen.renderExtent.height == 1, "an empty screen scissor without targets did not get a one-pixel framebuffer");
Require(emptyScreen.scissor.extent.width == 0 && emptyScreen.scissor.extent.height == 0, "an empty screen scissor without targets rasterized pixels");
queue.context[0xd] = 64u;
const auto emptyRows = AgcDriver::Graphics::DecodeState(queue);
Require(emptyRows.renderExtent.width == 64 && emptyRows.scissor.extent.height == 0, "a screen scissor without rows rasterized pixels");
queue = makeState();
queue.context[0x8e] = 3;
const auto partial = AgcDriver::Graphics::DecodeState(queue);
@@ -2596,6 +2603,28 @@ void DepthBoundsBiasTests() {
queue.context[0x009] = bits(0.75f);
auto state = AgcDriver::Graphics::DecodeState(queue);
Require(state.depthTest && state.depthBoundsTest && state.minDepthBounds == 0.25f && state.maxDepthBounds == 0.75f, "depth bounds decode changed");
const auto infinity = std::numeric_limits<float>::infinity();
for (const auto bounds : {std::array{0.0f, infinity}, std::array{-infinity, 1.0f}, std::array{-infinity, infinity}}) {
queue.context[0x008] = bits(bounds[0]);
queue.context[0x009] = bits(bounds[1]);
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.depthBoundsTest && state.minDepthBounds == bounds[0] && state.maxDepthBounds == bounds[1], "infinite depth bounds changed");
AgcDriver::Graphics::Context context{};
expectFailure([&] { AgcDriver::Graphics::ValidateDepthBounds(context, state); }, "depth bounds outside [0, 1]");
context.depthRangeUnrestricted = true;
AgcDriver::Graphics::ValidateDepthBounds(context, state);
}
for (const auto offset : {0x008u, 0x009u}) {
queue.context[0x008] = bits(0.25f);
queue.context[0x009] = bits(0.75f);
queue.context[offset] = bits(std::numeric_limits<float>::quiet_NaN());
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NaN depth bound");
}
queue.context[0x008] = bits(0.25f);
queue.context[0x009] = bits(0.75f);
queue.context[0x00b] = bits(infinity);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite register");
queue.context[0x00b] = bits(0.0f);
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
@@ -4814,6 +4843,7 @@ AgcDriver::Graphics::Context mockContext() {
context.limits.maxPerStageDescriptorStorageBuffers = 16;
context.limits.maxPerStageResources = 128;
context.limits.maxDescriptorSetStorageBuffers = 32;
context.limits.maxImageArrayLayers = 2048;
return context;
}
@@ -6401,10 +6431,10 @@ void storageTextureBudgetTests() {
Require(StorageTextureBudget(memory, &reported, 0, 0) == 2560 * MiB, "a 10 GiB budget on the 16 GiB heap does not limit the storage image cache to a quarter of the budget");
reported.heapUsage[2] = 5 * GiB;
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "2 GiB used outside the texture caches on a 10 GiB budget lowered the storage image cache below its quarter");
reported.heapUsage[2] = 8 * GiB;
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 1792 * MiB, "a 10 GiB budget with 5 GiB used outside the texture caches does not leave 1792 MiB after the sampled textures' 2 GiB floor and 1280 MiB of headroom");
reported.heapUsage[2] = 8 * GiB + 512 * MiB;
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2304 * MiB, "a 10 GiB budget with 5632 MiB used outside the texture caches does not leave 2304 MiB after 1 GiB of sampled textures and 1280 MiB of headroom");
reported.heapUsage[2] = 9 * GiB;
Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved past their 2 GiB floor");
Require(StorageTextureBudget(memory, &reported, 3 * GiB, 2 * GiB) == 1792 * MiB, "3 GiB of sampled textures were not reserved in full");
reported.heapUsage[2] = 1 * GiB;
Require(StorageTextureBudget(memory, &reported, 1 * GiB, 2 * GiB) == 2560 * MiB, "usage below the cached bytes is not read as no other device memory in use");
Require(StorageTextureBudget(memory, &reported, 7 * GiB, 1 * GiB) == 1792 * MiB, "7 GiB of sampled textures with the usage below the cached bytes do not leave 1792 MiB after their reservation and 1280 MiB of headroom");
@@ -6417,6 +6447,35 @@ void storageTextureBudgetTests() {
Require(StorageTextureBudget(memory, &reported, 0, 0) == 512 * MiB, "a 1 GiB budget does not keep the 512 MiB floor");
reported.heapBudget[2] = 40 * GiB;
Require(StorageTextureBudget(memory, &reported, 0, 0) == 4 * GiB, "a budget above the heap size raised the storage image cache past a quarter of the heap");
memory.memoryHeaps[2].size = 8 * GiB;
reported.heapBudget[2] = 7189 * MiB;
reported.heapUsage[2] = 4491 * MiB;
Require(StorageTextureBudget(memory, &reported, 49 * MiB, 506 * MiB) == 7189 * MiB / 4u, "49 MiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches held the storage image cache below a quarter of the budget");
reported.heapUsage[2] = 6490 * MiB;
Require(StorageTextureBudget(memory, &reported, 2 * GiB, 506 * MiB) == 512 * MiB, "2 GiB of sampled textures on an 8 GiB heap with a 7189 MiB budget and 3936 MiB used outside the texture caches did not take the storage image cache back to its 512 MiB floor");
memory.memoryHeaps[2].size = 10 * GiB;
reported.heapBudget[2] = 8767 * MiB;
reported.heapUsage[2] = 7981 * MiB;
Require(StorageTextureBudget(memory, &reported, 1427 * MiB, 490 * MiB) == 512 * MiB, "a 10 GiB heap at 7981 of its 8767 MiB budget with 1427 MiB of sampled textures does not keep the storage image cache at its 512 MiB floor");
for (const auto heapGiB : {2u, 4u, 6u, 8u, 16u, 32u}) {
memory.memoryHeaps[2].size = heapGiB * GiB;
for (std::uint64_t budget = GiB; budget <= heapGiB * GiB; budget += 512 * MiB) {
reported.heapBudget[2] = budget;
for (std::uint64_t sampled = 0; sampled <= budget; sampled += 256 * MiB) {
std::uint64_t previous = 0;
for (std::uint64_t outside = 0; outside <= budget + GiB; outside += 128 * MiB) {
reported.heapUsage[2] = outside + sampled + 512 * MiB;
const auto limit = StorageTextureBudget(memory, &reported, sampled, 512 * MiB);
Require(limit >= 512 * MiB, "pressure sweep lost the storage cache floor");
Require(limit <= std::max<std::uint64_t>(512 * MiB, budget / 4u), "pressure sweep exceeded a quarter of the reported budget above the floor");
Require(limit <= std::max<std::uint64_t>(2 * GiB, heapGiB * GiB / 4u), "pressure sweep exceeded the heap storage cache cap");
if (outside != 0) Require(limit <= previous, "additional non-cache memory usage grew the storage cache budget");
if (limit > 512 * MiB) Require(outside + sampled + budget / 8u + limit <= budget, "storage cache budget consumed live sampled memory or pressure headroom");
previous = limit;
}
}
}
}
memory.memoryHeaps[0].flags = 0;
memory.memoryHeaps[2].flags = 0;
Require(StorageTextureBudget(memory, &reported, 0, 0) == 2 * GiB, "a device without a device-local heap does not keep 2 GiB of storage images");
@@ -8721,6 +8780,14 @@ void unorm10_11_11TargetTests() {
auto plainState = state;
plainState.colors[0].packing = ColorExportPacking::None;
expectFailure([&] { validate(packed, plainState); }, "float4 colors");
auto targetless = plainState;
targetless.colors.clear();
targetless.blends.clear();
Require(validate(packed, targetless) == std::set<std::uint32_t>{0u}, "a uint4 export from a draw with no color target was refused");
auto gap = plainState;
gap.blends.insert(gap.blends.begin(), gap.blends.front());
gap.colors[0].exportIndex = 1;
Require(validate(packed, gap) == std::set<std::uint32_t>{0u}, "a uint4 export into the unused slot below a color target was refused");
auto uintExport = request;
uintExport.context.pixel->targetOutputMode[0] = 7;
expectFailure([&] { static_cast<void>(ShaderRecompiler::Recompile(uintExport)); }, "fragment output 0 has no color export packing path");
@@ -9721,21 +9788,36 @@ void pipelineStoreLazyBufferPoolTests() {
Require(mock.live == 0, "lazy buffer pool pipeline tests leaked Vulkan objects");
}
void pipelineJournalTests() {
void pipelineJournalTests(bool longPath = false, bool extended = false) {
using namespace AgcDriver::Graphics;
using Stage = ShaderRecompiler::ShaderStage;
const auto directory = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}()));
const auto root = std::filesystem::temp_directory_path() / ("aps5-pipeline-journal-mock-" + std::to_string(std::random_device{}()));
auto directory = root;
if (longPath) {
Require(root.native().size() < 180, "the temporary root is too long for the journal path fixture");
directory /= std::filesystem::path(u8"cache-\u00e4\u6f22");
directory /= std::string(229 - directory.native().size(), 'd');
}
std::filesystem::create_directories(directory);
struct Cleanup {
std::filesystem::path directory;
std::filesystem::path journal;
~Cleanup() {
ShaderRecompiler::ShaderDiskCache::Shutdown();
std::error_code error;
if (!journal.empty()) {
std::filesystem::remove(journal, error);
auto lock = journal;
lock += ".lock";
std::filesystem::remove(lock, error);
}
std::filesystem::remove_all(directory, error);
}
} cleanup{directory};
} cleanup{root, {}};
#ifdef _WIN32
if (extended) directory = std::filesystem::path(L"\\\\?\\" + directory.make_preferred().native());
_putenv_s("ANYPS5_NO_SHADER_CACHE", "0");
_putenv_s("ANYPS5_SHADER_CACHE_DIR", directory.string().c_str());
_wputenv_s(L"ANYPS5_SHADER_CACHE_DIR", directory.c_str());
_putenv_s("APS5_PIPELINE_JOURNAL_THREADS", "2");
#else
setenv("ANYPS5_NO_SHADER_CACHE", "0", 1);
@@ -9743,6 +9825,14 @@ void pipelineJournalTests() {
setenv("APS5_PIPELINE_JOURNAL_THREADS", "2", 1);
#endif
Require(PipelineJournalEnabled(), "the journal is disabled with a shader cache directory set");
auto journalFile = PipelineJournalPath();
#ifdef _WIN32
std::error_code pathError;
journalFile = ShaderRecompiler::NativeCachePath_nid_no_patch(journalFile, pathError);
Require(!pathError, "cannot normalize the journal fixture path");
#endif
cleanup.journal = journalFile;
if (longPath) Require(journalFile.native().size() > 260, "the journal fixture did not cross MAX_PATH");
const auto module = [](std::uint32_t salt) {
ShaderRecompiler::RecompileResult result;
result.spirv = std::vector<std::uint32_t>{0x07230203u, 0x00010000u, 0u, 8u, 0u, 0x00020011u, 1u, 0x00020011u, 2u + salt};
@@ -9934,13 +10024,15 @@ void pipelineJournalTests() {
FlushPipelineJournal();
const auto repeated = PipelineJournalTotals();
Require(repeated.recordedGraphics == recorded.recordedGraphics && repeated.recordedCompute == recorded.recordedCompute && repeated.recordedModules == recorded.recordedModules, "a repeated pipeline was recorded again");
Require(std::filesystem::file_size(PipelineJournalPath()) > 0, "the journal file is empty");
Require(std::filesystem::file_size(journalFile) > 0, "the journal file is empty");
const auto storeBefore = PipelineStoreTotals();
std::vector<std::byte> receivedWords;
ShaderRecompiler::RecompileResult received;
bool receivedPush = false;
std::uint32_t computeCalls = 0;
std::mutex computeMutex;
PipelineJournalHooks hooks{[&](const ShaderRecompiler::RecompileResult& shader, bool pushConstants) {
std::lock_guard lock(computeMutex);
++computeCalls;
received = shader;
receivedPush = pushConstants;
@@ -9963,7 +10055,7 @@ void pipelineJournalTests() {
const auto storeHit = PipelineStoreTotals();
Require(pipeline != nullptr && !built && mock.pipelineCreateCount == created && storeHit.hits == storeReplayed.hits + 1 && storeHit.misses == storeReplayed.misses, "the draw's configuration did not find the replayed pipeline");
{
const auto journalPath = PipelineJournalPath();
const auto& journalPath = journalFile;
const auto whole = std::filesystem::file_size(journalPath);
Require(whole % 8 == 0 && whole > 3, "the journal file is not record aligned");
std::filesystem::resize_file(journalPath, whole - 3);
@@ -9977,9 +10069,33 @@ void pipelineJournalTests() {
torn->Wait();
const auto reloaded = PipelineJournalTotals();
Require(reloaded.droppedRecords == 0 && reloaded.loadedModules == 4 && reloaded.loadedRecipes == 2 && reloaded.replayFailed == 0, "the journal did not recover from a tail torn inside a record's padding");
const auto writtenBefore = PipelineJournalTotals().writtenBytes;
RecordComputePipeline(compute, true);
FlushPipelineJournal();
Require(PipelineJournalTotals().recordedCompute == reloaded.recordedCompute + 1 && PipelineJournalTotals().recordedModules == reloaded.recordedModules, "the pipeline whose record was torn was not recorded again, or its module was");
const auto recordBytes = PipelineJournalTotals().writtenBytes - writtenBefore;
std::vector<std::byte> bytes;
Require(ShaderRecompiler::ReadWholeFile(journalFile, bytes) && recordBytes > 0 && recordBytes <= bytes.size(), "cannot read the re-recorded journal entry");
{
std::ofstream file(journalFile, std::ios::binary | std::ios::app);
file.write(reinterpret_cast<const char*>(bytes.data() + bytes.size() - recordBytes), static_cast<std::streamsize>(recordBytes));
const std::array<char, 8> tornTail{};
file.write(tornTail.data(), tornTail.size());
file.close();
Require(file.good(), "cannot append an external complete record and torn tail");
}
auto another = module(5);
another.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 1, std::vector<std::uint32_t>(4)));
const auto beforeAppend = PipelineJournalTotals();
RecordComputePipeline(another, false);
FlushPipelineJournal();
const auto appended = PipelineJournalTotals();
Require(appended.writeFailures == 0 && appended.recordedCompute == beforeAppend.recordedCompute + 1, "the append after an external torn tail failed");
Require(std::filesystem::file_size(journalFile) == bytes.size() + recordBytes + appended.writtenBytes - beforeAppend.writtenBytes, "tail recovery discarded an external complete record or retained torn bytes");
auto recovered = StartPipelineJournalReplay(context, fingerprint, hooks);
Require(recovered != nullptr, "the replay after the external append did not start");
recovered->Wait();
Require(PipelineJournalTotals().loadedModules == 5 && PipelineJournalTotals().loadedRecipes == 4 && PipelineJournalTotals().droppedRecords == 0, "the external append and torn-tail recovery did not replay cleanly");
}
{
const auto before = PipelineJournalTotals();
@@ -9992,7 +10108,7 @@ void pipelineJournalTests() {
}
{
const std::uint32_t meshWithoutOutputSize = 2u;
std::fstream file(PipelineJournalPath(), std::ios::in | std::ios::out | std::ios::binary);
std::fstream file(journalFile, std::ios::in | std::ios::out | std::ios::binary);
file.seekp(4);
file.write(reinterpret_cast<const char*>(&meshWithoutOutputSize), sizeof(meshWithoutOutputSize));
file.close();
@@ -10035,8 +10151,8 @@ int main(int argc, char** argv) {
commandBatchFenceFailureTests();
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--pipeline-journal") {
pipelineJournalTests();
if (argc == 2 && (std::string_view(argv[1]) == "--pipeline-journal" || std::string_view(argv[1]) == "--pipeline-journal-long-paths" || std::string_view(argv[1]) == "--pipeline-journal-extended-paths")) {
pipelineJournalTests(std::string_view(argv[1]) != "--pipeline-journal", std::string_view(argv[1]) == "--pipeline-journal-extended-paths");
std::cout << "pipeline journal recipe, record and replay tests passed\n";
return 0;
}
@@ -10210,6 +10326,11 @@ int main(int argc, char** argv) {
std::cout << "Multisampled DCC state checks passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--depth-bounds-state") {
DepthBoundsBiasTests();
std::cout << "Depth bounds state checks passed\n";
return 0;
}
{
const AgcDriver::Graphics::Context context{};
const AgcDriver::Graphics::State state{};
@@ -258,6 +258,16 @@ void RunUnmappedGapTests() {
Require(!GuestMemory::Accessible(reinterpret_cast<const void*>(std::uintptr_t{0x18}), 4), "a near-null address counts as accessible");
munmap(block, page);
munmap(block + 2 * page, page);
alignas(4096) static std::array<std::byte, 4096> imagePage{};
const auto imageAddress = reinterpret_cast<std::uintptr_t>(imagePage.data());
Require(GuestMemory::Accessible(imagePage.data(), page, true), "a main image data page is not writable");
Require(mprotect(imagePage.data(), page, PROT_READ) == 0, "cannot protect the main image test page behind the registry");
Require(GuestMemory::Accessible(imagePage.data(), page, true), "a checked main image page was queried again instead of read from the page table");
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(imageAddress, page);
Require(!GuestMemory::Accessible(imagePage.data(), page, true) && GuestMemory::Accessible(imagePage.data(), page), "an invalidated main image page kept its old access");
Require(mprotect(imagePage.data(), page, PROT_READ | PROT_WRITE) == 0, "cannot restore the main image test page");
GuestAllocations::GuestAllocationsInvalidate_nid_postfix(imageAddress, page);
Require(GuestMemory::Accessible(imagePage.data(), page, true), "a restored main image page is not writable");
if (!GuestArena::GuestArenaAvailable_nid_postfix()) return;
auto* guest = static_cast<std::byte*>(GuestArena::GuestArenaAllocate_nid_postfix(2 * page, page));
Require(mmap(guest, 2 * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) == guest, "cannot map the arena page test pages");
@@ -290,9 +290,14 @@ void RunGuestTextureResourceTests() {
Fields cubeBadArray = cube;
cubeBadArray.baseArray = 6;
rejectFields(cubeBadArray, "base array past its last array slice");
Fields cubeNotMultiple = cube;
cubeNotMultiple.depth = 4;
rejectFields(cubeNotMultiple, "multiple of 6");
Fields cubeExtraSlice = cube;
cubeExtraSlice.depth = 6;
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");
Fields multisampled = base;
multisampled.typeRaw = 14;
@@ -987,6 +987,42 @@ int main() {
return StencilReferenceValues(b.program) == std::vector<std::uint32_t>{0u, 255u} && !StencilReplaySafe(b.program);
});
passed &= run("an unprovable stencil reference replays every eight-bit value", [] {
Builder b;
b.program.Resources().stage = IrShaderStage::Pixel;
auto& zero = b.constant(0u);
auto& reference = b.emit(IrOpcode::UndefU32, IrType::U32, {});
ExportInfo info{};
info.kind = ExportTargetKind::MrtZ;
info.en = 2u;
info.vm = true;
b.program.Metadata().exportInfo.push_back(info);
ExportFlags flags{0u};
std::uint64_t bits = 0;
std::memcpy(&bits, &flags, sizeof(flags));
auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &reference, &zero, &zero});
b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits);
const auto values = StencilReferenceValues(b.program);
return values.size() == 256u && values.front() == 0u && values.back() == 255u;
});
passed &= run("a proven stencil reference replays its eight-bit value", [] {
Builder b;
b.program.Resources().stage = IrShaderStage::Pixel;
auto& zero = b.constant(0u);
ExportInfo info{};
info.kind = ExportTargetKind::MrtZ;
info.en = 2u;
info.vm = true;
b.program.Metadata().exportInfo.push_back(info);
ExportFlags flags{0u};
std::uint64_t bits = 0;
std::memcpy(&bits, &flags, sizeof(flags));
auto& value = b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&zero, &b.constant(300u), &zero, &zero});
b.emit(IrOpcode::SetAttribute, IrType::Void, {&value, &b.truth()}, bits);
return StencilReferenceValues(b.program) == std::vector<std::uint32_t>{44u};
});
passed &= run("stencil replay admits own-lane masks and their whole-quad expansion as export guards", [&] {
Builder b;
b.program.Resources().stage = IrShaderStage::Pixel;
@@ -0,0 +1,64 @@
#include "Recompiler.hpp"
#include <cstdio>
#include <exception>
#include <stdexcept>
#include <string>
namespace {
void Require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
ShaderRecompiler::MeshTargetLimits Limits(std::uint32_t vertexGranularity, std::uint32_t primitiveGranularity) {
ShaderRecompiler::MeshTargetLimits limits{};
limits.maxOutputMemoryBytes = 32768u;
limits.maxOutputComponents = 128u;
limits.outputPerVertexGranularity = vertexGranularity;
limits.outputPerPrimitiveGranularity = primitiveGranularity;
return limits;
}
ShaderRecompiler::MeshConfiguration Mesh(std::uint32_t vertices, std::uint32_t primitives) {
ShaderRecompiler::MeshConfiguration mesh{};
mesh.maxVertices = vertices;
mesh.maxPrimitives = primitives;
return mesh;
}
}
int main() {
try {
using ShaderRecompiler::MeshBarycentricOutputBudget;
using ShaderRecompiler::MeshOutputUse;
MeshOutputUse ccleste;
ccleste.flatParameters = 6u;
ccleste.perVertexParameters = 2u;
ccleste.clipVaryings = 1u;
const auto budget = MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(32u, 32u), ccleste);
Require(budget.vertexLocations == 8u, "per-vertex locations: position, six flat exports and the smooth clip varying make 8");
Require(budget.primitiveLocations == 10u, "per-primitive locations: three corners, two per-vertex parameters of three vertices and the cull flag make 10");
Require(budget.components == 72u, "components are 4 per location: 18 locations make 72");
Require(budget.bytes == 36864u, "bytes: (8 + 10) locations x 16 bytes x 128 = 36864, got " + std::to_string(budget.bytes));
Require(budget.bytes > Limits(32u, 32u).maxOutputMemoryBytes, "ccleste's configuration does not fit 32768 bytes");
Require(MeshBarycentricOutputBudget(Mesh(100u, 100u), Limits(32u, 32u), ccleste).bytes == 36864u, "100 vertices and primitives are padded to the granularity of 32, which gives 128");
Require(MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(1u, 1u), ccleste).bytes == 36864u, "a granularity of 1 changes nothing for 128");
MeshOutputUse distances = ccleste;
distances.clipDistances = 5u;
distances.cullDistances = 1u;
distances.layer = true;
const auto more = MeshBarycentricOutputBudget(Mesh(128u, 128u), Limits(32u, 32u), distances);
Require(more.vertexLocations == 8u + 2u + 1u, "five clip distances take 2 locations and one cull distance 1");
Require(more.primitiveLocations == 11u, "the layer output takes one per-primitive location");
std::puts("mesh output budget tests passed");
return 0;
} catch (const std::exception& error) {
std::fprintf(stderr, "%s\n", error.what());
return 1;
}
}
@@ -34,8 +34,9 @@ struct Use {
Images attachments;
bool anyReads = false;
bool anyWrites = false;
Ranges writeOnly;
PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites}; }
PassAccess View() const { return {reads, writes, images, attachments, anyReads, anyWrites, writeOnly}; }
};
void testRangeSet() {
@@ -184,12 +185,82 @@ void testAttachments() {
}
void testWriteOnly() {
PassHazards hazards;
hazards.BeginPass();
Use feedback;
feedback.writeOnly = {{0x1000, 0x8f80}};
hazards.Add(feedback.View());
Expect(hazards.Check(feedback.View(), true) == PassHazard::None, "draws only storing to the same buffer continue the pass");
Use reader;
reader.reads = {{0x2000, 0x2100}};
Expect(hazards.Check(reader.View(), true) == PassHazard::ReadAfterWrite, "a read of write-only stores of the pass");
Use writer;
writer.writes = {{0x8000, 0x9000}};
Expect(hazards.Check(writer.View(), true) == PassHazard::WriteAfterWrite, "a read-write use after write-only stores");
Use unknownReader;
unknownReader.anyReads = true;
Expect(hazards.Check(unknownReader.View(), true) == PassHazard::UnknownRead, "unknown reads after write-only stores");
Use elsewhere;
elsewhere.writeOnly = {{0x9000, 0xa000}};
elsewhere.reads = {{0xa000, 0xb000}};
Expect(hazards.Check(elsewhere.View(), true) == PassHazard::None, "write-only stores and reads elsewhere");
hazards.Clear();
hazards.BeginPass();
Use readWrite;
readWrite.reads = {{0x1000, 0x2000}};
readWrite.writes = {{0x1000, 0x2000}};
hazards.Add(readWrite.View());
Use storesOver;
storesOver.writeOnly = {{0x1800, 0x1900}};
Expect(hazards.Check(storesOver.View(), true) == PassHazard::WriteAfterWrite, "write-only stores after a read-write use");
hazards.Clear();
hazards.BeginPass();
Use readOnly;
readOnly.reads = {{0x1000, 0x2000}};
hazards.Add(readOnly.View());
Expect(hazards.Check(storesOver.View(), true) == PassHazard::WriteAfterRead, "write-only stores after a read");
hazards.Clear();
hazards.BeginPass();
Use unknownWriter;
unknownWriter.anyWrites = true;
unknownWriter.anyReads = true;
hazards.Add(feedback.View());
Expect(hazards.Check(unknownWriter.View(), true) == PassHazard::UnknownWrite, "unknown writes after write-only stores");
hazards.Clear();
hazards.Add(unknownWriter.View());
Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownWrite, "write-only stores after unknown writes");
hazards.Clear();
Use unknownRead;
unknownRead.anyReads = true;
hazards.Add(unknownRead.View());
Expect(hazards.Check(feedback.View(), true) == PassHazard::UnknownRead, "write-only stores after unknown reads");
hazards.Clear();
hazards.BeginPass();
hazards.Add(feedback.View());
hazards.SealWriteOnly();
Expect(hazards.Check(feedback.View(), true) == PassHazard::WriteAfterWrite, "write-only stores over stores sealed by a flush");
Use afterSeal;
afterSeal.writeOnly = {{0x9000, 0xa000}};
Expect(hazards.Check(afterSeal.View(), true) == PassHazard::None, "write-only stores elsewhere after a seal");
hazards.Add(afterSeal.View());
Expect(hazards.Check(afterSeal.View(), true) == PassHazard::None, "write-only stores after a seal continue the pass among themselves");
hazards.Clear();
Expect(hazards.Check(feedback.View(), true) == PassHazard::None, "a cleared seal");
hazards.Clear();
hazards.Add(feedback.View());
Expect(!hazards.Empty(), "write-only stores are tracked");
hazards.Clear();
Expect(hazards.Empty(), "cleared write-only stores");
}
int main() {
testRangeSet();
testRanges();
testUnknown();
testImages();
testAttachments();
testWriteOnly();
if (failures != 0) {
std::fprintf(stderr, "%d pass hazard checks failed\n", failures);
return 1;
@@ -22,6 +22,12 @@
namespace {
std::string Hex(std::uint32_t value) {
char text[16];
std::snprintf(text, sizeof(text), "0x%08x", value);
return text;
}
using namespace AgcDriver::Graphics;
using AgcDriver::OverlayContent;
using AgcDriver::OverlayCorner;
@@ -353,7 +359,7 @@ void requireTextDrawn(const std::vector<std::uint32_t>& drawn, VkExtent2D extent
const auto where = std::string("'") + line[index] + "' of '" + line + "' glyph pixel " + std::to_string(gx) + "," + std::to_string(gy);
Require(channel(word, 24) == channel(frameWord, 24), where + " changed the alpha");
if (set) {
Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: 0x" + std::to_string(word));
Require((word & 0x00ffffffu) == 0x00ffffffu, where + " is not white text: " + Hex(word));
continue;
}
for (const unsigned shift : {0u, 8u, 16u}) {
@@ -1,9 +1,12 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/TraceFile.hpp"
#include "zlib.h"
#include <atomic>
#include <condition_variable>
#include <fstream>
#include <iostream>
#include <iterator>
#include <mutex>
namespace {
@@ -346,6 +349,175 @@ void IntervalDroppedThreads() {
Check(line.find("\"queue worker 0x0\":{\"events_lost\":5,\"totals_full\":false}") != std::string::npos && line.find("events_full_after_s") == std::string::npos, "an interval without presents printed a loss time");
}
void SummaryMachineFields() {
const auto origin = Clock::now();
AgcDriver::TraceSummary summary;
summary.commit = "abc";
summary.dirty = false;
summary.gpu = "GPU";
summary.driver = "Driver 1";
summary.os = "OS 1";
summary.width = 1920;
summary.height = 1080;
summary.window = "summary_every 30";
summary.host = AgcDriver::TraceHost{"CPU \"X\"", 16, 32000};
summary.title = AgcDriver::TraceTitle{"PPSA00000", "Example", "01.002.000"};
const std::vector<Clock::time_point> presents{origin, origin + milliseconds(16)};
const auto line = TraceFormat::Summary(summary, {}, presents);
Check(line.find("\"os\":\"OS 1\",\"cpu\":\"CPU \\\"X\\\"\",\"cpu_threads\":16,\"memory_mib\":32000,\"title_id\":\"PPSA00000\",\"title\":\"Example\",\"content_version\":\"01.002.000\",\"resolution\":\"1920x1080\"") != std::string::npos, "the summary line did not name the machine and title");
summary.title.reset();
Check(TraceFormat::Summary(summary, {}, presents).find("title_id") == std::string::npos, "a summary without param.json named a title");
summary.host.reset();
const auto bare = TraceFormat::Summary(summary, {}, presents);
Check(bare.find("\"os\":\"OS 1\",\"resolution\":\"1920x1080\"") != std::string::npos && bare.find("cpu") == std::string::npos, "a summary on a host without a CPU name changed its fields");
}
void FrameMicroseconds() {
const auto origin = Clock::now();
const std::vector<Clock::time_point> presents{origin, origin + microseconds(16667), origin + microseconds(50000), origin + microseconds(50000)};
Check(TraceFormat::FrameMicroseconds(presents) == std::vector<std::uint32_t>{16667, 33333, 0}, "frame intervals in microseconds changed");
Check(TraceFormat::FrameMicroseconds({}).empty(), "no presents gave frame intervals");
}
void BenchFormat() {
AgcDriver::TraceSummary summary;
summary.commit = "abc";
summary.environment = {{"APS5_TRACE_BENCH", "1"}};
summary.gpu = "GPU";
summary.driver = "Driver 1";
summary.os = "OS 1";
summary.host = AgcDriver::TraceHost{"CPU", 8, 16000};
const AgcDriver::TraceBenchIntervals intervals{
std::make_shared<const AgcDriver::TraceBenchInterval>(AgcDriver::TraceBenchInterval{0, 30.5, 0, 1920, 1080, 120.25, {16667, 16666}}),
std::make_shared<const AgcDriver::TraceBenchInterval>(AgcDriver::TraceBenchInterval{30.5, 61, 1830, 2560, 1440, 98.5, {}}),
};
const std::string expected =
"{\"format\":\"aps5-bench/1\",\"commit\":\"abc\",\"dirty\":null,\"env\":{\"APS5_TRACE_BENCH\":\"1\"},\"gpu\":\"GPU\",\"driver\":\"Driver 1\",\"os\":\"OS 1\","
"\"cpu\":\"CPU\",\"cpu_threads\":8,\"memory_mib\":16000,\"intervals_dropped\":3,\"intervals\":[\n"
"{\"start_s\":0.000,\"end_s\":30.500,\"first_present\":0,\"resolution\":\"1920x1080\",\"gpu_busy_ms\":120.250,\"frame_us\":[16667,16666]},\n"
"{\"start_s\":30.500,\"end_s\":61.000,\"first_present\":1830,\"resolution\":\"2560x1440\",\"gpu_busy_ms\":98.500,\"frame_us\":[]}\n]}\n";
Check(TraceFormat::Bench(summary, intervals, 3) == expected, "benchmark file changed");
}
void BenchRotation() {
AgcDriver::TraceBenchIntervals kept;
std::uint64_t dropped = 0;
for (std::size_t index = 0; index < AgcDriver::TraceBench::MaximumIntervals; ++index) AgcDriver::TraceBench::Keep(kept, std::make_shared<const AgcDriver::TraceBenchInterval>(AgcDriver::TraceBenchInterval{static_cast<double>(index)}), dropped);
Check(kept.size() == AgcDriver::TraceBench::MaximumIntervals && dropped == 0, "intervals were dropped before the file was full");
for (std::size_t index = 0; index < 5; ++index) AgcDriver::TraceBench::Keep(kept, std::make_shared<const AgcDriver::TraceBenchInterval>(AgcDriver::TraceBenchInterval{static_cast<double>(AgcDriver::TraceBench::MaximumIntervals + index)}), dropped);
Check(kept.size() == AgcDriver::TraceBench::MaximumIntervals && dropped == 5, "a full file did not drop one interval per new one");
Check(kept.front()->startS == 5 && kept.back()->startS == static_cast<double>(AgcDriver::TraceBench::MaximumIntervals + 4), "the file did not keep the newest intervals");
const auto file = TraceFormat::Bench({}, kept, dropped);
Check(file.find("\"intervals_dropped\":5,") != std::string::npos && file.find("{\"start_s\":5.000,") != std::string::npos && file.find("{\"start_s\":4.000,") == std::string::npos, "the file does not match the kept intervals");
}
void BenchWriterKeepsNewest() {
std::mutex mutex;
std::condition_variable changed;
bool entered = false;
bool release = false;
std::vector<std::uint64_t> written;
AgcDriver::TraceBenchWriter writer([&](const AgcDriver::TraceBenchSnapshot& snapshot) {
std::unique_lock lock(mutex);
written.push_back(snapshot.dropped);
entered = true;
changed.notify_all();
changed.wait(lock, [&] { return release; });
});
writer.Write({{}, {}, 1});
{
std::unique_lock lock(mutex);
changed.wait(lock, [&] { return entered; });
}
writer.Write({{}, {}, 2});
writer.Write({{}, {}, 3});
writer.Write({{}, {}, 4});
{
std::lock_guard lock(mutex);
release = true;
}
changed.notify_all();
writer.Flush();
writer.Stop();
Check(written == std::vector<std::uint64_t>{1, 4}, "a benchmark snapshot was written after a newer one replaced it");
Check(writer.Superseded() == 2, "replaced benchmark snapshots were not counted");
}
void BenchWriterStopWrites() {
std::vector<std::uint64_t> written;
{
AgcDriver::TraceBenchWriter writer([&](const AgcDriver::TraceBenchSnapshot& snapshot) { written.push_back(snapshot.dropped); });
writer.Write({{}, {}, 7});
}
Check(written == std::vector<std::uint64_t>{7}, "stopping the benchmark writer lost its pending snapshot");
}
std::string ReadGzip(const std::filesystem::path& path) {
std::ifstream input(path, std::ios::binary);
const std::vector<unsigned char> bytes((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
Check(bytes.size() > 18, "the gzip file is truncated");
std::size_t size = 0;
for (unsigned index = 0; index < 4; ++index) size |= static_cast<std::size_t>(bytes[bytes.size() - 4 + index]) << (8 * index);
z_stream stream{};
Check(inflateInit2(&stream, -MAX_WBITS) == Z_OK, "inflateInit2 failed");
std::string inflated(size, 0);
stream.next_in = const_cast<Bytef*>(bytes.data() + 10);
stream.avail_in = static_cast<uInt>(bytes.size() - 18);
stream.next_out = reinterpret_cast<Bytef*>(inflated.data());
stream.avail_out = static_cast<uInt>(inflated.size());
const int status = inflate(&stream, Z_FINISH);
inflateEnd(&stream);
Check(status == Z_STREAM_END && stream.avail_out == 0, "the gzip file does not inflate to its size");
return inflated;
}
void ReplaceGzipFile() {
const auto directory = std::filesystem::temp_directory_path() / ("aps5-bench-test-" + std::to_string(Clock::now().time_since_epoch().count()));
std::filesystem::create_directories(directory);
const auto path = directory / "aps5-bench.json.gz";
auto temporary = path;
temporary += ".tmp";
AgcDriver::TraceFile::ReplaceGzip(path, "{\"first\":1}\n");
AgcDriver::TraceFile::ReplaceGzip(path, "{\"second\":2}\n");
Check(ReadGzip(path) == "{\"second\":2}\n" && !std::filesystem::exists(temporary), "the benchmark file was not replaced");
const auto blocked = directory / "aps5-bench-blocked.json.gz";
auto blockedTemporary = blocked;
blockedTemporary += ".tmp";
std::filesystem::create_directory(blocked);
bool failed = false;
try { AgcDriver::TraceFile::ReplaceGzip(blocked, "{\"third\":3}\n"); } catch (const std::system_error&) { failed = true; }
const bool blockedKept = std::filesystem::is_directory(blocked);
const bool temporaryLeft = std::filesystem::exists(blockedTemporary);
const auto kept = ReadGzip(path);
#ifndef _WIN32
// A directory at <name>.tmp makes the write itself fail, which is the case ReplaceGzip exists
// for: the previous file must survive, so an in-place rewrite fails this check. The failure is
// a std::ios_base::failure thrown inside libstdc++'s ofstream, which MinGW builds could not
// unwind to this catch, so the case runs on the other platforms only.
std::filesystem::create_directory(temporary);
bool writeFailed = false;
try { AgcDriver::TraceFile::ReplaceGzip(path, "{\"third\":3}\n"); } catch (const std::exception&) { writeFailed = true; }
const auto keptAfterWrite = ReadGzip(path);
#endif
std::filesystem::remove_all(directory);
Check(failed && blockedKept && !temporaryLeft, "a replace that could not move its file over the destination did not fail cleanly or left its temporary file");
Check(kept == "{\"second\":2}\n", "a failed replace of another file damaged the benchmark file");
#ifndef _WIN32
Check(writeFailed && keptAfterWrite == "{\"second\":2}\n", "a failed rewrite damaged the previous benchmark file");
#endif
}
void BenchSwitch() {
Check(!AgcDriver::TraceBench::Parse(nullptr, false), "the benchmark file was enabled without its switch");
Check(AgcDriver::TraceBench::Parse("1", true), "APS5_TRACE_BENCH=1 was not accepted");
for (const auto& [text, every] : {std::pair{"1", false}, std::pair{"0", true}, std::pair{"yes", true}, std::pair{"", true}}) {
bool rejected = false;
try { AgcDriver::TraceBench::Parse(text, every); }
catch (const std::invalid_argument&) { rejected = true; }
Check(rejected, "a malformed or unsupported APS5_TRACE_BENCH was accepted");
}
}
void GzipFile() {
std::string text;
for (int index = 0; index < 20000; ++index) text += "{\"name\":\"Driver.Packet\",\"ph\":\"X\",\"ts\":" + std::to_string(index) + "}\n";
@@ -413,7 +585,15 @@ int main() {
TotalsSince();
IntervalSummaryFields();
IntervalDroppedThreads();
SummaryMachineFields();
FrameMicroseconds();
BenchFormat();
BenchRotation();
BenchWriterKeepsNewest();
BenchWriterStopWrites();
BenchSwitch();
GzipFile();
ReplaceGzipFile();
AppendLines();
std::cout << "performance trace tests passed\n";
return 0;
@@ -15,6 +15,7 @@
#include <chrono>
#include <condition_variable>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
@@ -1012,6 +1013,54 @@ void testRegisterListsReadAtSubmission() {
check(std::atomic_ref<std::uint32_t>(done).load() == 1, "register list rewritten after submission was read by the worker");
}
void testInvalidRegisterListEntries() {
std::array<std::uint32_t, 6> pairs{0x038f038fu, 0xc8e9037eu, 0x10, 41, 0x11, 42};
const auto packet = makePacket(0x9f, {low(pairs.data()), high(pairs.data()), 0x80000000, 3});
check(!AgcDriver::Pm4::IndirectRegistersValid(pairs), "an offset dword with bits 16-27 set was taken as a register offset");
check(!AgcDriver::Pm4::IndirectRegistersValid(std::array<std::uint32_t, 2>{0xffffffffu, 0}), "the sentinel was taken as a register offset");
check(AgcDriver::Pm4::IndirectRegistersValid(std::array<std::uint32_t, 4>{0x10, 1, 0x70000011u, 2}), "a valid register list was rejected");
AgcDriver::QueueState state;
expectFailure([&] { AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state); }, "extended register");
check(!state.context.contains(0x10) && !state.context.contains(0x11), "a rejected register list partially changed state");
AgcDriver::Pm4::ExecuteIndirectRegisters(packet, pairs, state, true);
check(state.context.at(0x10) == 41 && state.context.at(0x11) == 42 && !state.context.contains(0x38f), "skipping an invalid entry lost the valid ones or wrote the invalid one");
}
void testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries() {
#ifdef _WIN32
check(_putenv_s("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1") == 0, "cannot enable skipping invalid register list entries");
#else
check(setenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS", "1", 1) == 0, "cannot enable skipping invalid register list entries");
#endif
alignas(8) static std::uint32_t gate = 0;
alignas(8) static std::uint32_t copied = 0;
static std::array<std::uint32_t, 4> registers{0x038f038fu, 5, 0x11, 9};
auto words = joinPackets({
makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}),
makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 2}),
makePacket(0x40, {0x100, 0xa010, 0, low(&copied), high(&copied)})});
submitWords(words);
registers = {0x10, 77, 0x03900390u, 6};
std::atomic_ref<std::uint32_t>(gate).store(1);
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint32_t>(copied).load() == 77, "a register list partly updated before its packet ran lost the current valid entries");
}
void testRegisterListReadAgainAtExecution() {
alignas(8) static std::uint32_t gate = 0;
static std::uint32_t done = 0;
static std::array<std::uint32_t, 2> registers{0x038f038fu, 74};
auto words = joinPackets({
makePacket(0x3c, {0x13, low(&gate), high(&gate), 1, 0xffffffffu, 0x19}),
makePacket(0x9f, {low(registers.data()), high(registers.data()), 0x80000000, 1}),
writeWord(done, 1)});
submitWords(words);
registers[0] = 0x10;
std::atomic_ref<std::uint32_t>(gate).store(1);
AgcDriverWaitIdle_nid_postfix();
check(std::atomic_ref<std::uint32_t>(done).load() == 1, "a register list invalid at submission and valid at execution was not read again");
}
void testIndexAttributesReadAtExecution() {
alignas(8) static std::uint32_t gate = 0;
static std::uint32_t done = 0;
@@ -1854,6 +1903,9 @@ int main(int argc, char** argv) {
testUnwrittenUserData();
testDriverSubmission();
testRegisterListsReadAtSubmission();
testInvalidRegisterListEntries();
testPartlyUpdatedRegisterListSkipsOnlyItsInvalidEntries();
testRegisterListReadAgainAtExecution();
testIndexAttributesReadAtExecution();
testPredicatedSubmission();
testConditionalSubmission();
@@ -90,13 +90,17 @@ void ConcurrentWriters() {
}
void WriterFailure() {
AgcDriver::ProfileOutput output([](std::string_view) { throw std::runtime_error("test writer failure"); });
AgcDriver::ProfileOutput output([](std::string_view) { throw std::runtime_error("test writer failure"); }, 8192);
output.Write("test");
for (int operation = 0; operation < 2; ++operation) {
for (int operation = 0; operation < 6; ++operation) {
bool failed = false;
try {
if (operation == 0) output.Flush();
else output.Write("next");
else if (operation == 1) output.Write("next");
else if (operation == 2) { output.Stop(); output.Write("after failed stop"); }
else if (operation == 3) output.Print("late failure");
else if (operation == 4) output.Print("%s", std::string(6000, 'x').c_str());
else output.Print("%s", std::string(10000, 'x').c_str());
} catch (const std::runtime_error& error) {
failed = std::string(error.what()) == "test writer failure";
}
@@ -106,16 +110,44 @@ void WriterFailure() {
void ExplicitStop() {
std::string written;
AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; });
AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; }, 8192);
output.Write("pending output");
output.Stop();
output.Stop();
output.Flush();
Check(written == "pending output", "explicit stop did not drain pending output");
bool failed = false;
try { output.Write("after stop"); }
catch (const std::runtime_error& error) { failed = std::string(error.what()) == "profile output is stopping"; }
Check(failed, "write after stop was accepted");
Check(!output.Write("after stop"), "write after stop was accepted");
Check(!output.Write(""), "empty write after stop was accepted");
output.Print("late small diagnostic %d", 1);
output.Print("%s", std::string(6000, 'x').c_str());
output.Print("%s", std::string(10000, 'x').c_str());
output.Flush();
Check(written == "pending output", "late output reached the stopped writer");
}
void StopWithProducers() {
std::string written;
AgcDriver::ProfileOutput output([&](std::string_view text) { written += text; });
std::atomic<unsigned> accepted{0};
std::atomic<bool> start{false};
std::vector<std::thread> producers;
for (int thread = 0; thread < 8; ++thread) {
producers.emplace_back([&] {
start.wait(false);
for (int item = 0; item < 1000; ++item) {
if (output.Write("x")) ++accepted;
}
});
}
Check(output.Write("x"), "initial write was rejected");
++accepted;
start.store(true);
start.notify_all();
output.Stop();
for (auto& producer : producers) producer.join();
output.Flush();
Check(written == std::string(accepted.load(), 'x'), "stop did not drain exactly the accepted output");
Check(!output.Write("late"), "concurrent stop left the writer open");
}
}
@@ -127,6 +159,7 @@ int main() {
ConcurrentWriters();
WriterFailure();
ExplicitStop();
StopWithProducers();
std::cout << "profile output tests passed\n";
return 0;
} catch (const std::exception& error) {
@@ -39,11 +39,13 @@
#include "VertexRangeProducer_frag_spv.h"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp"
#include "BdaAbi.hpp"
#include "ShaderCacheSourceVersion.hpp"
#include <SDL_loadso.h>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <io.h>
#include <windows.h>
#else
#include <fcntl.h>
@@ -57,6 +59,7 @@
#include <cctype>
#include <cmath>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
@@ -1140,6 +1143,67 @@ void completionCountTests(const Device& device, Recorder& recorder) {
Require(memory[0] == 1 && memory[2] == (storeAlways ? 1u : 7u) && memory[16] == 1, "completion stores ran for the wrong labels (overlapped and not-imported ones store, the untouched GPU-stored one skips)");
}
class StderrCapture {
public:
StderrCapture() {
std::fflush(stderr);
file = std::tmpfile();
Require(file != nullptr, "cannot open a temporary file for stderr");
#ifdef _WIN32
saved = _dup(_fileno(stderr));
_dup2(_fileno(file), _fileno(stderr));
#else
saved = dup(fileno(stderr));
dup2(fileno(file), fileno(stderr));
#endif
}
std::string Finish() {
std::fflush(stderr);
#ifdef _WIN32
_dup2(saved, _fileno(stderr));
_close(saved);
#else
dup2(saved, fileno(stderr));
close(saved);
#endif
std::rewind(file);
std::string text;
char buffer[4096];
for (std::size_t read = 0; (read = std::fread(buffer, 1, sizeof(buffer), file)) != 0;) text.append(buffer, read);
std::fclose(file);
std::fwrite(text.data(), 1, text.size(), stderr);
return text;
}
private:
std::FILE* file = nullptr;
int saved = -1;
};
std::size_t DeferredFailureLines(Recorder& recorder, std::size_t count, const std::function<void()>& failure, std::string* log = nullptr) {
StderrCapture capture;
for (std::size_t index = 0; index < count; ++index) recorder.OnComplete(failure);
recorder.Sync();
const auto text = capture.Finish();
if (log != nullptr) *log = text;
std::size_t lines = 0;
for (auto at = text.find("deferred write-back failed"); at != std::string::npos; at = text.find("deferred write-back failed", at + 1)) ++lines;
return lines;
}
void deferredFailureReportTests(Recorder& recorder) {
std::uint32_t serial = 0;
const auto plain = [&serial] { throw std::runtime_error("image table: poisoned entry " + std::to_string(serial++)); };
const auto unaligned = [](std::uint32_t instruction) { return [instruction] { throw BdaUnalignedAccess("BDA access failed: unaligned", instruction); }; };
Require(DeferredFailureLines(recorder, 12, plain) == 12, "a deferred failure other than an unaligned BDA access was not printed every time");
Require(DeferredFailureLines(recorder, 12, unaligned(0x5c8)) == 8, "the first 8 deferred reports of an unaligned BDA access were not the only ones printed");
std::string log;
Require(DeferredFailureLines(recorder, 1012, unaligned(0x5c8), &log) == 1 && log.find("pc 0x5c8 1024 times so far") != std::string::npos, "the 1024th deferred report of an unaligned BDA access was not the only one printed");
Require(DeferredFailureLines(recorder, 12, unaligned(0x7a0)) == 8, "unaligned BDA reports at one instruction limited the printing of the reports at another");
Require(DeferredFailureLines(recorder, 12, plain) == 12, "unaligned BDA reports limited the printing of other deferred failures");
}
void directCompletionLabelTests(Recorder& recorder) {
using namespace AgcDriver::GuestMemory;
constexpr std::size_t size = 65536;
@@ -2590,6 +2654,7 @@ int trimmedImportTests(const Device& device) {
const auto end = address + test.keptEnd;
const auto* kept = HostImportFor(context, begin, static_cast<std::size_t>(end - begin));
Require(kept != nullptr && kept->base == begin && kept->base + kept->bytes == end, name + ": the larger side is not imported whole");
const auto keptMemory = kept->memory;
if (test.trimmed) Require(kept->memory == original, "the larger side of " + name + " was imported again instead of kept");
else Require(kept->memory != original, "the side of " + name + " whose 2 MiB run the unmap replaced is served from the import that pinned the run's old pages");
requireImportServesGuest(context, recorder, *kept, begin, end, ++salt, "the larger side of " + name);
@@ -2609,7 +2674,7 @@ int trimmedImportTests(const Device& device) {
const auto from = reinterpret_cast<std::uint64_t>(cut);
const auto* remapped = HostImportFor(context, address, bytes);
Require(remapped != nullptr && remapped->base == address && remapped->bytes == bytes, name + ": the range mapped again is not imported whole");
Require(remapped->memory != original, "after " + name + " the pages mapped again are served from the import that pinned the old ones");
Require(remapped->memory != keptMemory, "after " + name + " the pages mapped again are served from the import made before they were mapped again");
requireImportServesGuest(context, recorder, *remapped, from, from + test.length, ++salt, "the pages mapped again after " + name);
requireImportServesGuest(context, recorder, *remapped, begin, end, ++salt, "the larger side after " + name + " and a new mapping");
}
@@ -8296,6 +8361,70 @@ void singleCubeTests(const Device& device, Recorder& recorder) {
}
}
void cubeExtraSliceTests(const Device& device, Recorder& recorder) {
auto context = device.GetContext();
TextureDetiler detiler(context);
context.detiler = &detiler;
SampleProgram program(context, recorder, SAMPLE_ARRAY_SPV);
const VkComponentMapping identity{};
std::array<std::uint32_t, 8> words{0x1000u, (56u << 20u) | (3u << 30u), 15u | (63u << 14u), 0xb0000facu, 6u, 0, 0, 0};
auto resource = DecodeTextureResource(words);
const auto geometry = DescribeSurface(resource);
Require(geometry.imageLayers == 7u, "a cube with a slice past its last whole cube does not allocate it");
std::vector<std::byte> memory(static_cast<std::size_t>(geometry.guestBytes) + 256);
auto* surface = reinterpret_cast<std::byte*>((reinterpret_cast<std::uintptr_t>(memory.data()) + 255) & ~std::uintptr_t{255});
for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) {
std::memset(surface + geometry.GuestLayerOffset(layer), 16 * (layer + 1), static_cast<std::size_t>(geometry.layerBytes));
}
resource.baseAddress = reinterpret_cast<std::uint64_t>(surface);
const std::span<const std::byte> snapshot(surface, static_cast<std::size_t>(geometry.guestBytes));
Texture texture(context, detiler, resource, identity, snapshot);
auto storage = StorageTexture::Create(context, detiler, resource, 0);
recorder.Keep(storage);
Texture storageView(context, storage, resource, identity);
const auto wordCube = [&words](std::vector<std::byte>& backing, std::uint32_t format, const std::function<std::uint32_t(std::uint32_t)>& word) {
auto cubeWords = words;
cubeWords[1] = (format << 20u) | (3u << 30u);
auto described = DecodeTextureResource(cubeWords);
const auto layout = DescribeSurface(described);
backing.assign(static_cast<std::size_t>(layout.guestBytes) + 256, std::byte{});
auto* base = reinterpret_cast<std::byte*>((reinterpret_cast<std::uintptr_t>(backing.data()) + 255) & ~std::uintptr_t{255});
for (std::uint32_t layer = 0; layer < layout.imageLayers; ++layer) {
const auto value = word(layer);
for (std::uint64_t offset = 0; offset < layout.layerBytes; offset += sizeof(value)) std::memcpy(base + layout.GuestLayerOffset(layer) + offset, &value, sizeof(value));
}
described.baseAddress = reinterpret_cast<std::uint64_t>(base);
return described;
};
std::vector<std::byte> depthMemory;
const auto depth = wordCube(depthMemory, 22u, [](std::uint32_t layer) {
const float value = static_cast<float>(layer + 1) / 8.0f;
std::uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return bits;
});
auto depthStorage = StorageTexture::Create(context, detiler, depth, 0);
recorder.Keep(depthStorage);
Require(Texture::CanDepthCopyFrom(*depthStorage, depth), "a cube with a slice past its last whole cube cannot take a depth copy");
Texture depthCopy(context, depthStorage, depth, identity, Texture::DepthCopyTag{});
depthCopy.RefreshDepthCopy();
constexpr std::array<std::uint32_t, 7> unsignedFloats{960u, 1024u, 1056u, 1088u, 1104u, 1120u, 1136u};
std::vector<std::byte> repackMemory;
const auto repacked = wordCube(repackMemory, 43u, [&unsignedFloats](std::uint32_t layer) { return unsignedFloats[layer] << 21u; });
auto repackStorage = StorageTexture::Create(context, detiler, repacked, 0);
recorder.Keep(repackStorage);
Require(Texture::CanRepackCopyFrom(*repackStorage, repacked), "a cube with a slice past its last whole cube cannot take a repacked copy");
Texture repackCopy(context, repackStorage, repacked, identity, Texture::RepackCopyTag{});
repackCopy.RefreshDepthCopy();
for (std::uint32_t layer = 0; layer < geometry.imageLayers; ++layer) {
const auto expected = 16.0f * (layer + 1) / 255.0f;
expectRed(program.Red(texture.View(), texture.Layout(), 0, static_cast<float>(layer)), expected, "a cube snapshot with a slice past its last whole cube sampled the wrong slice");
expectRed(program.Red(storageView.View(), storageView.Layout(), 0, static_cast<float>(layer)), expected, "a cube storage view with a slice past its last whole cube sampled the wrong slice");
expectRed(program.Red(depthCopy.View(), depthCopy.Layout(), 0, static_cast<float>(layer)), static_cast<float>(layer + 1) / 8.0f, "a cube depth copy with a slice past its last whole cube sampled the wrong slice");
expectRed(program.Red(repackCopy.View(), repackCopy.Layout(), 0, static_cast<float>(layer)), static_cast<float>(layer + 1), "a cube repacked copy with a slice past its last whole cube sampled the wrong slice");
}
}
struct SampledViewProbe {
Context& context;
PFN_vkGetDeviceProcAddr resolver;
@@ -8871,9 +9000,14 @@ int pipelineCacheFileTests(const Device& device) {
return after < before + footprints[index] / 2;
};
const auto cacheFile = [&] {
for (const auto& entry : std::filesystem::directory_iterator(directory)) Require(entry.is_directory(), "a pipeline cache file lives in the shader cache root");
char version[17];
std::snprintf(version, sizeof(version), "%016llx", static_cast<unsigned long long>(ShaderRecompiler::ShaderDiskCache::Generated::SourceVersion));
const auto versionDirectory = directory / version;
Require(std::filesystem::is_directory(versionDirectory), "the current shader cache version has no directory");
std::vector<std::filesystem::path> found;
for (const auto& entry : std::filesystem::directory_iterator(directory)) found.push_back(entry.path());
Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the pipeline cache directory does not hold exactly one cache file");
for (const auto& entry : std::filesystem::directory_iterator(versionDirectory)) found.push_back(entry.path());
Require(found.size() == 1 && found.front().filename().string().starts_with("vk-pipelines-"), "the current shader cache version's directory does not hold exactly one pipeline cache file");
return found.front();
};
const auto contents = [](const std::filesystem::path& path) {
@@ -9641,6 +9775,7 @@ int main(int argc, char** argv) {
}
if (argc == 2 && std::string_view(argv[1]) == "--cube-only") {
singleCubeTests(device, recorder);
cubeExtraSliceTests(device, recorder);
std::cout << "Single cube snapshot and storage sampling tests passed\n";
return 0;
}
@@ -9689,6 +9824,11 @@ int main(int argc, char** argv) {
std::cout << "Read list tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--deferred-failure-report-only") {
deferredFailureReportTests(recorder);
std::cout << "Deferred failure report tests passed\n";
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--host-import-unmap-only") {
gpu.unlock();
hostImportUnmapTests(device, recorder);
@@ -9911,6 +10051,7 @@ int main(int argc, char** argv) {
firstLayerViewTests(device, recorder);
sampledStorageBaseLevelUsageTests(device, recorder);
singleCubeTests(device, recorder);
cubeExtraSliceTests(device, recorder);
atomicViewTests(device, recorder);
metadataPassTests(device, recorder);
cmaskPassTests(device, recorder);
@@ -256,6 +256,34 @@ int main() {
return b.eliminate() == 0u && AllUsed({&plain, &flagged, &otherType, &floatImmediate});
});
passed &= run("four-operand expressions retain distinct trailing immediates across a large block", [] {
Builder b;
auto& source = b.unknown();
std::vector<IrValue*> reads;
std::vector<IrValue*> duplicates;
std::vector<IrValue*> different;
for (std::uint32_t index = 0; index < 256u; ++index) {
const auto composite = [&](std::uint32_t last) -> IrValue& {
return b.emit(IrOpcode::CompositeConstructU32x4, IrType::U32x4, {&source, &b.constant(index), &b.constant(7u), &b.constant(last)});
};
auto& first = composite(0u);
auto& second = composite(0u);
auto& changed = composite(1u);
auto& firstRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&first, &b.constant(3u)});
auto& secondRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&second, &b.constant(3u)});
auto& changedRead = b.emit(IrOpcode::CompositeExtractU32x4, IrType::U32, {&changed, &b.constant(3u)});
for (auto* read : {&firstRead, &secondRead, &changedRead}) b.keep(*read);
reads.push_back(&firstRead);
duplicates.push_back(&second);
duplicates.push_back(&secondRead);
different.push_back(&changedRead);
}
if (b.eliminate() != 512u || !AllUsed(different)) return false;
for (const auto* duplicate : duplicates) if (duplicate->HasUses()) return false;
for (const auto* read : reads) if (read->UseCount() != 2u) return false;
return true;
});
passed &= run("float immediates compare by bits: +0 and -0 and two NaN payloads stay apart, one NaN merges", [] {
Builder b;
auto& x = b.emit(IrOpcode::BitCastF32U32, IrType::F32, {&b.unknown()});
@@ -0,0 +1,88 @@
#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp"
#include "Decoder/Png.hpp"
#include <array>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>
namespace {
void require(bool condition, const char* message) {
if (!condition) throw std::runtime_error(message);
}
void setScreenshotDirectory(const std::filesystem::path& directory) {
#ifdef _WIN32
require(_putenv_s("ANYPS5_SCREENSHOTS", directory.string().c_str()) == 0, "set ANYPS5_SCREENSHOTS");
#else
require(::setenv("ANYPS5_SCREENSHOTS", directory.string().c_str(), 1) == 0, "set ANYPS5_SCREENSHOTS");
#endif
}
std::vector<std::uint8_t> readFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
require(file.good(), "open the written screenshot");
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
}
void requestsMerge() {
require(!AgcDriver::Screenshot::Take(), "no screenshot is requested at start");
AgcDriver::Screenshot::Request();
AgcDriver::Screenshot::Request();
require(AgcDriver::Screenshot::Take(), "a requested screenshot is taken");
require(!AgcDriver::Screenshot::Take(), "repeated requests are taken once");
}
void writesRgbPng(const std::filesystem::path& directory) {
const std::array<std::byte, 8> bgra{std::byte{0x10}, std::byte{0x20}, std::byte{0x30}, std::byte{0x00}, std::byte{0xA0}, std::byte{0xB0}, std::byte{0xC0}, std::byte{0x7F}};
const auto first = AgcDriver::Screenshot::Write("TEST00001", 2, 1, bgra);
const auto second = AgcDriver::Screenshot::Write("TEST00001", 2, 1, bgra);
require(first.parent_path() == directory, "the screenshot is written to ANYPS5_SCREENSHOTS");
require(first.extension() == ".png" && first.filename().string().starts_with("TEST00001-"), "the screenshot is named after the title ID");
require(first != second && std::filesystem::exists(second), "a second screenshot does not replace the first");
const auto image = Decoder::Png::Decode(readFile(first));
require(image.has_value() && image->width == 2 && image->height == 1, "the screenshot decodes at the frame extent");
const std::vector<std::uint8_t> expected{0x30, 0x20, 0x10, 0xFF, 0xC0, 0xB0, 0xA0, 0xFF};
require(image->pixels == expected, "the screenshot holds the frame's colors as opaque RGB");
}
void rejectsInvalidFrames() {
const std::array<std::byte, 4> pixel{};
const auto rejects = [](auto write) {
try {
write();
} catch (const std::invalid_argument&) {
return true;
}
return false;
};
require(rejects([&] { AgcDriver::Screenshot::Write("TEST00001", 2, 1, pixel); }), "a frame smaller than its extent is rejected");
require(rejects([&] { AgcDriver::Screenshot::Write("TEST00001", 0, 1, {}); }), "an empty frame is rejected");
require(rejects([&] { AgcDriver::Screenshot::Write("", 1, 1, pixel); }), "an empty title ID is rejected");
}
}
int main() try {
const auto root = std::filesystem::temp_directory_path() / ("anyps5_screenshot-" + std::to_string(std::random_device{}()));
const auto directory = root / "nested";
std::filesystem::remove_all(root);
setScreenshotDirectory(directory);
requestsMerge();
writesRgbPng(directory);
rejectsInvalidFrames();
std::filesystem::remove_all(root);
std::cout << "agc screenshot tests passed\n";
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
@@ -41,6 +41,9 @@
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <io.h>
#include <fcntl.h>
#include "prx/libc/include/General.hpp"
#endif
namespace {
@@ -59,6 +62,40 @@ void setEnvironment(const char* name, const std::string& value) {
#endif
}
#ifdef _WIN32
void verifyHostStandardError() {
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-host-stderr-" + std::to_string(std::random_device{}()));
setEnvironment("ANYPS5_NO_SHADER_CACHE", "0");
setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string());
int capture[2], guest[2];
require(::_pipe(capture, 65536, _O_BINARY) == 0, "cannot create host capture");
require(::_pipe(guest, 65536, _O_BINARY) == 0, "cannot create guest capture");
const int original = ::_dup(2);
require(original >= 0 && ::_dup2(capture[1], 2) == 0, "cannot capture initial stderr");
const int saved = ::_dup(2);
require(saved >= 0 && AdoptHostStandardStream_nid_no_patch(2, saved), "cannot preserve host stderr");
require(::_dup2(guest[1], 2) == 0, "cannot redirect guest stderr");
require(ShaderDiskCache::Enabled(), "shader disk cache was disabled");
ShaderDiskCache::Shutdown();
std::fflush(stderr);
std::fflush(HostStandardErrorFile_nid_no_patch());
require(::_dup2(original, 2) == 0, "cannot restore stderr");
::_close(original);
DWORD hostBytes = 0, guestBytes = 0;
require(::PeekNamedPipe(reinterpret_cast<HANDLE>(::_get_osfhandle(capture[0])), nullptr, 0, nullptr, &hostBytes, nullptr) != 0, "cannot inspect host capture");
require(::PeekNamedPipe(reinterpret_cast<HANDLE>(::_get_osfhandle(guest[0])), nullptr, 0, nullptr, &guestBytes, nullptr) != 0, "cannot inspect guest capture");
std::string host(hostBytes, '\0');
if (hostBytes != 0) require(::_read(capture[0], host.data(), hostBytes) == static_cast<int>(hostBytes), "cannot read host capture");
::_close(capture[0]);
::_close(capture[1]);
::_close(guest[0]);
::_close(guest[1]);
std::filesystem::remove_all(directory);
require(guestBytes == 0, "shader cache diagnostic entered the guest stderr pipe");
require(host.find("[shader-disk-cache]") != std::string::npos, "shader cache diagnostic did not reach host stderr");
}
#endif
void removeCacheDirectory(const std::filesystem::path& directory) {
std::error_code error;
#ifdef _WIN32
@@ -144,6 +181,7 @@ void requireSameResult(const RecompileResult& left, const RecompileResult& right
require(a.location == b.location && a.components == b.components && a.resource.fields == b.resource.fields && a.fetchIndex == b.fetchIndex && a.formatComponents == b.formatComponents, prefix + "vertex attribute differs");
}
require(left.barycentricEmulation.active == right.barycentricEmulation.active && left.barycentricEmulation.smooth == right.barycentricEmulation.smooth && left.barycentricEmulation.linear == right.barycentricEmulation.linear, prefix + "barycentric emulation differs");
require(left.meshBarycentrics == right.meshBarycentrics, prefix + "mesh barycentric layout differs");
}
void requireSameVariant(const CompiledVariant& left, const CompiledVariant& right, const char* what) {
@@ -224,6 +262,7 @@ RecompileResult sampleResult() {
result.poisonedSrtReads = 3;
result.shaderDataUserWords = {2u, 0xffffffffu, 9u};
result.barycentricEmulation = {true, false, true};
result.meshBarycentrics = MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 2, false, true}, FragmentParameter{3, 3, false, false}}};
result.variantId = 99;
result.imageTablePoison = {{{1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}, 8u, 2u, 6u}, {{9u, 0u, 0u, 0u, 0u, 0u, 0u, 0u}, 4u, 0xffffffffu, 19u}};
result.imageTableRanges = {{0x1000u, 0x200u}, {0x5000000000ull, 48u}};
@@ -611,6 +650,18 @@ void verifyArtifactStorageIsolation() {
require(ShaderDiskCache::EncodeEntry(key, variant) != before, "the stored artifact ignores vertex input metadata");
}
void verifyMeshBarycentricRequestSerialization() {
for (const auto& layout : {std::optional<MeshBarycentricLayout>{}, std::optional<MeshBarycentricLayout>{MeshBarycentricLayout{{true, true, true}, {FragmentParameter{0, 2, false, true}, FragmentParameter{4, 4, false, false}}}}}) {
SampleRequest sample;
sample.request.shader.stage = ShaderStage::Mesh;
sample.request.context.compute.reset();
sample.request.graphics = GraphicsCompileContext{0, {}, MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}, std::nullopt, {}, layout};
const auto text = RequestSerializer{}.Serialize(sample.request);
const auto restored = RequestSerializer{}.Deserialize(text);
require(restored.request.graphics.has_value() && restored.request.graphics->meshBarycentrics == layout, "a captured mesh request lost its barycentric layout");
}
}
void verifyKeySensitivity() {
SampleRequest base;
const auto key = base.Key();
@@ -679,6 +730,40 @@ void verifyKeySensitivity() {
require(changed.first != astro.first && changed.second != astro.second, std::string("the key ignores ") + what);
}
const auto meshKeys = [&](const std::optional<MeshBarycentricLayout>& layout) {
SampleRequest sample;
sample.request.shader.stage = ShaderStage::Mesh;
sample.request.context.compute.reset();
sample.request.graphics = GraphicsCompileContext{0, {}, MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}, std::nullopt, {}, layout};
return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)};
};
const std::vector<std::pair<const char*, std::optional<MeshBarycentricLayout>>> meshLayouts{
{"no mesh barycentric layout", std::nullopt},
{"smooth mesh barycentrics", MeshBarycentricLayout{{true, true, false}, {}}},
{"linear mesh barycentrics", MeshBarycentricLayout{{true, false, true}, {}}},
{"smooth and linear mesh barycentrics", MeshBarycentricLayout{{true, true, true}, {}}},
{"a per-vertex mesh parameter", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 0, false, true}}}},
{"another per-vertex mesh parameter source", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 1, false, true}}}},
{"a flat mesh parameter", MeshBarycentricLayout{{true, true, false}, {FragmentParameter{0, 0, false, false}}}},
};
for (std::size_t i = 0; i < meshLayouts.size(); ++i) {
require(meshKeys(meshLayouts[i].second) == meshKeys(meshLayouts[i].second), std::string("the key of ") + meshLayouts[i].first + " is not deterministic");
for (std::size_t j = i + 1; j < meshLayouts.size(); ++j) {
const auto left = meshKeys(meshLayouts[i].second);
const auto right = meshKeys(meshLayouts[j].second);
require(left.first != right.first && left.second != right.second, std::string("the mesh key confuses ") + meshLayouts[i].first + " with " + meshLayouts[j].first);
}
}
const auto pixelKeys = [&](bool meshPath) {
SampleRequest sample;
sample.request.shader.stage = ShaderStage::Fragment;
sample.request.context.compute.reset();
sample.request.graphics = GraphicsCompileContext{0, {}, meshPath ? std::optional(MeshConfiguration{4u, 4u, 12u, 12u, 4u, 64u, 1024u, 0u, 4u}) : std::nullopt, std::nullopt, {}, std::nullopt};
return std::pair{sample.Key(), RecompileCacheKey::ContextHash(sample.request)};
};
require(pixelKeys(true).first != pixelKeys(false).first && pixelKeys(true).second != pixelKeys(false).second, "a pixel shader on the mesh path shares a key with the vertex path");
SampleRequest moved;
moved.userData[0] ^= 0x10000u;
moved.request.shader.codeAddress += 0x1000000u;
@@ -745,6 +830,13 @@ int runLargeFileWrite() {
std::error_code error;
std::filesystem::remove_all(directory, error);
std::filesystem::create_directories(directory, error);
struct Cleanup {
std::filesystem::path directory;
~Cleanup() {
std::error_code error;
std::filesystem::remove_all(directory, error);
}
} cleanup{directory};
const auto space = std::filesystem::space(directory, error);
if (error || space.available < size + (std::uintmax_t{1} << 30u)) {
std::cout << "skipping: not enough free disk space for a file of more than 2 GiB\n";
@@ -771,6 +863,8 @@ int runLargeFileWrite() {
}).detach();
if (result.wait_for(std::chrono::seconds(200)) != std::future_status::ready) {
std::cerr << "WriteFileAtomically did not return within 200 s for a file of more than 2 GiB\n";
std::filesystem::remove_all(directory, error);
if (error) std::cerr << "cannot remove the large file test directory: " << error.message() << '\n';
std::cerr.flush();
std::_Exit(1);
}
@@ -925,6 +1019,62 @@ void verifyPersist() {
requireSameArtifact(variant.artifact, persisted->artifact, "a persisted entry whose file was replaced and removed");
}
#ifdef _WIN32
void verifyLongHostPaths() {
using namespace ShaderRecompiler::ShaderDiskCache;
auto directory = std::filesystem::temp_directory_path() / ("aps5-cache-long-" + std::to_string(std::random_device{}()));
require(directory.native().size() < 224, "temporary path is too long for the cache control");
directory /= std::wstring(224 - directory.native().size(), L'd');
const auto extended = [](const auto& path) { return std::filesystem::path(L"\\\\?\\" + path.native()); };
std::filesystem::create_directories(extended(directory));
setEnvironment("ANYPS5_NO_SHADER_CACHE", "0");
setEnvironment("ANYPS5_SHADER_CACHE_DIR", directory.string());
std::vector<std::filesystem::path> files;
std::filesystem::path entries;
struct Cleanup {
const std::filesystem::path& directory;
const std::vector<std::filesystem::path>& files;
const std::filesystem::path& entries;
~Cleanup() {
ShaderRecompiler::ShaderDiskCache::Shutdown();
std::error_code error;
for (const auto& file : files) std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + file.native()), error);
if (!entries.empty()) std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + entries.native()), error);
std::filesystem::remove(std::filesystem::path(L"\\\\?\\" + directory.native()), error);
}
} cleanup{directory, files, entries};
SampleRequest sample;
const auto key = sample.Key();
const auto variant = sampleVariant();
entries = ShaderDiskCache::EntryDirectory();
files.push_back(entries / EntryName(key));
files.push_back(entries / "last-used");
require(files.front().native().size() >= MAX_PATH, "the cache persistence control did not cross MAX_PATH");
const auto persisted = ShaderDiskCache::Persist(key, variant);
std::vector<std::byte> published;
require(ReadWholeFile(files.front(), published) && published == EncodeEntry(key, variant),
"the long-path cache entry was not correctly published before mapping");
require(persisted != nullptr, "a long-path cache entry was not persisted and mapped");
MEMORY_BASIC_INFORMATION region{};
require(VirtualQuery(persisted->artifact.spirv.data(), &region, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED,
"long-path persisted SPIR-V is not mapped");
requireSameVariant(variant, *persisted, "long-path persist round trip");
CompiledVariant loaded;
require(ShaderDiskCache::Load(key, loaded), "a long-path persisted cache entry did not load");
requireSameVariant(variant, loaded, "long-path load round trip");
const auto packPath = directory / (std::wstring(75, L'p') + L".pack");
files.push_back(packPath);
require(WriteFileAtomically(packPath, packBytes({{key, EncodeEntry(key, variant)}})), "cannot publish the long-path cache pack");
CachePack pack(packPath);
require(pack.Valid(), "a long-path cache pack was rejected: " + pack.Error());
PackLoadTimings timings;
CompiledVariant decoded;
require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a long-path cache pack entry did not load");
requireSameVariant(variant, decoded, "long-path cache pack round trip");
std::cout << "long-path shader cache persistence and pack tests passed\n";
}
#endif
struct ComputeRequest {
std::vector<std::uint32_t> code{0xe0700000u, 0x80000000u, 0xbf810000u};
std::array<std::uint32_t, 4> userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu};
@@ -2273,6 +2423,16 @@ int main(int argc, char** argv) {
std::cout << "artifact codec tests passed\n";
return 0;
}
#ifdef _WIN32
if (argc == 2 && std::string_view(argv[1]) == "--host-stderr") {
verifyHostStandardError();
return 0;
}
if (argc == 2 && std::string_view(argv[1]) == "--long-host-paths") {
verifyLongHostPaths();
return 0;
}
#endif
if (argc == 2 && std::string_view(argv[1]) == "--large-file") return runLargeFileWrite();
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
if (argc == 2 && std::string_view(argv[1]) == "--load-prebuilt") return runPrebuiltLoadingProcess();
@@ -2289,6 +2449,7 @@ int main(int argc, char** argv) {
verifyPrebuiltPack(directory / "packs");
verifyArtifactStorageIsolation();
verifyKeySensitivity();
verifyMeshBarycentricRequestSerialization();
verifyFloatModeSerialization();
verifyPlanRoundTrip();
verifyPlanKey();
@@ -850,7 +850,8 @@ void verifyOutOfRangeConstantReads() {
require(plan->info.images.size() == 2u && plan->info.samplers.size() == 1u, "out-of-range s_buffer_load: the two T#s were not planned as one image each");
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(request);
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
static_cast<void>(Recompile(request, *capture));
const auto holds = [&](const std::array<std::uint32_t, 8>& words) {
return std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) {
@@ -1025,8 +1026,10 @@ void verifyGuardedDescriptorSources() {
fill(valid, mode);
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(storeRequest);
storeRequest.context.memory = memory.Regions();
return Recompile(storeRequest, *capture);
const auto memoryRegions = memory.Regions();
auto compileRequest = storeRequest;
compileRequest.context.memory = memoryRegions;
return Recompile(compileRequest, *capture);
};
const auto storePlan = GetResourcePlan(storeRequest);
require(storePlan->info.images.size() == 2u && storePlan->descriptorSources.size() == 1u, "guarded images: the two stores were not planned as two images of one T#");
@@ -3614,7 +3617,8 @@ void verifyDescriptorPhis() {
require(plan->info.images.size() == images && plan->info.samplers.size() == samplers && plan->info.sampledPairs.size() == 2u, "descriptor Phi: the edges were not given one resource each");
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(request);
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
const auto compiled = Recompile(request, *capture);
require(countOps(compiled->spirv, OpImageSampleExplicitLod) == 2u, "descriptor Phi: the specialized SPIR-V does not sample once per edge");
#if ANYPS5_ENABLE_SPIRV_TOOLS
@@ -3645,7 +3649,8 @@ void verifyDescriptorPhis() {
auto twoLane = makeRequest(samplerCode, 64u);
AgcDriver::ShaderMemory twoLaneMemory({});
const auto twoLaneCapture = twoLaneMemory.Capture(twoLane);
twoLane.context.memory = twoLaneMemory.Regions();
const auto twoLaneRegions = twoLaneMemory.Regions();
twoLane.context.memory = twoLaneRegions;
require(countOps(Recompile(twoLane, *twoLaneCapture)->spirv, OpImageSampleExplicitLod) == 4u, "descriptor Phi: the specialized two-lane SPIR-V does not sample once per edge and half");
auto dynamic = makeRequest(dynamicCode);
@@ -3667,7 +3672,8 @@ void verifyDescriptorPhis() {
require(sequentialPlan->info.images.size() == 2u && sequentialPlan->info.samplers.size() == 2u, "descriptor Phi: a T# and an S# chosen in separate blocks were not split into their SRT descriptors");
AgcDriver::ShaderMemory sequentialMemory({});
const auto sequentialCapture = sequentialMemory.Capture(sequential);
sequential.context.memory = sequentialMemory.Regions();
const auto sequentialRegions = sequentialMemory.Regions();
sequential.context.memory = sequentialRegions;
static_cast<void>(Recompile(sequential, *sequentialCapture));
const auto planRestoredSampler = [&](std::span<const std::uint32_t> code, const char* message) {
@@ -3676,7 +3682,8 @@ void verifyDescriptorPhis() {
require(plan->info.samplers.size() == 1u && plan->info.sampledPairs.size() == 1u, message);
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(request);
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
static_cast<void>(Recompile(request, *capture));
};
std::vector<std::uint32_t> restoredSamplerCode{0xf40c0200u, 0xfa000000u, 0xf4080500u, 0xfa000020u, 0xf4080600u, 0xfa000040u, 0xbe930380u, 0xbf8cc07fu, 0xf09c0f08u, 0x00a20400u, 0xbe900380u, 0xbe910380u, 0xbe9c0414u, 0xbe9e0416u, 0xbf068011u, 0xbf850001u, 0x80128112u, 0xbe94041cu, 0xbe96041eu, 0x80118111u, 0xbf0a8211u, 0xbf85fff6u, 0xf09c0f08u, 0x00a20000u, 0x80108110u, 0xbf0a8210u, 0xbf85fff0u, 0x80138113u, 0xbf0a8213u, 0xbf85ffecu, 0xbf8c3f70u, 0xe0700000u, 0x80060000u, 0xe0700000u, 0x80060400u, 0x7d880090u, 0xbea23c6au, 0x4a020081u, 0xe0700000u, 0x80060100u, 0xbf810000u};
@@ -3850,7 +3857,8 @@ void verifyLaneSelectedDescriptors() {
require(plan->info.images.size() == 2u && plan->info.samplers.size() == 1u, "lane-selected T#: the T#s selected per lane were not planned as one image each");
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(request);
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
static_cast<void>(Recompile(request, *capture));
const auto holds = [&](const std::array<std::uint32_t, 8>& words) {
return std::ranges::any_of(capture->snapshot.images, [&](const DescriptorValue& value) {
@@ -3896,7 +3904,8 @@ void verifyProgramCounterRelativeData() {
};
AgcDriver::ShaderMemory memory({});
static_cast<void>(memory.Capture(request));
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
const auto first = Recompile(request);
require(dataBase(first) == codeAddress + 56u, "program counter data: the V# does not name the data at the shader's address");
auto relocated = request;
@@ -4839,7 +4848,7 @@ void verifyPixelRequestSerialization() {
minimal.context.pixel->layerExported = true;
minimal.context.pixel->ditheredAlphaToCoverage = true;
const auto encoded = serializer.Serialize(minimal);
require(requestPrefix(encoded, 8u) == "NVNQQSUAAAA=", "new requests did not use serialization version 37");
require(requestPrefix(encoded, 8u) == "NVNQQScAAAA=", "new requests did not use serialization version 39");
const std::array<std::uint32_t, 2> fallbacks{0x40u, 0x1ac4u};
auto withFallbacks = minimal;
withFallbacks.context.linkedCallFallbacks = fallbacks;
@@ -4957,7 +4966,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=", "NVNQQSYAAAA="}) {
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQSgAAAA="}) {
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
}
static constexpr std::string_view version18Request{
@@ -5243,6 +5252,43 @@ void verifyDepthRemapRequests() {
require(key != other, "the depth remap mode did not change the shader cache key");
}
void verifyMergedHullConfiguration() {
using namespace ShaderRecompiler;
static constexpr std::array<std::uint32_t, 1> code{0xbf810000u};
const RequestSerializer serializer;
RecompileRequest request{};
request.shader = {ShaderStage::TessellationControl, 0x10000u, code, 0, {}};
request.context.waveSize = 64;
request.context.vertex = ShaderVertexStageInfo{};
request.graphics = GraphicsCompileContext{0u, {}, std::nullopt, TessellationConfiguration{3u, 3u, 1u, 2u, 2u}, {}};
const auto separate = serializer.Serialize(request);
std::vector<std::uint64_t> separateKey;
RecompileCacheKey::Build(request, separateKey);
request.graphics->tessellation->mergedHull = true;
const auto merged = serializer.Serialize(request);
std::vector<std::uint64_t> mergedKey;
RecompileCacheKey::Build(request, mergedKey);
const auto replay = serializer.Deserialize(merged);
require(replay.request.graphics.has_value() && replay.request.graphics->tessellation.has_value() && replay.request.graphics->tessellation->mergedHull, "a merged hull configuration was lost in serialization");
require(!serializer.Deserialize(separate).request.graphics->tessellation->mergedHull, "a separate hull configuration was read as merged");
auto legacy = decodeBase64(merged);
require(legacy.size() > 52u && legacy[legacy.size() - 52u] == 1u, "the merged hull flag is not the last byte of the tessellation configuration");
legacy.erase(legacy.end() - 52);
for (const auto version : {30u, 31u, 32u, 34u, 35u, 36u, 37u, 38u}) {
auto older = legacy;
older[4] = static_cast<std::uint8_t>(version);
if (version < 38u) older.erase(older.end() - 31);
if (version < 36u) older.pop_back();
if (version < 35u) older.resize(older.size() - 5u);
if (version < 32u) older.pop_back();
const auto upgraded = serializer.Deserialize(encodeBase64(older));
const auto what = "a main version " + std::to_string(version) + " tessellation request";
require(upgraded.request.graphics.has_value() && upgraded.request.graphics->tessellation.has_value() && !upgraded.request.graphics->tessellation->mergedHull, (what + " read a merged hull flag").c_str());
require(serializer.Serialize(upgraded.request) == separate, (what + " misread the fields after its tessellation configuration").c_str());
}
require(mergedKey != separateKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(serializer.Deserialize(separate).request), "the cache keys ignore whether the hull program is merged");
}
void verifyLegacyPixelRequests() {
using namespace ShaderRecompiler;
static constexpr std::array<std::string_view, 14> legacyPixelRequests{
@@ -5570,7 +5616,8 @@ void verifyComputedTexelOffsets() {
request.useCache = false;
AgcDriver::ShaderMemory memory({});
static_cast<void>(memory.Capture(request));
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
return Recompile(request).spirv;
};
const auto sampleOperands = [](std::span<const std::uint32_t> words) {
@@ -5620,7 +5667,8 @@ void verifyShaderClockScopes() {
request.useCache = false;
AgcDriver::ShaderMemory memory({});
static_cast<void>(memory.Capture(request));
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
const auto words = Recompile(request).spirv;
std::map<std::uint32_t, std::uint32_t> constants;
std::vector<std::uint32_t> scopeIds;
@@ -5934,7 +5982,8 @@ void verifyUnnormalizedSamplers() {
request.layout.pushConstantSizeBytes = 128;
AgcDriver::ShaderMemory memory({});
static_cast<void>(memory.Capture(request));
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
return Recompile(request);
};
const auto flags = [](const RecompileResult& result, DescriptorRole role) {
@@ -6357,7 +6406,8 @@ void verifyTwoLaneUniformValues() {
request.layout.pushConstantSizeBytes = 128;
AgcDriver::ShaderMemory memory({});
const auto capture = memory.Capture(request);
request.context.memory = memory.Regions();
const auto memoryRegions = memory.Regions();
request.context.memory = memoryRegions;
const auto words = Recompile(request, *capture)->spirv;
std::size_t count = 0;
for (std::size_t cursor = 5; cursor < words.size();) {
@@ -6938,7 +6988,8 @@ void verifyGuardedNullPointers() {
const auto valid = validMemory.Capture(branch.request);
require(valid->snapshot.srtPoison.empty() && valid->plan->guardedSrtSlots.size() == 1u && slotPc(*valid->plan, valid->plan->guardedSrtSlots[0]) == 0x2cu && GuardRecords(*valid), "guarded pointer: a mapped nested pointer was poisoned, or its read has no guard");
require(valid->snapshot.flattenedSrt.at(valid->plan->guardedSrtSlots[0]) == payload, "guarded pointer: a mapped nested read does not hold its word");
branch.request.context.memory = validMemory.Regions();
const auto validRegions = validMemory.Regions();
branch.request.context.memory = validRegions;
const auto validResult = Recompile(branch.request, *valid);
require(validResult->poisonedSrtReads == 0u && faultBinding(*validResult) && validResult->bdaAbiVersion == BdaAbi::Version, "guarded pointer: a guarded program has no fault buffer");
@@ -6980,20 +7031,23 @@ void verifyGuardedNullPointers() {
const bool throughPointer = own == 0x38u && poison.address == unmapped;
require(poison.pc == 0x2cu && (pointerWord || throughPointer), "guarded pointer: a derived poison does not name the inaccessible read");
}
chain.request.context.memory = chainMemory.Regions();
const auto chainRegions = chainMemory.Regions();
chain.request.context.memory = chainRegions;
const auto chainResult = Recompile(chain.request, *chained);
indirect = 0;
point(static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(&indirect)));
AgcDriver::ShaderMemory nullChainMemory({});
const auto nullChain = nullChainMemory.Capture(chain.request);
require(nullChain->snapshot.srtPoison.size() == 1u && nullChain->snapshot.srtPoison[0].pc == 0x38u && nullChain->snapshot.srtPoison[0].address == 0u && GuardRecords(*nullChain), "guarded pointer: a null pointer read from memory did not poison the read through it");
chain.request.context.memory = nullChainMemory.Regions();
const auto nullChainRegions = nullChainMemory.Regions();
chain.request.context.memory = nullChainRegions;
const auto nullChainResult = Recompile(chain.request, *nullChain);
indirect = payloadAddress;
AgcDriver::ShaderMemory mappedChainMemory({});
const auto mappedChain = mappedChainMemory.Capture(chain.request);
require(mappedChain->snapshot.srtPoison.empty() && mappedChain->plan->guardedSrtSlots.size() == 3u && GuardRecords(*mappedChain), "guarded pointer: a mapped pointer chain was poisoned, or its reads have no guards");
chain.request.context.memory = mappedChainMemory.Regions();
const auto mappedChainRegions = mappedChainMemory.Regions();
chain.request.context.memory = mappedChainRegions;
const auto mappedChainResult = Recompile(chain.request, *mappedChain);
require(chainResult->poisonedSrtReads == 3u && nullChainResult->poisonedSrtReads == 1u && mappedChainResult->poisonedSrtReads == 0u, "guarded pointer: a chain result does not count its poisoned reads");
require(samePipeline(*chainResult, *mappedChainResult) && samePipeline(*nullChainResult, *mappedChainResult), "guarded pointer: a chain's poison changed the variant, the specialization or the module");
@@ -7016,7 +7070,8 @@ void verifyGuardedNullPointers() {
AgcDriver::ShaderMemory zeroMemory({});
const auto zeroCapture = zeroMemory.Capture(descriptor.request);
require(zeroCapture->snapshot.srtPoison.empty() && zeroBuffer(*zeroCapture) && GuardRecords(*zeroCapture), "guarded pointer: a zero V# read from mapped memory was poisoned or not read");
descriptor.request.context.memory = zeroMemory.Regions();
const auto zeroRegions = zeroMemory.Regions();
descriptor.request.context.memory = zeroRegions;
const auto zeroResult = Recompile(descriptor.request, *zeroCapture);
for (const auto pointer : {std::uint64_t{0}, unmapped}) {
point(pointer);
@@ -7072,7 +7127,8 @@ void verifyGuardedNullPointers() {
const auto rootlessCapture = rootlessMemory.Capture(rootless.request);
require(rootlessCapture->snapshot.nullRootReads != 0u && GuardRecords(*rootlessCapture), "guarded pointer: reads through a null user-data pointer were not counted");
require(std::all_of(rootlessCapture->snapshot.buffers.begin(), rootlessCapture->snapshot.buffers.end(), [](const DescriptorValue& value) { return std::all_of(value.dwords.begin(), value.dwords.end(), [](std::uint32_t word) { return word == 0u; }); }), "guarded pointer: a V# read through a null user-data pointer is not zero");
rootless.request.context.memory = rootlessMemory.Regions();
const auto rootlessRegions = rootlessMemory.Regions();
rootless.request.context.memory = rootlessRegions;
const auto rootlessResult = Recompile(rootless.request, *rootlessCapture);
require(rootlessResult->poisonedSrtReads >= rootlessCapture->snapshot.nullRootReads, "guarded pointer: a capture through a null user-data pointer is cacheable");
point(0u);
@@ -7412,6 +7468,7 @@ int main(int argc, char** argv) {
verifyPixelRequestSerialization();
verifyVertexInfoVersion19();
verifyDepthRemapRequests();
verifyMergedHullConfiguration();
verifyPositionExportComponents();
verifyLegacyPixelRequests();
verifyPixelExportReplay();
@@ -7613,7 +7670,8 @@ int main(int argc, char** argv) {
const auto changedCapture = changedMemory.Capture(request);
auto changed = updated;
changed.useCache = true;
changed.context.memory = changedMemory.Regions();
const auto changedRegions = changedMemory.Regions();
changed.context.memory = changedRegions;
auto before = updated;
before.useCache = true;
before.context.memory = updatedRegions;
@@ -7676,7 +7734,8 @@ int main(int argc, char** argv) {
AgcDriver::ShaderMemory nullRoot({});
const auto nullRootCapture = nullRoot.Capture(request);
require(nullRootCapture->snapshot.nullRootReads != 0u, "reads through a null user-data pointer were not counted");
request.context.memory = nullRoot.Regions();
const auto nullRootRegions = nullRoot.Regions();
request.context.memory = nullRootRegions;
require(Recompile(request, *nullRootCapture)->poisonedSrtReads >= nullRootCapture->snapshot.nullRootReads, "a capture through a null user-data pointer is cacheable");
std::cout << "Shader memory capture, strict validation and deterministic replay passed\n";
return 0;
@@ -0,0 +1,75 @@
#include "IntermediateRepresentation/IrProgram.hpp"
#include "Optimization/ShaderStageInputInfo.hpp"
#include "SpirvBackend/SpirvEmitter.hpp"
#include "PipelineSpecialization.hpp"
#include <cstdint>
#include <cstdio>
#include <exception>
#include <stdexcept>
#include <string>
#include <string_view>
namespace {
using namespace ShaderRecompiler;
void require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
enum class Failure { MissingComputeInput, UnloweredInstruction };
std::string emissionFailure(Failure failure) {
IrProgram program;
program.SetWaveSize(32);
program.Resources().stage = IrShaderStage::Compute;
program.Resources().shaderHash = 0x1173ae1000ull;
program.Resources().srtPlanComplete = true;
program.Resources().resourceTrackingComplete = true;
program.Metadata().shaderInfoComplete = true;
program.Metadata().bindingLayoutComplete = true;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
program.Metadata().blockInfo.emplace_back();
if (failure == Failure::UnloweredInstruction) block.AppendInstruction(&program.CreateValue(IrOpcode::Unreachable, IrType::Void));
ShaderComputeInputInfo compute;
compute.threadsNum[0] = 32;
compute.threadsNum[1] = 1;
compute.threadsNum[2] = 1;
compute.hostSubgroupSize = 32;
compute.waveSize = 32;
ShaderStageInputInfo inputs;
if (failure != Failure::MissingComputeInput) inputs.compute = &compute;
CompiledBindingLayout bindings;
bindings.layout = program.Metadata().bindings;
SpirvTargetOptions target{};
target.vulkanVersion = 0x00401000;
target.spirvVersion = 0x00010300;
target.subgroupSize = 32;
try {
static_cast<void>(SpirvEmitter().Emit(program, inputs, bindings, target));
} catch (const std::runtime_error& error) {
return error.what();
}
throw std::runtime_error("the program emitted without a failure");
}
void requireFailure(Failure failure, std::string_view prefix, std::string_view reason) {
const auto text = emissionFailure(failure);
require(text.starts_with(prefix) && text.find(reason) != std::string::npos, "expected \"" + std::string(prefix) + "...\" with \"" + std::string(reason) + "\", got \"" + text + "\"");
}
}
int main() {
try {
requireFailure(Failure::MissingComputeInput, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " reason=compute input info is missing");
requireFailure(Failure::UnloweredInstruction, "SPIR-V emission failed: hash=0x1173ae1000 stage=", " opcode=Unreachable reason=");
} catch (const std::exception& error) {
std::fprintf(stderr, "SpirvFailureText: %s\n", error.what());
return 1;
}
std::puts("SpirvFailureText: emission failures name the shader hash in hex");
return 0;
}
@@ -0,0 +1,172 @@
#define SPV_ENABLE_UTILITY_CODE
#include <spirv/unified1/spirv.hpp>
#undef SPV_ENABLE_UTILITY_CODE
#include "Recompiler.hpp"
#include "SubvectorLoopFixture.hpp"
#include <array>
#include <cstdint>
#include <cstdio>
#include <exception>
#include <map>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace {
using namespace ShaderRecompiler;
void require(bool condition, const std::string& message) {
if (!condition) throw std::runtime_error(message);
}
std::vector<std::uint32_t> recompile(bool robustBufferAccess2) {
const std::array<std::uint32_t, 8> userData{0x10000000u, 0u, 0x100u, 0x31016facu, 0x10001000u, 0u, 0x900u, 0x31016facu};
const std::array<std::string_view, 1> extensions{"SPV_KHR_storage_buffer_storage_class"};
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityShader, spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot};
RecompileRequest request{};
request.shader = {ShaderStage::Compute, 0x20000u, SubvectorLoopFixture::ExecKeptCode, 0, {}};
request.context.waveSize = 64;
request.context.userDataBaseRegister = 0;
request.context.userData = userData;
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
request.target.vulkanVersion = 0x00403000u;
request.target.spirvVersion = 0x00010600u;
request.target.subgroupSize = 64;
request.target.bdaAbiVersion = 1;
request.target.supportedCapabilities = capabilities;
request.target.supportedExtensions = extensions;
request.target.maxWorkgroupSize = {1024u, 1024u, 64u};
request.target.maxWorkgroupInvocations = 1024;
request.target.maxWorkgroupSharedMemoryBytes = 49152;
request.target.robustBufferAccess2 = robustBufferAccess2;
request.layout = {0, 0, 0, 128};
request.useCache = false;
auto result = Recompile(request);
require(!result.spirv.empty(), "the shader did not recompile");
return result.spirv.Copy();
}
enum class EmptyExecTest {
Other,
OneCompare,
TwoCompares
};
class BranchConditions {
public:
explicit BranchConditions(std::vector<std::uint32_t> module) : words(std::move(module)) {
require(words.size() >= 5, "the SPIR-V header is missing");
for (std::size_t offset = 5; offset < words.size();) {
const auto count = words[offset] >> spv::WordCountShift;
require(count != 0 && count <= words.size() - offset, "a SPIR-V instruction is truncated");
const auto opcode = static_cast<spv::Op>(words[offset] & spv::OpCodeMask);
bool result = false;
bool type = false;
spv::HasResultAndType(opcode, &result, &type);
if (result) definitions.emplace(words[offset + (type ? 2 : 1)], offset);
if (opcode == spv::OpBranchConditional) conditions.push_back(words[offset + 1]);
offset += count;
}
}
std::vector<EmptyExecTest> Tests() const {
std::vector<EmptyExecTest> tests;
for (const auto condition : conditions) tests.push_back(classify(condition));
return tests;
}
private:
std::vector<std::uint32_t> words;
std::vector<std::uint32_t> conditions;
std::map<std::uint32_t, std::size_t> definitions;
std::span<const std::uint32_t> instruction(std::uint32_t id, spv::Op opcode, std::size_t count) const {
const auto found = definitions.find(id);
if (found == definitions.end()) return {};
const auto offset = found->second;
if ((words[offset] & spv::OpCodeMask) != static_cast<std::uint32_t>(opcode) || (words[offset] >> spv::WordCountShift) != count) return {};
return std::span<const std::uint32_t>(words).subspan(offset, count);
}
bool zero(std::uint32_t id) const {
const auto constant = instruction(id, spv::OpConstant, 4);
return !constant.empty() && constant[3] == 0u;
}
std::optional<std::uint32_t> comparedWithZero(std::uint32_t id) const {
const auto compare = instruction(id, spv::OpIEqual, 5);
if (compare.empty()) return std::nullopt;
if (zero(compare[4])) return compare[3];
if (zero(compare[3])) return compare[4];
return std::nullopt;
}
std::optional<std::pair<std::uint32_t, std::uint32_t>> execWord(std::uint32_t id) const {
const auto extract = instruction(id, spv::OpCompositeExtract, 5);
if (extract.empty() || instruction(extract[3], spv::OpGroupNonUniformBallot, 5).empty()) return std::nullopt;
return std::pair{extract[3], extract[4]};
}
bool execWords(std::uint32_t first, std::uint32_t second) const {
const auto low = execWord(first);
const auto high = execWord(second);
return low && high && low->first == high->first && low->second <= 1u && high->second <= 1u && low->second != high->second;
}
EmptyExecTest classify(std::uint32_t condition) const {
if (const auto negation = instruction(condition, spv::OpLogicalNot, 4); !negation.empty()) return classify(negation[3]);
if (const auto operand = comparedWithZero(condition)) {
const auto combined = instruction(*operand, spv::OpBitwiseOr, 5);
if (!combined.empty() && execWords(combined[3], combined[4])) return EmptyExecTest::OneCompare;
}
const auto both = instruction(condition, spv::OpLogicalAnd, 5);
if (!both.empty()) {
const auto first = comparedWithZero(both[3]);
const auto second = comparedWithZero(both[4]);
if (first && second && execWords(*first, *second)) return EmptyExecTest::TwoCompares;
}
return EmptyExecTest::Other;
}
};
int failures = 0;
void fail(const std::string& message) {
std::fprintf(stderr, "SpirvSubvectorLoopBegin: %s\n", message.c_str());
++failures;
}
void check(bool robustBufferAccess2) {
const std::string mode = robustBufferAccess2 ? "robustBufferAccess2 on" : "robustBufferAccess2 off";
std::uint32_t oneCompare = 0;
std::uint32_t twoCompares = 0;
for (const auto test : BranchConditions(recompile(robustBufferAccess2)).Tests()) {
if (test == EmptyExecTest::OneCompare) ++oneCompare;
if (test == EmptyExecTest::TwoCompares) ++twoCompares;
}
if (twoCompares != 0u) {
fail(mode + ": branches that compare exec_lo and exec_hi with 0 separately: " + std::to_string(twoCompares) + ", expected 0");
}
if (oneCompare != 1u) {
fail(mode + ": branches that compare (exec_lo | exec_hi) with 0: " + std::to_string(oneCompare) + ", expected 1 (s_subvector_loop_begin)");
}
}
}
int main() {
try {
check(false);
check(true);
} catch (const std::exception& error) {
fail(error.what());
}
if (failures != 0) return 1;
std::puts("SpirvSubvectorLoopBegin: s_subvector_loop_begin tests an empty exec mask as (exec_lo | exec_hi) == 0 in both robustBufferAccess2 modes");
return 0;
}
@@ -4,6 +4,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawCache.hpp"
#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/libc/include/GuestHeap.hpp"
#include "prx/libc/include/Shutdown.hpp"
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
@@ -573,6 +574,49 @@ void testCopyIntoHostMemory() {
GuestHeap::GuestHeapFree_nid_postfix(produced);
}
void testSmallCopyBehindPendingSource() {
alignas(64) static volatile std::uint32_t copied = 0, settledCopied = 0, hostCopied = 0;
constexpr std::size_t words = 32768;
constexpr auto bytes = static_cast<std::uint32_t>(words * 4);
constexpr std::size_t smallWords = 1024;
constexpr auto smallBytes = static_cast<std::uint32_t>(smallWords * 4);
auto* produced = static_cast<std::uint32_t*>(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes));
auto* source = static_cast<std::uint32_t*>(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, bytes));
auto* destination = static_cast<std::uint32_t*>(GuestHeap::GuestHeapAlignForGpu_nid_postfix(65536, smallBytes));
for (std::size_t i = 0; i < words; ++i) {
produced[i] = 0xc3000000u | static_cast<std::uint32_t>(i);
source[i] = 0x3c000000u | static_cast<std::uint32_t>(i);
}
std::fill(destination, destination + smallWords, 0u);
const auto before = AgcDriver::DriverDetail::copiesBehindPendingSources.load();
submit(0, commands(memoryCopy(source, produced, bytes), memoryCopy(destination, source, smallBytes), writeData(&copied, 1)));
waitFor(&copied, 1, "the label after a small copy behind a pending source never landed");
AgcDriverWaitIdle_nid_postfix();
check(std::equal(destination, destination + smallWords, produced), "a small copy did not see the GPU copy into its source before it");
if (std::getenv("APS5_COPY_SYNC") != nullptr) check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before, "a small copy that has to wait for a completion store over its source went through the GPU copy");
else check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == before + 1, "a small copy whose source the GPU still stores into was not recorded on the GPU");
std::fill(destination, destination + smallWords, 0u);
const auto settled = AgcDriver::DriverDetail::copiesBehindPendingSources.load();
submit(0, commands(memoryCopy(destination, produced, smallBytes), writeData(&settledCopied, 1)));
waitFor(&settledCopied, 1, "the label after a small copy of a settled source never landed");
AgcDriverWaitIdle_nid_postfix();
check(std::equal(destination, destination + smallWords, produced), "a small copy of a settled source lost bytes");
check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == settled, "a small copy of a settled source was recorded on the GPU");
std::vector<std::uint32_t> hostDestination(smallWords);
for (std::size_t i = 0; i < words; ++i) source[i] = 0x3c000000u | static_cast<std::uint32_t>(i);
const auto hostCopies = AgcDriver::DriverDetail::copiesBehindPendingSources.load();
const auto aliasChecks = AgcDriver::VulkanDevice::CopyAliasCounts().noSource;
submit(0, commands(memoryCopy(source, produced, bytes), memoryCopy(hostDestination.data(), source, smallBytes), writeData(&hostCopied, 1)));
waitFor(&hostCopied, 1, "the label after a small copy into memory that is not imported never landed");
AgcDriverWaitIdle_nid_postfix();
check(std::equal(hostDestination.begin(), hostDestination.end(), produced), "a small copy into memory that is not imported did not see the GPU copy into its source");
check(AgcDriver::DriverDetail::copiesBehindPendingSources.load() == hostCopies, "a small copy into memory that is not imported was recorded on the GPU");
check(AgcDriver::VulkanDevice::CopyAliasCounts().noSource == aliasChecks + 1, "a small copy into memory that is not imported went through the GPU copy before its CPU path");
GuestHeap::GuestHeapFree_nid_postfix(destination);
GuestHeap::GuestHeapFree_nid_postfix(source);
GuestHeap::GuestHeapFree_nid_postfix(produced);
}
void testShaderHeaderAlignment() {
alignas(256) static const std::array<std::uint32_t, 64> code{0xbf810000};
struct Header {
@@ -881,6 +925,13 @@ int main(int argc, char** argv) {
}
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
if (argc == 2 && std::string_view(argv[1]) == "--small-copy-sync") {
testHeaderWithoutProgramAddress();
testSmallCopyBehindPendingSource();
LibcRunShutdown_nid_postfix();
std::puts("Small copies with APS5_COPY_SYNC passed");
return 0;
}
alignas(256) std::array<std::uint32_t, 64> rawCode{};
rawCode.fill(0xbf800000);
rawCode[0] = 0xbe8003ff;
@@ -975,6 +1026,7 @@ int main(int argc, char** argv) {
testHeaderWithoutProgramAddress();
testCopyIntoHostMemory();
testLargeDataFill();
testSmallCopyBehindPendingSource();
testRegisteredFloatMode();
testDispatchFloatMode();
testReadOnlyResummarize();
@@ -0,0 +1,21 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_TESTS_SUBVECTORLOOPFIXTURE_HPP
#include <array>
#include <cstdint>
namespace SubvectorLoopFixture {
alignas(256) constexpr std::array<std::uint32_t, 49> ExecKeptCode{
0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280,
0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000a, 0x80148114, 0x4a141481,
0x7e160214, 0x7e180226, 0xbe96047e, 0x7e1a0216, 0x7e1c0217, 0xbf098414, 0xbf850001, 0xbe26fff6,
0xbe98047e, 0xbefe04c1, 0x7e1e0226, 0x7e200218, 0x7e220219, 0x7e240214, 0xe0701000, 0x80010a01,
0xe0701100, 0x80010b01, 0xe0701200, 0x80010c01, 0xe0701300, 0x80010d01, 0xe0701400, 0x80010e01,
0xe0701500, 0x80010f01, 0xe0701600, 0x80011001, 0xe0701700, 0x80011101, 0xe0701800, 0x80011201,
0xbf810000,
};
}
#endif
@@ -440,4 +440,47 @@ void RunTextureTilingTests() {
volume.baseArray = 0;
Require(ViewedArrayWindow(volume).baseAddress == cube.baseAddress, "a volume keeps its descriptor");
}
{
GuestTextureResource array{};
array.baseAddress = 0x10000000ull;
array.width = 16;
array.height = 16;
array.depthOrLastArray = 3200;
array.baseArray = 1536;
array.mipCount = 2;
array.tileMode = TextureTileMode::kStandard64KB;
array.dimension = TextureDimension::k2DArray;
array.format = 56;
const auto geometry = DescribeSurface(array);
const auto window = ArrayLayerWindow(array, 2048);
Require(window.baseAddress == array.baseAddress + 1536ull * geometry.layerBytes, "a storage view past maxImageArrayLayers must start at its first viewed slice");
Require(window.baseArray == 0 && window.depthOrLastArray == 1664, "a storage view past maxImageArrayLayers must hold exactly its viewed slices");
Require(DescribeSurface(window).layerBytes == geometry.layerBytes && DescribeSurface(window).mips[1].tiledOffset == geometry.mips[1].tiledOffset, "a viewed slice must keep the layout of the whole array's slice");
auto fitting = array;
fitting.depthOrLastArray = 2047;
const auto kept = ArrayLayerWindow(fitting, 2048);
Require(kept.baseAddress == fitting.baseAddress && kept.baseArray == fitting.baseArray && kept.depthOrLastArray == fitting.depthOrLastArray, "an array within maxImageArrayLayers must keep its descriptor");
auto linear = array;
linear.tileMode = TextureTileMode::kLinear;
Require(ArrayLayerWindow(linear, 2048).baseAddress == linear.baseAddress + 1536ull * DescribeSurface(linear).layerBytes, "a linear storage view past maxImageArrayLayers must start at its first viewed slice");
auto wide = array;
wide.baseArray = 1000;
reject([&] { static_cast<void>(ArrayLayerWindow(wide, 2048)); }, "more array slices than maxImageArrayLayers");
auto xorSwizzle = array;
xorSwizzle.tileMode = TextureTileMode::kR64KBX;
reject([&] { static_cast<void>(ArrayLayerWindow(xorSwizzle, 2048)); }, "depends on the slice");
auto volume = array;
volume.dimension = TextureDimension::k3D;
Require(ArrayLayerWindow(volume, 2048).baseAddress == volume.baseAddress && ArrayLayerWindow(volume, 2048).depthOrLastArray == volume.depthOrLastArray, "a volume deeper than maxImageArrayLayers is not an array and keeps its descriptor");
auto flat = array;
flat.dimension = TextureDimension::k2D;
Require(ArrayLayerWindow(flat, 2048).baseAddress == flat.baseAddress && ArrayLayerWindow(flat, 2048).depthOrLastArray == flat.depthOrLastArray, "a 2D image is one layer whatever its last array field holds and keeps its descriptor");
auto cube = array;
cube.dimension = TextureDimension::kCube;
reject([&] { static_cast<void>(ArrayLayerWindow(cube, 2048)); }, "not a 1D or 2D array");
}
}
@@ -20,6 +20,7 @@
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <thread>
#include <vector>
@@ -30,6 +31,7 @@ using namespace AgcDriver::GuestMemory;
constexpr std::size_t Block = 65536;
#ifdef _WIN32
constexpr wchar_t HostHoleMode[] = L"--host-hole";
constexpr wchar_t ResidentRemapMode[] = L"--resident-direct-remap";
#endif
void Require(bool condition, const std::string& message) {
@@ -647,6 +649,82 @@ void CheckRewatch() {
GuestArena::GuestArenaRelease_nid_postfix(memory, 4 * Block);
}
void CheckResidentDirectMappingReuse() {
constexpr std::size_t bytes = 5 * Block;
auto* memory = static_cast<std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block));
Require(memory != nullptr, "cannot reserve the resident remap fixture");
struct Cleanup {
std::uint8_t* memory;
std::array<HANDLE, 3> sections{};
~Cleanup() {
GuestArena::GuestArenaReset_nid_postfix(memory, bytes);
GuestArena::GuestArenaRelease_nid_postfix(memory, bytes);
for (const auto section : sections) {
if (section == nullptr) continue;
GuestArena::GuestArenaForgetSection_nid_postfix(section);
CloseHandle(section);
}
}
} cleanup{memory};
GuestArena::GuestArenaCommit_nid_postfix(memory, 3 * Block, PAGE_READWRITE, 3 * Block);
GuestArena::GuestArenaCommit_nid_postfix(memory + 3 * Block, 2 * Block, PAGE_READWRITE, 2 * Block);
Require(Watched(reinterpret_cast<std::uint64_t>(memory), bytes), "the resident remap fixture is not watched");
for (auto& section : cleanup.sections) {
section = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_EXECUTE_READWRITE, 0, 3 * Block, nullptr);
Require(section != nullptr, "cannot create the resident remap backing");
}
const auto base = reinterpret_cast<std::uint64_t>(memory);
const auto nativeType = [&](std::size_t offset) {
MEMORY_BASIC_INFORMATION info{};
Require(VirtualQuery(memory + offset, &info, sizeof(info)) == sizeof(info), "cannot query the resident remap backing");
return info.Type;
};
for (std::size_t use = 0; use < 2; ++use) {
BumpCollectEpoch();
const auto before = CollectWrites(base, 3 * Block);
Require(before != 0, "the backing before a resident remap was not collected");
Unwatch(base + Block, Block);
Require(!Watched(base + Block, Block), "the resident remap exclusion was not established");
const auto outside = CollectWrites(base + 3 * Block, 2 * Block);
const auto mapping = GuestArena::GuestArenaCommitGeneration_nid_postfix();
GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[use], 0, PAGE_READWRITE);
Require(nativeType(0) == MEM_PRIVATE && nativeType(2 * Block) == MEM_PRIVATE, "the resident remap did not create private memory");
Require(Watched(base, 3 * Block), "a resident direct remap retained an old import exclusion");
Require(GuestArena::GuestArenaCommitGeneration_nid_postfix() > mapping, "a resident direct remap reused the mapping generation");
Require(!UnchangedSince(base, Block, before) && !UnchangedSince(base + 2 * Block, Block, before), "a resident direct remap reused old neighbour stamps");
Require(UnchangedSince(base + 3 * Block, 2 * Block, outside), "a resident direct remap invalidated memory outside its range");
const auto after = CollectWritesUncached(base, 3 * Block);
Require(after > before && UnchangedSince(base, 3 * Block, after), "the resident remap could not establish a current generation");
memory[Block + 9] = static_cast<std::uint8_t>(0x51 + use);
CollectWritesUncached(base, 3 * Block);
Require(!UnchangedSince(base + Block, Block, after), "a CPU store after a resident direct remap was missed");
std::cout << "resident direct remap " << use << ": private, watched, new mapping generation, CPU store collected\n";
}
Unwatch(base, 3 * Block);
GuestArena::GuestArenaMap_nid_postfix(memory, 3 * Block, cleanup.sections[0], 0, PAGE_READWRITE);
Require(nativeType(0) == MEM_MAPPED, "the shared remap did not create mapped memory");
for (std::size_t block = 0; block < 3; ++block) Require(!Watched(base + block * Block, Block), "an ordinary shared remap restored a private import exclusion");
GuestArena::GuestArenaMapDirect_nid_postfix(memory + 3 * Block, 2 * Block, cleanup.sections[2], 0, PAGE_READWRITE);
GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[2], 0, PAGE_READWRITE);
Require(nativeType(0) == MEM_MAPPED && nativeType(Block) == MEM_MAPPED && nativeType(2 * Block) == MEM_PRIVATE, "the mixed direct remap did not establish shared and private spans");
Require(!Watched(base, Block) && !Watched(base + Block, Block) && Watched(base + 2 * Block, Block), "the mixed direct remap restored an exclusion outside its new private span");
std::cout << "resident direct remap controls: shared exclusions retained, only the new private span restored\n";
Unwatch(base, 3 * Block);
bool refused = false;
try {
GuestArena::GuestArenaMapDirect_nid_postfix(memory, 3 * Block, cleanup.sections[1], 0, 0);
} catch (const std::system_error& error) {
Require(error.code().value() == ERROR_INVALID_PARAMETER, "the failed resident remap reported an unexpected error");
refused = true;
}
std::uintptr_t createdBegin = 0;
std::uintptr_t createdEnd = 0;
Require(refused && GuestArena::GuestArenaResidentSpan_nid_postfix(base, 1, &createdBegin, &createdEnd), "the failing protection change did not leave a new private span");
Require(createdBegin == base && createdEnd <= base + 3 * Block && Watched(base, createdEnd - base), "a failed resident remap did not report its completed private span");
for (auto block = createdEnd; block < base + 3 * Block; block += Block) Require(!Watched(block, Block), "a failed resident remap restored an uncreated span");
std::cout << "resident direct remap failure: completed private span watched, original mapping error retained\n";
}
void CheckUntrackedHint() {
constexpr std::size_t bytes = 4 * Block;
auto* memory = static_cast<volatile std::uint8_t*>(GuestArena::GuestArenaAllocate_nid_postfix(bytes, Block));
@@ -1044,6 +1122,37 @@ void CheckImportWindowZeroPages() {
Require(changed[4] == BlockWritten && WrittenSince(base + 4 * Block, Block, synced), "a store of new bytes after the zero pages of an import window was missed");
}
void CheckImportWindowOverUnwatchedBlock() {
void* memory = AllocateWatched(4 * Block);
const auto base = reinterpret_cast<std::uint64_t>(memory);
auto* bytes = static_cast<volatile std::uint8_t*>(memory);
std::memset(memory, 0x11, 4 * Block);
Unwatch(base + Block, Block);
const auto synced = CollectWritesUncached(base + 2 * Block, 2 * Block);
const auto syncedFirst = CollectWritesUncached(base, Block);
Require(synced != 0 && syncedFirst != 0, "the watched blocks around the unwatched one are not collected");
bytes[0x500] = 0x44;
Require(ImportWatched(base, 4 * Block, [&] {
bytes[0x100] = 0x11;
bytes[2 * Block + 0x100] = 0x11;
bytes[3 * Block + 0x2000] = 0x22;
return true;
}), "the import over an unwatched block failed");
Require(!Watched(base + Block, Block), "an import window made an unwatched block watched");
Require(Watched(base, Block) && Watched(base + 2 * Block, 2 * Block), "an import window over an unwatched block took the watched blocks out of the write watch");
const std::array<std::uint64_t, 2> generations{synced, synced};
std::array<std::uint8_t, 2> changed{};
std::array<std::uint8_t, 2> cpu{};
Require(ChangedBlocks(base + 2 * Block, 2 * Block, generations, changed, cpu), "the watched blocks after an unwatched block are not tracked");
Require(changed[0] == BlockUnchanged && UnchangedSince(base + 2 * Block, Block, synced), "a page an import window over an unwatched block resolved without changing its bytes reads as written");
Require(changed[1] == BlockWritten && WrittenSince(base + 3 * Block, Block, synced), "a store of new bytes within an import window over an unwatched block was missed");
Require(WrittenSince(base, Block, syncedFirst), "a store before an import window over an unwatched block was missed");
const auto after = CollectWritesUncached(base, Block);
bytes[0x100] = 0x33;
static_cast<void>(CollectWritesUncached(base, Block));
Require(after != 0 && !UnchangedSince(base, Block, after), "a store after an import window over an unwatched block was missed");
}
void ProtectPage(std::uint64_t address, bool readable) {
#ifdef _WIN32
DWORD previous = 0;
@@ -1300,6 +1409,10 @@ int main() {
CheckHostHole();
return 0;
}
if (std::wstring_view(GetCommandLineW()).ends_with(ResidentRemapMode)) {
CheckResidentDirectMappingReuse();
return 0;
}
#endif
CheckSharedBlock();
CheckStoredAllSince();
@@ -1327,6 +1440,7 @@ int main() {
CheckImportWindow();
CheckImportWindowZeroPages();
CheckImportWindowOtherThread();
CheckImportWindowOverUnwatchedBlock();
CheckStoreAcrossImports();
CheckUnwatchedImport();
CheckCommittedRanges();
@@ -629,6 +629,52 @@ void DccFastClearTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSa
}
}
}
for (const auto samples : {2u, 4u, 8u}) {
if (selectedSamples != 0 && samples != selectedSamples) continue;
blocks.push_back(std::make_unique<Block>(layout.Bytes() * samples));
auto& source = *blocks.back();
std::memset(source.data, 0, source.bytes);
blocks.push_back(std::make_unique<Block>(BlockBytes));
auto& metadata = *blocks.back();
std::memset(metadata.data, 0, BlockBytes);
auto first = DecodeTarget(source, samples);
first.dccAddress = metadata.Address();
first.dccPipeAligned = true;
const Color written{0.25f, 0.25f, 0.25f, 0.25f};
Draw(device, first, false, Float32Export, Program(written, Float32Export, 1u), false, true);
const std::vector<std::byte> ones(AgcDriver::Graphics::MultisampledDccBytes(first), std::byte{0xc0});
AgcDriver::GuestMemory::Write(metadata.Address(), ones);
auto second = first;
second.pipeBankXor = 0x4000u;
Draw(device, second, false, Float32Export, Program(written, Float32Export, 1u), false, true);
blocks.push_back(std::make_unique<Block>(layout.Bytes()));
auto& resolved = *blocks.back();
std::memset(resolved.data, 0, resolved.bytes);
AgcDriver::Graphics::ColorMetadataPass pass{};
pass.mode = AgcDriver::Graphics::ColorMetadataPass::Mode::Resolve;
pass.source = second;
pass.targets = {DecodeTarget(resolved)};
device.ColorMetadataPass(pass);
const auto stored = ReadBack(device, resolved);
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) {
const auto expected = x < ScissorWidth ? 1.0f + (written[channel] - 1.0f) / static_cast<float>(samples) : 1.0f;
Require(FromHalf(stored[layout.Offset(x, y) / 2u + channel]) == expected, "a multisampled DCC target recreated with another pipe-bank XOR did not start from its new fast clear");
}
}
}
auto third = second;
third.dccAlphaOnMsb = true;
bool refused = false;
try {
Draw(device, third, false, Float32Export, Program(written, Float32Export, 1u), false, true);
} catch (const std::runtime_error& error) {
refused = std::string_view(error.what()).find("layout or metadata changed") != std::string_view::npos;
}
Require(refused, "a resident multisampled DCC target accepted a use with other DCC metadata");
programCount = 0;
}
for (const auto samples : {2u, 4u, 8u}) {
if (selectedSamples != 0 && samples != selectedSamples) continue;
for (const auto mode : {0u, 1u, 2u, 3u, 4u, 5u, 6u, 7u}) {
@@ -818,6 +864,56 @@ void DccRefillTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSampl
}
}
void DccImportWindowTests(AgcDriver::VulkanDevice& device, std::uint32_t selectedSamples) {
if (!AgcDriver::GuestMemory::WriteWatched()) return;
const ColorTargetLayout layout(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 Color written{0.25f, 0.25f, 0.25f, 0.25f};
for (const auto samples : {2u, 4u, 8u}) {
if (selectedSamples != 0 && samples != selectedSamples) continue;
blocks.push_back(std::make_unique<Block>(layout.Bytes() * samples));
auto& source = *blocks.back();
std::memset(source.data, 0, source.bytes);
blocks.push_back(std::make_unique<Block>(BlockBytes));
auto& metadata = *blocks.back();
std::memset(metadata.data, 0, BlockBytes);
auto target = DecodeTarget(source, samples);
target.dccAddress = metadata.Address();
target.dccPipeAligned = true;
target.dccAlphaOnMsb = true;
Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true);
Require(AgcDriver::GuestMemory::ImportWatched(metadata.Address(), BlockBytes, [&] {
auto* first = reinterpret_cast<volatile std::uint8_t*>(metadata.data);
*first = *first;
return true;
}), "the import over resident multisampled DCC metadata failed");
Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true);
Require(AgcDriver::GuestMemory::ImportWatched(metadata.Address(), BlockBytes, [&] {
auto* first = reinterpret_cast<volatile std::uint8_t*>(metadata.data);
*first = static_cast<std::uint8_t>(*first ^ 0xffu);
return true;
}), "the second import over resident multisampled DCC metadata failed");
bool refused = false;
try {
Draw(device, target, false, Float32Export, Program(written, Float32Export, 1u), false, true);
} catch (const std::runtime_error& error) {
refused = std::string_view(error.what()).find("writes to a resident multisampled DCC") != std::string_view::npos;
}
Require(refused, "a store of new bytes to resident multisampled DCC metadata within an import window was not refused");
programCount = 0;
}
}
int main(int argc, char** argv) {
try {
const auto device = OpenVulkanTestDevice();
@@ -828,6 +924,7 @@ int main(int argc, char** argv) {
try {
DccFastClearTests(*device, samples);
DccRefillTests(*device, samples);
DccImportWindowTests(*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());
@@ -0,0 +1,248 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ColorTargetLayout.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/PlaceholderImages.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include "prx/libc/include/GuestArena.hpp"
#include "prx/libc/include/GuestWriteWatch.hpp"
#include "VulkanTestDevice.hpp"
#include "ImageTableAbi.hpp"
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
#include <sys/mman.h>
#endif
#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <mutex>
#include <stdexcept>
#include <string_view>
namespace AgcDriver {
struct VulkanDeviceTestAccess {
static Graphics::Context ContextOf(const VulkanDevice& device) { return device.graphicsContext(); }
};
}
namespace {
using namespace AgcDriver::Graphics;
constexpr std::uint32_t Side = 64;
constexpr std::size_t BlockBytes = 65536;
constexpr std::size_t AllocationBytes = 2 * BlockBytes;
class GuestBlock {
public:
GuestBlock() {
#ifdef _WIN32
data = GuestArena::GuestArenaAllocate_nid_postfix(AllocationBytes, BlockBytes);
if (data != nullptr) GuestArena::GuestArenaCommit_nid_postfix(data, AllocationBytes, PAGE_READWRITE, AllocationBytes);
#else
void* mapped = mmap(nullptr, AllocationBytes + BlockBytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (mapped != MAP_FAILED) {
const auto begin = reinterpret_cast<std::uintptr_t>(mapped);
const auto aligned = (begin + BlockBytes - 1) & ~(static_cast<std::uintptr_t>(BlockBytes) - 1);
if (aligned != begin) munmap(mapped, aligned - begin);
if (aligned + AllocationBytes != begin + AllocationBytes + BlockBytes) munmap(reinterpret_cast<void*>(aligned + AllocationBytes), begin + BlockBytes - aligned);
data = reinterpret_cast<void*>(aligned);
GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(data, AllocationBytes);
}
#endif
Require(data != nullptr, "cannot allocate tracked image memory");
std::memset(data, 0, AllocationBytes);
GuestAllocations::Mutation().Add(data, AllocationBytes, true, true, true);
}
~GuestBlock() {
GuestAllocations::Mutation().Remove(data);
#ifdef _WIN32
GuestArena::GuestArenaReset_nid_postfix(data, AllocationBytes);
GuestArena::GuestArenaRelease_nid_postfix(data, AllocationBytes);
#else
GuestWriteWatch::GuestWriteWatchUnregister_nid_postfix(data, AllocationBytes);
munmap(data, AllocationBytes);
#endif
}
GuestBlock(const GuestBlock&) = delete;
GuestBlock& operator=(const GuestBlock&) = delete;
std::uint64_t Address() const { return reinterpret_cast<std::uintptr_t>(data); }
private:
void* data = nullptr;
};
template<typename TCall>
void RequireColorGuard(TCall&& call, const char* failure) {
bool guarded = false;
try {
call();
} catch (const std::runtime_error& error) {
guarded = std::string_view(error.what()).find("multisampled color target") != std::string_view::npos;
if (!guarded) throw;
}
Require(guarded, failure);
}
void CheckCachedImage(AgcDriver::VulkanDevice& device, bool sampled, bool draw) {
std::printf("checking %s %s image reuse\n", draw ? "draw" : "compute", sampled ? "sampled" : "storage");
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
GuestBlock block;
struct Cleanup {
VkDevice device;
~Cleanup() {
ClearMultisampleTargets(device);
ClearCachedTextures(device);
}
} cleanup{context.device};
const auto address = block.Address();
ShaderRecompiler::DescriptorBinding binding{};
binding.kind = sampled ? ShaderRecompiler::DescriptorKind::SampledImage : ShaderRecompiler::DescriptorKind::StorageImage;
binding.role = ShaderRecompiler::DescriptorRole::GuestImages;
binding.binding = (sampled ? ShaderRecompiler::RuntimeAbi::FirstImageBinding : ShaderRecompiler::RuntimeAbi::FirstStorageImageBinding) + PlaceholderDimensionsPerClass + 2u;
binding.count = 1;
binding.imageShape = ShaderRecompiler::DescriptorImageShape::Image2D;
binding.imageWritten = {false};
binding.imageSamplers = {0u};
binding.guestDescriptor = {static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (20u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (9u << 28u), 0u, 0u, 0u, 0u};
ShaderRecompiler::RecompileResult vertex;
ShaderRecompiler::RecompileResult program;
program.bindings.push_back(binding);
const CompiledShader shader{draw ? ShaderRecompiler::ShaderStage::Fragment : ShaderRecompiler::ShaderStage::Compute, &program, 0};
const std::array<CompiledShader, 2> stages{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, shader}};
const auto build = [&] {
if (draw) return std::make_unique<ShaderResources>(context, stages, ColorTarget{}, 0, 0, 0);
return std::make_unique<ShaderResources>(context, shader);
};
auto resources = build();
Require(resources->Reusable(), "the ordinary image resources are not reusable");
const auto revalidate = [&](ShaderResources::ProofReport* report = nullptr) {
return draw ? resources->Revalidate(stages, report) : resources->Revalidate(shader, report);
};
ShaderResources::ProofReport proof;
Require(revalidate(&proof), "the unchanged image resources failed their initial proof");
if (std::getenv("APS5_NO_FAST_REVALIDATE") == nullptr) {
if (AgcDriver::GuestMemory::Watched(address, Side * Side * 4u)) Require(proof.path == ShaderResources::ProofPath::Fast, "the fixture did not exercise the fast image proof");
else std::puts("image memory is compared, not watched: fast image proof not tested; checking the image guards through the full proof");
}
ColorTarget target{};
target.address = address + BlockBytes;
target.surfaceAddress = target.address;
target.extent = {Side, Side};
target.surfaceExtent = target.extent;
target.format = VK_FORMAT_R8G8B8A8_UNORM;
target.tileMode = ColorTileMode::RenderTarget;
target.elementBytes = 4;
target.bytes = Side * Side * 4u;
target.samples = 4;
Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the disjoint multisample target has no view");
device.SubmitRecorded(true);
device.WaitIdle();
Require(revalidate(), "a disjoint multisample target prevented image reuse");
target.address = address;
target.surfaceAddress = address;
Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the overlapping multisample target has no view");
device.SubmitRecorded(true);
device.WaitIdle();
RequireColorGuard([&] { revalidate(); }, "a cached image access accepted memory owned by a multisampled color target");
RequireColorGuard([&] { build(); }, "a fresh image access accepted memory owned by a multisampled color target");
ClearMultisampleTargets(context.device);
Require(revalidate(), "retiring the multisample targets did not restore image reuse");
}
void CheckMultisampleTable(AgcDriver::VulkanDevice& device) {
namespace Abi = ShaderRecompiler::ImageTableAbi;
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(device);
if (context.imageTableCapacity == 0) {
std::puts("skipped, native multisample table check needs image table support");
return;
}
GuestBlock block;
struct Cleanup {
VkDevice device;
~Cleanup() { ClearMultisampleTargets(device); }
} cleanup{context.device};
ColorTarget target{};
target.address = block.Address();
target.surfaceAddress = target.address;
target.extent = {Side, Side};
target.surfaceExtent = target.extent;
target.format = VK_FORMAT_R8G8B8A8_UNORM;
target.tileMode = ColorTileMode::RenderTarget;
target.elementBytes = 4;
target.bytes = Side * Side * 4u;
target.samples = 4;
Require(MultisampleTargetView(context, target) != VK_NULL_HANDLE, "the native multisample table target has no view");
device.SubmitRecorded(true);
device.WaitIdle();
ShaderRecompiler::RecompileResult program;
ShaderRecompiler::DescriptorBinding map{};
map.kind = ShaderRecompiler::DescriptorKind::StorageBuffer;
map.role = ShaderRecompiler::DescriptorRole::ImageTableMap;
map.binding = static_cast<std::uint32_t>(ShaderRecompiler::RuntimeAbi::Binding::ImageTableMap);
map.count = 1;
map.guestDescriptor.resize(Abi::HeaderWords + 1u);
map.guestDescriptor[Abi::HeaderVersion] = Abi::Version;
map.guestDescriptor[Abi::HeaderElementCount] = 1u;
map.guestDescriptor[Abi::HeaderElementFaults] = Abi::HeaderWords;
map.guestDescriptor[Abi::HeaderSamplerFaults] = Abi::HeaderWords + 1u;
program.bindings.push_back(map);
ShaderRecompiler::DescriptorBinding image{};
image.kind = ShaderRecompiler::DescriptorKind::SampledImage;
image.role = ShaderRecompiler::DescriptorRole::ImageTable;
image.binding = ShaderRecompiler::RuntimeAbi::ImageTableBinding;
image.count = 1;
image.imageShapes = {ShaderRecompiler::DescriptorImageShape::Image2DMultisample};
image.imageDepthCompare = {false};
image.imageWritten = {false};
image.imageSamplers = {0u};
const auto address = block.Address();
image.guestDescriptor = {static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (56u << 20u) | (((Side - 1u) & 3u) << 30u), ((Side - 1u) >> 2u) | ((Side - 1u) << 14u), 0xfacu | (2u << 16u) | (0x1bu << 20u) | (14u << 28u), 0u, 2u << 4u, 0u, 0u};
program.bindings.push_back(image);
const CompiledShader shader{ShaderRecompiler::ShaderStage::Compute, &program, 0};
ShaderResources resources(context, shader);
Require(resources.SampledTextures().size() == 1u, "the native multisample table entry was rejected during construction");
Require(resources.Reusable(), "the native multisample table did not reach resource validation");
static_cast<void>(resources.Revalidate(shader));
device.SubmitRecorded(true);
device.WaitIdle();
std::puts("native multisample table validation did not reject its color target");
}
}
int main() {
try {
const auto device = OpenVulkanTestDevice();
if (!device) return VulkanTestSkipped;
const auto context = AgcDriver::VulkanDeviceTestAccess::ContextOf(*device);
if (!AgcDriver::GuestMemory::WriteWatched() || (context.limits.framebufferColorSampleCounts & VK_SAMPLE_COUNT_4_BIT) == 0) {
Require(std::getenv("ANYPS5_REQUIRE_VULKAN") == nullptr, "cached image guard tests require write tracking and 4x color targets");
std::puts("skipped, cached image guard tests require write tracking and 4x color targets");
return VulkanTestSkipped;
}
std::lock_guard gpu(AgcDriver::GuestMemory::GpuMutex());
for (const bool draw : {false, true}) {
for (const bool sampled : {false, true}) CheckCachedImage(*device, sampled, draw);
}
CheckMultisampleTable(*device);
std::puts("cached image multisample guard tests passed");
return 0;
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
Loaded 100 of 723 files, more files were not shown because too many files have changed in this diff. Show more