mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
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:
commit
143ed02750
723 files changed
+32965
-2820
No files matched your search
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ concurrency:
|
||||
jobs:
|
||||
labels:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]; };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+2
-2
@@ -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() == ∑
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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(), ®ion, 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
Reference in new issue
Block a user