mirror of
https://github.com/boykopovar/AnyPS5.git
synced 2026-10-27 00:28:14 +00:00
Merge remote-tracking branch 'origin/main' into fix/windows-runtime-module-start
This commit is contained in:
commit
079e6ca316
489 files changed
+21305
-1705
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
|
||||
|
||||
+17
-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}")
|
||||
@@ -167,6 +170,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}
|
||||
@@ -184,7 +189,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,10 +213,15 @@ 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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -354,6 +354,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 +408,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 +478,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;
|
||||
|
||||
@@ -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
|
||||
@@ -804,6 +807,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 +833,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)
|
||||
@@ -1124,6 +1153,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)
|
||||
@@ -1406,6 +1441,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)
|
||||
@@ -1842,10 +1883,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
|
||||
@@ -1863,7 +1905,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>")
|
||||
@@ -1943,6 +1985,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>)
|
||||
|
||||
@@ -2031,7 +2074,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_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_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()
|
||||
@@ -2065,6 +2108,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)
|
||||
@@ -2083,6 +2127,9 @@ 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)
|
||||
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()
|
||||
if(WIN32)
|
||||
add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint)
|
||||
@@ -2125,12 +2172,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)
|
||||
@@ -2325,6 +2377,14 @@ 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)
|
||||
@@ -2549,6 +2609,7 @@ if(AGC_BUILD_VISUAL_TEST)
|
||||
|
||||
target_link_libraries(agc_driver_visual_test PRIVATE
|
||||
shader_recompiler_agc_driver
|
||||
decoder_png
|
||||
SDL2-static
|
||||
libc
|
||||
libkernel
|
||||
@@ -3139,6 +3200,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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
@@ -361,6 +361,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;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"
|
||||
@@ -69,6 +70,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 +87,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 +108,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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -3060,6 +3078,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;
|
||||
@@ -3111,10 +3136,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;
|
||||
@@ -3160,9 +3189,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;
|
||||
@@ -3173,6 +3200,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{};
|
||||
@@ -3182,14 +3212,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");
|
||||
@@ -3363,7 +3400,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;
|
||||
@@ -3431,6 +3468,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -196,6 +196,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 +805,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);
|
||||
|
||||
@@ -75,8 +75,8 @@ struct SurfaceGeometry {
|
||||
};
|
||||
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor);
|
||||
bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels);
|
||||
bool TracksBlockUnits(const GuestTextureResource& surface, const SurfaceGeometry& geometry);
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@@ -2103,6 +2103,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");
|
||||
|
||||
@@ -2840,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() {
|
||||
@@ -3072,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);
|
||||
@@ -4124,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);
|
||||
|
||||
@@ -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 {
|
||||
@@ -3396,6 +3397,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 +3451,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 +3970,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"
|
||||
@@ -274,16 +275,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 +308,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 +553,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 +663,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)) {
|
||||
@@ -1095,7 +1102,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;
|
||||
@@ -1220,6 +1229,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;
|
||||
{
|
||||
@@ -5477,13 +5490,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 +5549,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 {
|
||||
|
||||
@@ -710,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();
|
||||
@@ -1373,7 +1374,7 @@ StorageTexture::StorageTexture(const Context& context, TextureDetiler& detiler,
|
||||
const bool layered = geometry.layers >= 2 && !geometry.thick && geometry.imageDepth == 1 && geometry.layers == geometry.imageLayers && geometry.layerBytes * geometry.layers == guestBytes && geometry.layerBytes % 65536 == 0 && descriptor.baseAddress % 65536 == 0;
|
||||
// Tile blocks divide the stamp blocks, so at an aligned base every unit is whole tile blocks
|
||||
// of its (layer, mip) slices (sliceWindows).
|
||||
blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && TracksBlockUnits(descriptor, geometry);
|
||||
blockUnits = LayerTrackingEnabled() && BlockTrackingEnabled() && !geometry.thick && descriptor.tileMode != TextureTileMode::kLinear && descriptor.baseAddress % 65536 == 0;
|
||||
trackedLayers = blockUnits ? static_cast<std::uint32_t>((guestBytes + 65535) / 65536) : layered && LayerTrackingEnabled() ? geometry.layers : 1u;
|
||||
trackedLayerBytes = blockUnits ? 65536 : guestBytes / trackedLayers;
|
||||
layerGeneration.assign(trackedLayers, 0);
|
||||
@@ -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;
|
||||
|
||||
@@ -466,10 +466,6 @@ bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t leve
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TracksBlockUnits(const GuestTextureResource& surface, const SurfaceGeometry& geometry) {
|
||||
return !geometry.thick && surface.tileMode != TextureTileMode::kLinear && surface.baseAddress % 65536 == 0 && (surface.dccAddress == 0 || surface.mipCount == 1);
|
||||
}
|
||||
|
||||
GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed) {
|
||||
const bool layered = viewed.dimension == TextureDimension::k2DArray || viewed.dimension == TextureDimension::k1DArray || viewed.dimension == TextureDimension::kCube;
|
||||
if (!layered || viewed.baseArray == 0 || viewed.baseArray > viewed.depthOrLastArray || viewed.dccAddress != 0) return viewed;
|
||||
@@ -490,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);
|
||||
|
||||
@@ -518,6 +518,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);
|
||||
}
|
||||
@@ -551,9 +565,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));
|
||||
|
||||
@@ -3238,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;
|
||||
|
||||
@@ -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,102 @@ 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 reversesTo(std::uint32_t word, std::uint32_t expected) {
|
||||
IrProgram program;
|
||||
auto& block = program.CreateBlock();
|
||||
@@ -118,6 +214,12 @@ 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());
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -8721,6 +8751,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 +9759,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 +9796,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 +9995,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 +10026,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 +10040,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 +10079,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 +10122,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 +10297,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -973,6 +973,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};
|
||||
@@ -2321,6 +2377,12 @@ 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]) == "--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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -141,27 +141,6 @@ void requireSliceAddresses(const GuestTextureResource& resource, std::uint32_t b
|
||||
}
|
||||
|
||||
void RunTextureTilingTests() {
|
||||
{
|
||||
GuestTextureResource surface{};
|
||||
surface.baseAddress = 0x30000000;
|
||||
surface.width = 1920;
|
||||
surface.height = 1080;
|
||||
surface.mipCount = 6;
|
||||
surface.tileMode = TextureTileMode::kR64KBX;
|
||||
surface.dimension = TextureDimension::k2D;
|
||||
surface.format = 71;
|
||||
const auto tracks = [](const GuestTextureResource& resource) { return TracksBlockUnits(resource, DescribeSurface(resource)); };
|
||||
Require(tracks(surface), "a tiled surface without DCC metadata must be tracked as 64 KiB blocks");
|
||||
surface.dccAddress = 0x31000000;
|
||||
Require(!tracks(surface), "a DCC surface with more than one mip must be tracked as one unit");
|
||||
surface.mipCount = 1;
|
||||
Require(tracks(surface), "a DCC surface with one mip must be tracked as 64 KiB blocks");
|
||||
surface.baseAddress += 0x1000;
|
||||
Require(!tracks(surface), "a surface off a 64 KiB boundary must be tracked as one unit");
|
||||
surface.baseAddress -= 0x1000;
|
||||
surface.tileMode = TextureTileMode::kLinear;
|
||||
Require(!tracks(surface), "a linear surface must be tracked as one unit");
|
||||
}
|
||||
{
|
||||
const auto mips = ComputeMipLayout(TextureTileMode::kLinear, 1, 4, 4, 2);
|
||||
Require(mips.size() == 2, "linear mip chain must contain the requested mip count");
|
||||
@@ -461,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");
|
||||
}
|
||||
}
|
||||
@@ -1044,6 +1044,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;
|
||||
@@ -1327,6 +1358,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());
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
@@ -88,6 +89,10 @@ void CheckTable() {
|
||||
}
|
||||
|
||||
int main() {
|
||||
#ifdef ANYPS5_ENABLE_ASAN
|
||||
std::puts("skipped: the shader calls through the FS table at the PS5's fixed 0xfe0040000, which an ANYPS5_ENABLE_ASAN build cannot map");
|
||||
return VulkanTestSkipped;
|
||||
#endif
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t Lanes = 32;
|
||||
constexpr std::uint32_t BufferBytes = Lanes * 4u;
|
||||
constexpr std::uint32_t ComputeQueue = 0x20;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::uint32_t Unwritten = 0xcafebabeu;
|
||||
|
||||
std::uint32_t* Constants = nullptr;
|
||||
std::uint32_t* Input = nullptr;
|
||||
std::uint32_t* Output = nullptr;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> AddressCode{0xf4000200u, 0xfa000000u, 0x34020082u, 0xdc308000u, 0x03020001u, 0xbf8cc07fu, 0x7e040208u, 0x4a040481u, 0xbf8c3f70u, 0x4a040503u, 0xe0701000u, 0x80010201u, 0xbf810000u};
|
||||
|
||||
std::uint32_t* NewBlock() {
|
||||
#ifdef _WIN32
|
||||
auto* block = static_cast<std::uint32_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
auto* block = static_cast<std::uint32_t*>(std::aligned_alloc(BlockBytes, BlockBytes));
|
||||
#endif
|
||||
if (block == nullptr) throw std::runtime_error("dispatch data hit words: cannot allocate a guest block");
|
||||
std::memset(block, 0, BlockBytes);
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true, true);
|
||||
return block;
|
||||
}
|
||||
|
||||
void check(bool condition, const std::string& what) {
|
||||
if (!condition) throw std::runtime_error("dispatch data hit words: " + what);
|
||||
}
|
||||
|
||||
template <typename Ready>
|
||||
void WaitUntil(Ready ready, const std::string& what) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||
while (!ready()) {
|
||||
if (std::chrono::steady_clock::now() >= deadline) throw std::runtime_error("dispatch data hit words: timed out waiting for " + what);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Dispatch() {
|
||||
const auto codeAddress = reinterpret_cast<std::uintptr_t>(AddressCode.data());
|
||||
const auto constants = reinterpret_cast<std::uintptr_t>(Constants);
|
||||
const auto input = reinterpret_cast<std::uintptr_t>(Input);
|
||||
const auto output = BufferDescriptor(Output, BufferBytes);
|
||||
return {
|
||||
0xc0027600u, 0x20cu, static_cast<std::uint32_t>(codeAddress >> 8u), static_cast<std::uint32_t>(codeAddress >> 40u),
|
||||
0xc0037600u, 0x207u, Lanes, 1u, 1u,
|
||||
0xc0017600u, 0x213u, 8u << 1u,
|
||||
0xc0087600u, 0x240u, static_cast<std::uint32_t>(constants), static_cast<std::uint32_t>(constants >> 32u), static_cast<std::uint32_t>(input), static_cast<std::uint32_t>(input >> 32u), output[0], output[1], output[2], output[3],
|
||||
0xc0031500u, 1u, 1u, 1u, 0x8041u,
|
||||
};
|
||||
}
|
||||
|
||||
void SetConstant(std::uint32_t value) {
|
||||
for (std::uint32_t lane = 0; lane < Lanes; ++lane) Constants[lane] = value;
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Constants), BufferBytes);
|
||||
}
|
||||
|
||||
void CheckOutput(std::uint32_t expected, const std::string& what) {
|
||||
WaitUntil([] {
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Output), BufferBytes);
|
||||
for (std::uint32_t lane = 0; lane < Lanes; ++lane) {
|
||||
if (std::atomic_ref<std::uint32_t>(Output[lane]).load(std::memory_order_acquire) == Unwritten) return false;
|
||||
}
|
||||
return true;
|
||||
}, what + " to store");
|
||||
for (std::uint32_t lane = 0; lane < Lanes; ++lane) check(Output[lane] == expected, what + ": lane " + std::to_string(lane) + " is " + std::to_string(Output[lane]) + ", expected " + std::to_string(expected));
|
||||
}
|
||||
|
||||
void Round(std::uint32_t value, const std::string& what) {
|
||||
SetConstant(value);
|
||||
for (std::uint32_t lane = 0; lane < Lanes; ++lane) Output[lane] = Unwritten;
|
||||
auto words = Dispatch();
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
check(sceAgcDriverSubmitAcb(ComputeQueue, &packet) == 0, "submission failed");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
CheckOutput(value + 1u, what);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
{
|
||||
const auto probe = OpenVulkanTestDevice();
|
||||
if (!probe) return VulkanTestSkipped;
|
||||
}
|
||||
|
||||
Shader shader{};
|
||||
shader.file_header = 0x34333231;
|
||||
shader.version = 0x18;
|
||||
shader.header_size = sizeof(Shader);
|
||||
shader.shader_size = sizeof(AddressCode);
|
||||
shader.code = AddressCode.data();
|
||||
AgcDriverRegisterShader_nid_postfix(&shader);
|
||||
|
||||
Constants = NewBlock();
|
||||
Input = NewBlock();
|
||||
Output = NewBlock();
|
||||
|
||||
Round(0x11112222u, "the first dispatch");
|
||||
const std::array<std::uint32_t, 4> values{0x33334444u, 0x55556666u, 0x7a7b7c7du, 0x0f1e2d3cu};
|
||||
for (std::size_t round = 0; round < values.size(); ++round) {
|
||||
const auto mismatches = AgcDriver::Graphics::SnapshotMismatches();
|
||||
const auto what = "data hit " + std::to_string(round + 1) + ", resolved ahead on the compute queue";
|
||||
Round(values[round], what);
|
||||
const auto added = AgcDriver::Graphics::SnapshotMismatches() - mismatches;
|
||||
check(added == 0, what + ": its snapshots differ from the registered memory serving them (" + std::to_string(added) + " snapshot mismatches)");
|
||||
}
|
||||
std::printf("dispatch data hit words: %zu data hits resolved ahead read their own live words\n", values.size());
|
||||
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("dispatch data hit words tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try { LibcRunShutdown_nid_postfix(); } catch (...) {}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,11 @@ int main() {
|
||||
Require(Contains(ranges, written, written + surfaceBytes), "a written image next to a null one is not a write range");
|
||||
Require(!Touches(ranges, 0, surfaceBytes), "a null storage image is a write range");
|
||||
}
|
||||
bool bounded = false;
|
||||
const auto dispatchRanges = AgcDriver::DriverDetail::DispatchWriteRanges(program, bounded);
|
||||
Require(bounded, "a dispatch whose written images are a real one and a null one is not bounded");
|
||||
Require(Contains(dispatchRanges, written, written + surfaceBytes), "a written image next to a null one is not a dispatch write range");
|
||||
Require(!Touches(dispatchRanges, 0, surfaceBytes), "a null storage image is a dispatch write range");
|
||||
AgcDriver::Graphics::State targets{};
|
||||
targets.hasColorTarget = true;
|
||||
auto& color = targets.color;
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "ControlFlow/RequestSerializer.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <iterator>
|
||||
#include <random>
|
||||
#include <span>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 8> Code{
|
||||
0xf4080200u, 0xfa000000u, 0xbf8cc07fu, 0x7e020280u, 0xe0702000u, 0x80020100u, 0xbf810000u, 0u,
|
||||
};
|
||||
constexpr std::uint32_t Untouched = 0xcafebabeu;
|
||||
alignas(256) std::array<std::uint32_t, 64> Output{};
|
||||
alignas(16) std::array<std::uint32_t, 4> Valid{};
|
||||
alignas(16) std::array<std::uint32_t, 4> Unsupported{};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(std::uint32_t word3) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Output.data());
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (4u << 16u), static_cast<std::uint32_t>(Output.size()), word3};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 2> Pointer(const void* descriptor) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(descriptor);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u)};
|
||||
}
|
||||
|
||||
void Dispatch(const std::array<std::uint32_t, 2>& userData) {
|
||||
std::vector<std::uint32_t> words;
|
||||
const auto registers = [&](std::uint32_t first, std::span<const std::uint32_t> values) {
|
||||
words.push_back(0xc0007600u | (static_cast<std::uint32_t>(values.size()) << 16u));
|
||||
words.push_back(first);
|
||||
words.insert(words.end(), values.begin(), values.end());
|
||||
};
|
||||
const std::array<std::uint32_t, 3> shape{64, 1, 1};
|
||||
registers(0x207, shape);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(Code.data());
|
||||
const std::array<std::uint32_t, 2> program{static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u)};
|
||||
registers(0x20c, program);
|
||||
const std::array<std::uint32_t, 1> resource{static_cast<std::uint32_t>(userData.size()) << 1u};
|
||||
registers(0x213, resource);
|
||||
registers(0x240, userData);
|
||||
words.insert(words.end(), {0xc0031500u, 1, 1, 1, 0x41u});
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
std::string Read(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
Require(file.is_open(), "shader request " + path.string() + " was not written");
|
||||
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
|
||||
}
|
||||
|
||||
std::string Replay(const std::string& text) {
|
||||
const auto replay = ShaderRecompiler::RequestSerializer{}.Deserialize(text);
|
||||
try {
|
||||
static_cast<void>(ShaderRecompiler::Recompile(replay.request));
|
||||
} catch (const std::exception& error) {
|
||||
return error.what();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> UserData(const std::string& text) {
|
||||
const auto replay = ShaderRecompiler::RequestSerializer{}.Deserialize(text);
|
||||
return {replay.request.context.userData.begin(), replay.request.context.userData.end()};
|
||||
}
|
||||
|
||||
void Run() {
|
||||
char prefix[64];
|
||||
std::snprintf(prefix, sizeof(prefix), "shader_%llx", static_cast<unsigned long long>(reinterpret_cast<std::uintptr_t>(Code.data())));
|
||||
const std::string first = std::string(prefix) + ".req";
|
||||
const std::string failed = std::string(prefix) + "_failed.req";
|
||||
Valid = BufferDescriptor(0x00014204u);
|
||||
Unsupported = BufferDescriptor(0xa1b00facu);
|
||||
Output.fill(Untouched);
|
||||
const auto valid = Pointer(Valid.data());
|
||||
Dispatch(valid);
|
||||
AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uintptr_t>(Output.data()), sizeof(Output));
|
||||
for (const auto word : Output) Require(word == 0u, "the dispatch through the valid descriptor stored nothing");
|
||||
const auto unsupported = Pointer(Unsupported.data());
|
||||
std::string message;
|
||||
try {
|
||||
Dispatch(unsupported);
|
||||
} catch (const std::exception& error) {
|
||||
message = error.what();
|
||||
}
|
||||
Require(message.find("unsupported type") != std::string::npos, "the dispatch through a type 2 descriptor failed for another reason: " + message);
|
||||
Require(message.find("(" + failed + ")") != std::string::npos, "the failure does not name the failing dispatch's request: " + message);
|
||||
const auto firstText = Read(first);
|
||||
const auto failedText = Read(failed);
|
||||
Require(UserData(firstText) == std::vector<std::uint32_t>(valid.begin(), valid.end()), "the first dispatch's request lost its user data");
|
||||
Require(UserData(failedText) == std::vector<std::uint32_t>(unsupported.begin(), unsupported.end()), "the failed request holds another dispatch's user data");
|
||||
Require(Replay(firstText).empty(), "the first dispatch's request does not replay: " + Replay(firstText));
|
||||
const auto replayed = Replay(failedText);
|
||||
Require(replayed.find("unsupported type") != std::string::npos, "the failed request does not replay its failure: " + replayed);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
const auto directory = std::filesystem::temp_directory_path() / ("agc_failed_capture_dump-" + std::to_string(std::random_device{}()));
|
||||
try {
|
||||
{
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
}
|
||||
std::filesystem::remove_all(directory);
|
||||
std::filesystem::create_directories(directory);
|
||||
std::filesystem::current_path(directory);
|
||||
#ifdef _WIN32
|
||||
Require(_putenv_s("APS5_DUMP_SHADERS", "1") == 0, "cannot enable shader capture");
|
||||
#else
|
||||
Require(setenv("APS5_DUMP_SHADERS", "1", 1) == 0, "cannot enable shader capture");
|
||||
#endif
|
||||
Run();
|
||||
try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {}
|
||||
std::filesystem::current_path(directory.parent_path());
|
||||
std::filesystem::remove_all(directory);
|
||||
std::puts("failed capture dump tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
try { AgcDriverShutdown_nid_postfix(); } catch (const std::exception&) {}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@ void Draw(AgcDriver::VulkanDevice& device, bool allBits) {
|
||||
pixel.inputAddr = ShaderRecompiler::PixelInputBit(ShaderRecompiler::PixelInput::FrontFace);
|
||||
pixel.frontFace = true;
|
||||
pixel.frontFaceAllBits = allBits;
|
||||
pixel.targetOutputMode[0] = 1;
|
||||
pixel.targetOutputMode[0] = 7;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
const std::vector<std::uint32_t> pixelUserData(8, 0u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> pixelMemory{{{reinterpret_cast<std::uintptr_t>(PixelCode.data()), std::as_bytes(std::span(PixelCode))}}};
|
||||
@@ -87,6 +87,7 @@ void Draw(AgcDriver::VulkanDevice& device, bool allBits) {
|
||||
AgcDriver::Graphics::State state{};
|
||||
state.stages = {AgcDriver::Graphics::ShaderPath::Vertex, 0u, WaveSize, WaveSize, std::nullopt, std::nullopt};
|
||||
state.color = {reinterpret_cast<std::uintptr_t>(Pixels.data()), {Width, Height}, VK_FORMAT_R32_UINT, sizeof(Pixels), 0xe4u};
|
||||
state.color.uintExport = true;
|
||||
state.colors = {state.color};
|
||||
state.hasColorTarget = true;
|
||||
state.renderExtent = {Width, Height};
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Width = 8;
|
||||
constexpr std::uint32_t Layers = 3;
|
||||
constexpr std::uint32_t BaseArray = 8100;
|
||||
constexpr std::uint32_t LastArray = BaseArray + Layers - 1u;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
constexpr std::uint32_t Type1DArray = 12;
|
||||
constexpr std::uint32_t ClampEdge = 2;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
|
||||
alignas(256) std::array<float, Threads * 8> Output{};
|
||||
alignas(65536) std::array<std::uint8_t, 4u << 20u> Texels{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 15> Code{
|
||||
0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf09c0f20, 0x00820602, 0xf0001f20, 0x00020a04,
|
||||
0x34020085, 0xbf8c3f70, 0xe0781000, 0x80010601, 0xe0781010, 0x80010a01, 0xbf810000,
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(std::uint32_t baseArray = BaseArray) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Texels.data()));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u),
|
||||
(Width - 1u) >> 2u,
|
||||
0xfacu | (Type1DArray << 28u),
|
||||
LastArray | (baseArray << 16u), 0u, 0u, 0u,
|
||||
};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> SamplerDescriptor() {
|
||||
return {ClampEdge | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u};
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, 4> Texel(std::uint32_t x, std::uint32_t guestLayer) {
|
||||
return {static_cast<std::uint8_t>((guestLayer * Width + x) * 9u + 3u), static_cast<std::uint8_t>(x * 30u + guestLayer % 7u), static_cast<std::uint8_t>(guestLayer), 0xff};
|
||||
}
|
||||
|
||||
void FillTexels() {
|
||||
Texels.fill(0);
|
||||
const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(TextureDescriptor()));
|
||||
const auto& mip = surface.mips.at(0);
|
||||
Require(surface.layers == LastArray + 1u && surface.guestBytes <= Texels.size(), "the test array does not fit its storage");
|
||||
for (std::uint32_t layer = 0; layer <= LastArray; ++layer) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto texel = Texel(x, layer);
|
||||
std::copy(texel.begin(), texel.end(), Texels.begin() + static_cast<std::ptrdiff_t>(surface.GuestLayerOffset(layer) + mip.tiledOffset + x * 4u));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FillInput() {
|
||||
constexpr std::array<float, 8> layers{0.0f, 1.0f, 2.0f, 0.4f, 1.6f, 2.4f, -1.0f, 5.0f};
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
Input[tid * 4u + 0u] = std::bit_cast<std::uint32_t>(-0.2f + 0.045f * static_cast<float>(tid));
|
||||
Input[tid * 4u + 1u] = std::bit_cast<std::uint32_t>(layers[tid % layers.size()]);
|
||||
Input[tid * 4u + 2u] = tid % Width;
|
||||
Input[tid * 4u + 3u] = (tid / Width) % Layers;
|
||||
}
|
||||
}
|
||||
|
||||
void Expect(std::uint32_t tid, const char* name, const float* actual, const std::array<std::uint8_t, 4>& expected) {
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
const float scaled = actual[component] * 255.0f;
|
||||
Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && std::lround(scaled) == expected[component], std::string(name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255");
|
||||
}
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::uint32_t baseArray = BaseArray) {
|
||||
Output.fill(-1.0f);
|
||||
std::vector<std::uint32_t> userData(20, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output)));
|
||||
const auto texture = TextureDescriptor(baseArray);
|
||||
const auto sampler = SamplerDescriptor();
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
|
||||
std::copy(sampler.begin(), sampler.end(), userData.begin() + 16);
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const float u = std::bit_cast<float>(Input[tid * 4u + 0u]);
|
||||
const float layer = std::clamp(std::nearbyint(std::bit_cast<float>(Input[tid * 4u + 1u])), 0.0f, static_cast<float>(Layers - 1u));
|
||||
const auto x = static_cast<std::uint32_t>(std::clamp(static_cast<int>(std::floor(u * static_cast<float>(Width))), 0, static_cast<int>(Width) - 1));
|
||||
Expect(tid, "image_sample_lz on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u], Texel(x, baseArray + static_cast<std::uint32_t>(layer)));
|
||||
Expect(tid, "image_load on a window of a 1D array past maxImageArrayLayers", &Output[tid * 8u + 4u], Texel(Input[tid * 4u + 2u], baseArray + Input[tid * 4u + 3u]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
FillTexels();
|
||||
FillInput();
|
||||
Run(*device);
|
||||
try {
|
||||
Run(*device, 0);
|
||||
} catch (const std::exception& error) {
|
||||
if (std::string(error.what()).find("an image view of more array slices than maxImageArrayLayers") == std::string::npos) throw;
|
||||
}
|
||||
std::puts("a window of a 1D array past maxImageArrayLayers was sampled and loaded from its viewed layers");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -873,7 +873,7 @@ void RunTests(AgcDriver::VulkanDevice& device) {
|
||||
const auto sintControl = RunDirect(device, LinearWrap, sintImage);
|
||||
const auto floatControl = RunDirect(device, LinearWrap, floatImage);
|
||||
const auto uintControl = RunDirect(device, LinearWrap, uintImage);
|
||||
Require(floatControl.samplers == std::vector{LinearWrap} && uintControl.samplers == std::vector{LinearWrap} && sintControl.samplers == std::vector{PointWrap}, "image table: the direct bindings under a linear S# changed their sampler copies");
|
||||
Require(floatControl.samplers == std::vector{LinearWrap} && uintControl.samplers == std::vector{PointWrap} && sintControl.samplers == std::vector{PointWrap}, "image table: the direct bindings under a linear S# changed their sampler copies");
|
||||
Require(Group(integerLinear, 0) == Group(sintControl, 0) && Group(integerLinear, 1) == Group(floatControl, 0), "image table: an entry under a linear direct S# differs from the direct binding");
|
||||
RequireRed(integerLinear, 0, Lanes, [](std::uint32_t x) { return static_cast<std::uint32_t>(SintTexels[x]); }, "sint texels through a linear direct sampler");
|
||||
RequireRed(integerLinear, 2, Lanes, [](std::uint32_t x) { return UintTexels[x]; }, "uint texels through a linear direct sampler");
|
||||
|
||||
@@ -358,6 +358,15 @@ bool WindowsImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& c
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
bool ImportsStayWatchedThroughReads(const AgcDriver::Graphics::Context& context) {
|
||||
if (context.hostImportAlignment == 0) return true;
|
||||
const auto probe = AgcDriver::Graphics::ProbeImportWriteProtection(context);
|
||||
if (probe.failure == nullptr && probe.writtenAfterSubmit == 0) return true;
|
||||
if (probe.failure != nullptr) std::printf("skipped, the device reports no host import probe result: it failed at %s (%d)\n", probe.failure, static_cast<int>(probe.result));
|
||||
else std::printf("skipped, the device reports %u of %u host-imported pages written after a GPU read, so a watched imported table cannot be vouched for\n", probe.writtenAfterSubmit, probe.pages);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
@@ -371,6 +380,8 @@ int main(int argc, char** argv) {
|
||||
#ifdef _WIN32
|
||||
if (argc == 2 && std::string(argv[1]) == "--vouched-lookups" &&
|
||||
!WindowsImportsStayWatchedThroughReads(device->graphicsContext())) return VulkanTestSkipped;
|
||||
#else
|
||||
if (argc == 2 && std::string(argv[1]) == "--vouched-lookups" && !ImportsStayWatchedThroughReads(device->graphicsContext())) return VulkanTestSkipped;
|
||||
#endif
|
||||
}
|
||||
GuestBlock block;
|
||||
|
||||
+29
-17
@@ -22,6 +22,7 @@
|
||||
#include <iostream>
|
||||
#include <new>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
@@ -168,12 +169,14 @@ public:
|
||||
Require(library != nullptr, "cannot load Vulkan for the producer submission gate");
|
||||
resolve = reinterpret_cast<PFN_vkGetDeviceProcAddr>(SDL_LoadFunction(library.get(), "vkGetDeviceProcAddr"));
|
||||
Require(resolve != nullptr, "cannot resolve vkGetDeviceProcAddr for the producer submission gate");
|
||||
const auto createEvent = function<PFN_vkCreateEvent>("vkCreateEvent");
|
||||
destroyEvent = function<PFN_vkDestroyEvent>("vkDestroyEvent");
|
||||
setEvent = function<PFN_vkSetEvent>("vkSetEvent");
|
||||
waitEvents = function<PFN_vkCmdWaitEvents>("vkCmdWaitEvents");
|
||||
const VkEventCreateInfo info{VK_STRUCTURE_TYPE_EVENT_CREATE_INFO};
|
||||
Require(createEvent(device.Device(), &info, nullptr, &event) == VK_SUCCESS, "cannot create the producer submission gate");
|
||||
const auto createSemaphore = function<PFN_vkCreateSemaphore>("vkCreateSemaphore");
|
||||
destroySemaphore = function<PFN_vkDestroySemaphore>("vkDestroySemaphore");
|
||||
signalSemaphore = reinterpret_cast<PFN_vkSignalSemaphore>(resolve(device.Device(), "vkSignalSemaphoreKHR"));
|
||||
if (signalSemaphore == nullptr) signalSemaphore = function<PFN_vkSignalSemaphore>("vkSignalSemaphore");
|
||||
VkSemaphoreTypeCreateInfo type{VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO};
|
||||
type.semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE;
|
||||
VkSemaphoreCreateInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, &type};
|
||||
Require(createSemaphore(device.Device(), &info, nullptr, &semaphore) == VK_SUCCESS, "cannot create the producer submission gate");
|
||||
}
|
||||
|
||||
~ProducerSubmissionGate() {
|
||||
@@ -181,16 +184,17 @@ public:
|
||||
worker.request_stop();
|
||||
worker.join();
|
||||
}
|
||||
setEvent(device.Device(), event);
|
||||
Release();
|
||||
try {
|
||||
device.WaitIdle();
|
||||
} catch (...) {
|
||||
}
|
||||
destroyEvent(device.Device(), event, nullptr);
|
||||
destroySemaphore(device.Device(), semaphore, nullptr);
|
||||
}
|
||||
|
||||
void Record(AgcDriver::Graphics::Recorder& recorder) {
|
||||
waitEvents(recorder.Commands(), 1, &event, VK_PIPELINE_STAGE_HOST_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, nullptr, 0, nullptr, 0, nullptr);
|
||||
void Arm(AgcDriver::Graphics::Recorder& recorder) {
|
||||
recorder.Commands();
|
||||
recorder.HoldNextSubmit(semaphore, 1);
|
||||
}
|
||||
|
||||
void ReleaseAfterSubmission(AgcDriver::Graphics::Recorder& recorder, std::uint64_t serial) {
|
||||
@@ -198,8 +202,8 @@ public:
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!stop.stop_requested() && recorder.NewestSubmitted() < serial && std::chrono::steady_clock::now() < deadline) std::this_thread::sleep_for(std::chrono::microseconds(100));
|
||||
producerSubmitted = recorder.NewestSubmitted() >= serial;
|
||||
releaseResult = setEvent(device.Device(), event);
|
||||
if (!producerSubmitted && !stop.stop_requested()) std::fprintf(stderr, "pending producer gate: watchdog expired with producer batch %llu unsubmitted (vkSetEvent %d)\n", static_cast<unsigned long long>(serial), static_cast<int>(releaseResult));
|
||||
Release();
|
||||
if (!producerSubmitted && !stop.stop_requested()) std::fprintf(stderr, "pending producer gate: watchdog expired with producer batch %llu unsubmitted (vkSignalSemaphore %d)\n", static_cast<unsigned long long>(serial), static_cast<int>(releaseResult));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -218,14 +222,22 @@ private:
|
||||
return function;
|
||||
}
|
||||
|
||||
void Release() {
|
||||
if (signaled.exchange(true)) return;
|
||||
VkSemaphoreSignalInfo info{VK_STRUCTURE_TYPE_SEMAPHORE_SIGNAL_INFO};
|
||||
info.semaphore = semaphore;
|
||||
info.value = 1;
|
||||
releaseResult = signalSemaphore(device.Device(), &info);
|
||||
}
|
||||
|
||||
AgcDriver::VulkanDevice& device;
|
||||
std::unique_ptr<void, decltype(&SDL_UnloadObject)> library{nullptr, &SDL_UnloadObject};
|
||||
PFN_vkGetDeviceProcAddr resolve = nullptr;
|
||||
PFN_vkDestroyEvent destroyEvent = nullptr;
|
||||
PFN_vkSetEvent setEvent = nullptr;
|
||||
PFN_vkCmdWaitEvents waitEvents = nullptr;
|
||||
VkEvent event = VK_NULL_HANDLE;
|
||||
PFN_vkDestroySemaphore destroySemaphore = nullptr;
|
||||
PFN_vkSignalSemaphore signalSemaphore = nullptr;
|
||||
VkSemaphore semaphore = VK_NULL_HANDLE;
|
||||
std::jthread worker;
|
||||
std::atomic<bool> signaled{false};
|
||||
bool producerSubmitted = false;
|
||||
VkResult releaseResult = VK_NOT_READY;
|
||||
};
|
||||
@@ -243,7 +255,7 @@ void RunArgumentLabel(Slot& slot) {
|
||||
auto* recorder = AgcDriver::Graphics::Recorder::Active();
|
||||
Require(recorder != nullptr, "the argument-label test has no active recorder");
|
||||
ProducerSubmissionGate gate(*device);
|
||||
gate.Record(*recorder);
|
||||
gate.Arm(*recorder);
|
||||
const std::uint32_t labelValue = 2;
|
||||
const auto labelAddress = reinterpret_cast<std::uintptr_t>(&slot.arguments[1]);
|
||||
Require(device->WriteLabelOnGpu(labelAddress, std::as_bytes(std::span(&labelValue, 1)), 1, Graphics, false) == 0, "the argument label was not recorded on the GPU");
|
||||
|
||||
@@ -24,6 +24,13 @@ alignas(256) constexpr std::array<std::uint32_t, 43> ScanCode{
|
||||
0xe0701000, 0x80030802, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr auto StoreScanCode = [] {
|
||||
auto code = ScanCode;
|
||||
code[16] = 0xe0745000;
|
||||
code[38] = 0xe0745000;
|
||||
return code;
|
||||
}();
|
||||
|
||||
constexpr std::uint32_t Lanes = 64;
|
||||
constexpr std::uint32_t Blocks = 4096;
|
||||
|
||||
@@ -41,9 +48,17 @@ std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t by
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) {
|
||||
alignas(256) static std::array<std::uint32_t, ScanCode.size()> code{};
|
||||
code = ScanCode;
|
||||
struct Variant {
|
||||
const char* name;
|
||||
const std::array<std::uint32_t, ScanCode.size()>& words;
|
||||
};
|
||||
|
||||
constexpr std::array<Variant, 2> Variants{{{"buffer_atomic_swap_x2", ScanCode}, {"buffer_store_dwordx2", StoreScanCode}}};
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t groups) {
|
||||
alignas(256) static std::array<std::array<std::uint32_t, ScanCode.size()>, Variants.size()> programs{};
|
||||
auto& code = programs[variant];
|
||||
code = Variants[variant].words;
|
||||
std::vector<std::uint32_t> userData;
|
||||
for (const auto& descriptor : {BufferDescriptor(Counter.data(), sizeof(Counter)), BufferDescriptor(Aggregates.data(), sizeof(Aggregates)), BufferDescriptor(Pairs.data(), sizeof(Pairs)), BufferDescriptor(Prefixes.data(), sizeof(Prefixes))}) {
|
||||
userData.insert(userData.end(), descriptor.begin(), descriptor.end());
|
||||
@@ -64,8 +79,8 @@ void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) {
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) {
|
||||
const auto name = "look-back from block " + std::to_string(block);
|
||||
void CheckLookBack(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t block) {
|
||||
const auto name = std::string(Variants[variant].name) + " look-back from block " + std::to_string(block);
|
||||
Pairs.fill(0u);
|
||||
std::uint32_t prefix = 0;
|
||||
for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, block);
|
||||
@@ -77,20 +92,20 @@ void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) {
|
||||
Counter.fill(0u);
|
||||
Counter[0] = block;
|
||||
Prefixes.fill(0xdeadbeefu);
|
||||
Run(device, 1);
|
||||
Run(device, variant, 1);
|
||||
Require(Counter[0] == block + 1, name + ": the counter is " + std::to_string(Counter[0]));
|
||||
Require(Prefixes[block] == prefix, name + ": its prefix is " + std::to_string(Prefixes[block]) + ", expected " + std::to_string(prefix));
|
||||
Require(Pairs[2 * block] == 2u, name + ": its flag is " + std::to_string(Pairs[2 * block]) + ", expected 2");
|
||||
Require(Pairs[2 * block + 1] == prefix + Aggregates[block], name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + Aggregates[block]));
|
||||
}
|
||||
|
||||
void CheckWhole(AgcDriver::VulkanDevice& device, std::uint32_t seed) {
|
||||
const auto name = "whole scan " + std::to_string(seed);
|
||||
void CheckWhole(AgcDriver::VulkanDevice& device, std::size_t variant, std::uint32_t seed) {
|
||||
const auto name = std::string(Variants[variant].name) + " whole scan " + std::to_string(seed);
|
||||
for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, seed);
|
||||
Counter.fill(0u);
|
||||
Pairs.fill(0u);
|
||||
Prefixes.fill(0xdeadbeefu);
|
||||
Run(device, Blocks);
|
||||
Run(device, variant, Blocks);
|
||||
std::uint32_t running = 0;
|
||||
for (std::uint32_t b = 0; b < Blocks; ++b) {
|
||||
Require(Prefixes[b] == running, name + ": block " + std::to_string(b) + "'s prefix is " + std::to_string(Prefixes[b]) + ", expected " + std::to_string(running));
|
||||
@@ -113,8 +128,10 @@ int main() {
|
||||
std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize);
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, block);
|
||||
for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, seed);
|
||||
for (std::size_t variant = 0; variant < Variants.size(); ++variant) {
|
||||
for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, variant, block);
|
||||
for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, variant, seed);
|
||||
}
|
||||
std::puts("look-back scan tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace {
|
||||
|
||||
void QueuedCode(bool cached) {
|
||||
using namespace AgcDriver::DriverDetail;
|
||||
alignas(256) static std::array<std::array<std::uint32_t, 64>, 2> programs{};
|
||||
auto& code = programs[cached ? 0 : 1];
|
||||
code.fill(0xbf810000u);
|
||||
code[0] = 0x7e000200u;
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(code.data());
|
||||
const auto before = cached ? ReadRawShader(address) : nullptr;
|
||||
std::atomic<bool> finished{false};
|
||||
std::atomic<bool> started{false};
|
||||
struct Restore {
|
||||
std::atomic<bool>& finished;
|
||||
bool active = DrawPipeline::Active();
|
||||
~Restore() {
|
||||
finished.store(true, std::memory_order_release);
|
||||
DrawPipeline::Active() = active;
|
||||
DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission);
|
||||
}
|
||||
} restore{finished};
|
||||
DrawPipeline::Queue0().Enqueue([&, address] {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200);
|
||||
started.store(true, std::memory_order_release);
|
||||
while (!finished.load(std::memory_order_acquire) && std::chrono::steady_clock::now() < deadline) std::this_thread::yield();
|
||||
const std::uint32_t replacement = 0x7e0002f2u;
|
||||
AgcDriver::GuestMemory::Write(address, std::as_bytes(std::span(&replacement, 1)), alignof(std::uint32_t));
|
||||
}, {{address, address + sizeof(std::uint32_t)}});
|
||||
while (!started.load(std::memory_order_acquire)) std::this_thread::yield();
|
||||
DrawPipeline::Active() = true;
|
||||
const auto after = ReadRawShader(address);
|
||||
finished.store(true, std::memory_order_release);
|
||||
DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Submission);
|
||||
if (after->code.front() != 0x7e0002f2u || (before != nullptr && (before->code.front() != 0x7e000200u || before == after))) {
|
||||
throw std::runtime_error(std::string(cached ? "cached" : "fresh") + " raw shader read ignored a queued write to its code");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (AgcDriver::DriverDetail::DrawPipeline::Depth() == 0) return 77;
|
||||
if (argc == 1 || std::string(argv[1]) == "cached") QueuedCode(true);
|
||||
if (argc == 1 || std::string(argv[1]) == "fresh") QueuedCode(false);
|
||||
std::cout << "Raw shader reads observed queued code writes\n";
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,9 @@
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -26,8 +28,11 @@
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -48,6 +53,67 @@ constexpr std::size_t AllocationBytes = BlockBytes + RegistrationGapBytes;
|
||||
constexpr std::uint64_t Budget = 1ull << 20u;
|
||||
constexpr std::uint64_t Ceiling = Budget + Budget / 4u;
|
||||
|
||||
std::optional<std::size_t> selectDevice(std::span<const VkPhysicalDeviceProperties> devices, std::string_view request) {
|
||||
const auto rankDeviceType = [](VkPhysicalDeviceType type) {
|
||||
switch (type) {
|
||||
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3;
|
||||
case VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU: return 2;
|
||||
case VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU: return 1;
|
||||
default: return 0;
|
||||
}
|
||||
};
|
||||
std::optional<std::size_t> selected;
|
||||
int selectedRank = -1;
|
||||
for (std::size_t index = 0; index < devices.size(); ++index) {
|
||||
const auto& candidate = devices[index];
|
||||
if (candidate.apiVersion < VK_API_VERSION_1_1) continue;
|
||||
const std::string_view name(candidate.deviceName);
|
||||
if (!request.empty() && std::search(name.begin(), name.end(), request.begin(), request.end(), [](char left, char right) {
|
||||
return std::tolower(static_cast<unsigned char>(left)) == std::tolower(static_cast<unsigned char>(right));
|
||||
}) == name.end()) continue;
|
||||
const int rank = request.empty() ? rankDeviceType(candidate.deviceType) : 4;
|
||||
if (rank <= selectedRank) continue;
|
||||
selected = index;
|
||||
selectedRank = rank;
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
void deviceSelectionTests() {
|
||||
const auto properties = [](const char* name, VkPhysicalDeviceType type) {
|
||||
VkPhysicalDeviceProperties result{};
|
||||
result.apiVersion = VK_API_VERSION_1_1;
|
||||
result.deviceType = type;
|
||||
std::memcpy(result.deviceName, name, std::strlen(name) + 1u);
|
||||
return result;
|
||||
};
|
||||
std::array devices{
|
||||
properties("AMD Radeon Graphics", VK_PHYSICAL_DEVICE_TYPE_INTEGRATED_GPU),
|
||||
properties("NVIDIA GeForce RTX 5080", VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU),
|
||||
properties("Virtual GPU", VK_PHYSICAL_DEVICE_TYPE_VIRTUAL_GPU),
|
||||
properties("CPU", VK_PHYSICAL_DEVICE_TYPE_CPU),
|
||||
};
|
||||
Require(!selectDevice({}, {}).has_value(), "an empty device list selected a device");
|
||||
Require(selectDevice(devices, {}) == 1u, "default selection did not prefer the discrete GPU");
|
||||
Require(selectDevice(devices, "AMD") == 0u, "the AMD request did not select the integrated GPU");
|
||||
Require(selectDevice(devices, "NVIDIA") == 1u, "the NVIDIA request did not select the discrete GPU");
|
||||
Require(selectDevice(devices, "aMd rAdEoN") == 0u, "a requested integrated GPU lost to an unrelated discrete GPU");
|
||||
Require(selectDevice(devices, "rtx 5080") == 1u, "a partial GPU name did not match without regard to case");
|
||||
Require(selectDevice(devices, "Virtual") == 2u, "a requested virtual GPU was not selected");
|
||||
Require(selectDevice(devices, "cpu") == 3u, "a requested CPU device was not selected");
|
||||
Require(!selectDevice(devices, "missing").has_value(), "an unmatched request selected another GPU");
|
||||
Require(!selectDevice(devices, "NVIDIA GeForce RTX 5080 with a longer name").has_value(), "a request longer than the device name matched");
|
||||
Require(selectDevice(std::span(devices).subspan(2), {}) == 0u, "default selection did not prefer the virtual GPU to the CPU");
|
||||
devices[1].apiVersion = VK_API_VERSION_1_0;
|
||||
Require(selectDevice(devices, {}) == 0u, "default selection did not prefer the integrated GPU to the virtual GPU");
|
||||
Require(!selectDevice(devices, "NVIDIA").has_value(), "a requested device without Vulkan 1.1 was selected");
|
||||
devices[1] = devices[0];
|
||||
Require(selectDevice(devices, {}) == 0u && selectDevice(devices, "AMD") == 0u, "equally ranked matches did not retain enumeration order");
|
||||
for (auto& device : devices) device.apiVersion = VK_API_VERSION_1_0;
|
||||
Require(!selectDevice(devices, {}).has_value(), "a list without Vulkan 1.1 selected a device");
|
||||
std::puts("sampled-cache device selection CPU tests passed");
|
||||
}
|
||||
|
||||
class Device {
|
||||
public:
|
||||
explicit Device(bool enableStorageImageMultisample = true) {
|
||||
@@ -71,7 +137,13 @@ public:
|
||||
Require(count != 0, "no Vulkan device");
|
||||
std::vector<VkPhysicalDevice> devices(count);
|
||||
Check(enumerate(instance, &count, devices.data()), "vkEnumeratePhysicalDevices");
|
||||
context.physical = devices.front();
|
||||
const auto physicalProperties = function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties");
|
||||
std::vector<VkPhysicalDeviceProperties> deviceProperties(devices.size());
|
||||
for (std::size_t index = 0; index < devices.size(); ++index) physicalProperties(devices[index], &deviceProperties[index]);
|
||||
const char* requestedName = std::getenv("ANYPS5_GPU");
|
||||
const auto selectedIndex = selectDevice(deviceProperties, requestedName != nullptr ? requestedName : "");
|
||||
Require(selectedIndex.has_value(), requestedName != nullptr && *requestedName != '\0' ? std::string("no Vulkan 1.1 device matching ANYPS5_GPU=\"") + requestedName + "\"" : "no Vulkan 1.1 device");
|
||||
context.physical = devices[*selectedIndex];
|
||||
const auto extensions = function<PFN_vkEnumerateDeviceExtensionProperties>("vkEnumerateDeviceExtensionProperties");
|
||||
Check(extensions(context.physical, nullptr, &count, nullptr), "vkEnumerateDeviceExtensionProperties");
|
||||
std::vector<VkExtensionProperties> available(count);
|
||||
@@ -122,6 +194,7 @@ public:
|
||||
function<PFN_vkGetPhysicalDeviceMemoryProperties>("vkGetPhysicalDeviceMemoryProperties")(context.physical, &context.memory);
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties);
|
||||
std::printf("sampled-cache fixture Vulkan device: %s vendor=0x%04x device=0x%04x\n", properties.deviceName, properties.vendorID, properties.deviceID);
|
||||
context.limits = properties.limits;
|
||||
context.bufferDeviceAddress = true;
|
||||
context.formatProperties = function<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties");
|
||||
@@ -141,6 +214,14 @@ public:
|
||||
~Device() { release(); }
|
||||
const Context& GetContext() const { return context; }
|
||||
|
||||
void VerifySelection() const {
|
||||
VkPhysicalDeviceProperties properties{};
|
||||
function<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(context.physical, &properties);
|
||||
const char* request = std::getenv("ANYPS5_GPU");
|
||||
Require(selectDevice(std::span(&properties, 1), request != nullptr ? request : "").has_value(), "the sampled-cache fixture physical device does not match ANYPS5_GPU");
|
||||
std::puts("sampled-cache fixture physical device selection verified");
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename TFunction>
|
||||
TFunction function(const char* name) const {
|
||||
@@ -563,10 +644,10 @@ void frameTests(const Context& context) {
|
||||
hits += again == made[surface].lock();
|
||||
}
|
||||
Require(hits == capacity, "the next frame over the same " + std::to_string(frame) + " textures found " + std::to_string(hits) + " of them cached, not the " + std::to_string(capacity) + " the cache kept");
|
||||
auto admitted = Sampled(context, block.Surface(frame - 1u));
|
||||
Require(Sampled(context, block.Surface(frame - 1u)) == admitted, "a texture used a second time in the frame was not cached");
|
||||
Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture admitted on its second use left the cache over its budget");
|
||||
admitted.reset();
|
||||
auto refused = Sampled(context, block.Surface(frame - 1u));
|
||||
Require(Sampled(context, block.Surface(frame - 1u)) != refused, "a texture used a second time in the frame was cached in place of one of the frame's textures");
|
||||
Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture used a second time left the cache over its budget");
|
||||
refused.reset();
|
||||
Recorder::CountPresent();
|
||||
std::vector<std::weak_ptr<Texture>> moved(frame);
|
||||
for (std::size_t surface = 0; surface < frame; ++surface) moved[surface] = Sampled(context, block.Surface(frame + surface));
|
||||
@@ -578,6 +659,71 @@ void frameTests(const Context& context) {
|
||||
Require(TextureCacheUsage().sampledEntries == capacity, "a second frame over other textures left " + std::to_string(TextureCacheUsage().sampledEntries) + " entries cached");
|
||||
}
|
||||
|
||||
void scanTests(const Context& context) {
|
||||
Block block(context);
|
||||
ClearCachedTextures(context.device);
|
||||
Recorder::CountPresent();
|
||||
const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes();
|
||||
const auto capacity = static_cast<std::size_t>(Budget / bytes);
|
||||
const auto working = 2u * capacity;
|
||||
Require(capacity >= 2u && working <= Surfaces && TextureCacheUsage().sampledEntries == 1u, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 1 MiB budget does not fit as planned");
|
||||
constexpr std::size_t Presents = 6;
|
||||
constexpr std::size_t Uses = 2;
|
||||
std::vector<std::weak_ptr<Texture>> last(working);
|
||||
std::size_t hits = 0;
|
||||
std::size_t lookups = 0;
|
||||
for (std::size_t present = 0; present < Presents; ++present) {
|
||||
Recorder::CountPresent();
|
||||
for (std::size_t surface = 0; surface < working; ++surface) {
|
||||
for (std::size_t use = 0; use < Uses; ++use) {
|
||||
const auto texture = Sampled(context, block.Surface(surface));
|
||||
if (present != 0u) {
|
||||
hits += texture == last[surface].lock();
|
||||
++lookups;
|
||||
}
|
||||
last[surface] = texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
Require(TextureCacheUsage().sampledBytes <= Budget, "the sampled texture cache holds more than its budget after a scan");
|
||||
const auto wanted = (Presents - 1u) * Uses * capacity / 2u;
|
||||
Require(hits >= wanted, "a cyclic scan using each of " + std::to_string(working) + " textures twice in a row, twice the " + std::to_string(capacity) + " the budget holds, found " + std::to_string(hits) + " of " + std::to_string(lookups) + " lookups cached, fewer than " + std::to_string(wanted));
|
||||
}
|
||||
|
||||
void shrinkTests(const Context& context) {
|
||||
Block block(context);
|
||||
ClearCachedTextures(context.device);
|
||||
Recorder::CountPresent();
|
||||
OverrideCacheBudgetForTest(2u * Budget);
|
||||
const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes();
|
||||
const auto capacity = static_cast<std::size_t>((2u * Budget) / bytes);
|
||||
Require(capacity >= 4u && capacity + 1u <= Surfaces, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 2 MiB budget does not fit as planned");
|
||||
for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface));
|
||||
Require(TextureCacheUsage().sampledEntries == capacity, "the 2 MiB budget did not keep " + std::to_string(capacity) + " snapshots");
|
||||
Recorder::CountPresent();
|
||||
OverrideCacheBudgetForTest(Budget);
|
||||
for (std::size_t surface = 0; surface < capacity; ++surface) Sampled(context, block.Surface(surface));
|
||||
Sampled(context, block.Surface(capacity));
|
||||
Require(TextureCacheUsage().sampledBytes <= Ceiling, "after the budget fell to 1 MiB, one insert left " + std::to_string(TextureCacheUsage().sampledBytes) + " bytes cached with every entry in use this frame, past a quarter over the budget");
|
||||
OverrideCacheBudgetForTest(0);
|
||||
}
|
||||
|
||||
void secondUseTests(const Context& context) {
|
||||
Block block(context);
|
||||
ClearCachedTextures(context.device);
|
||||
Recorder::CountPresent();
|
||||
const auto bytes = Sampled(context, block.Surface(0))->AllocationBytes();
|
||||
const auto capacity = static_cast<std::size_t>(Budget / bytes);
|
||||
Require(capacity >= 2u && capacity < Surfaces && TextureCacheUsage().sampledEntries == 1u, "a snapshot of the 256 KiB surface holds " + std::to_string(bytes) + " bytes, which the test's 1 MiB budget does not fit as planned");
|
||||
for (std::size_t surface = 1; surface < capacity; ++surface) Sampled(context, block.Surface(surface));
|
||||
Recorder::CountPresent();
|
||||
auto first = Sampled(context, block.Surface(capacity));
|
||||
Require(TextureCacheUsage().sampledEntries == capacity && first.use_count() == 1, "the first use in a frame of a texture the cache does not hold was cached in place of one the last frame used");
|
||||
const auto second = Sampled(context, block.Surface(capacity));
|
||||
Require(Sampled(context, block.Surface(capacity)) == second, "a texture used a second time in a frame whose least recently used entry is from the last frame was not cached");
|
||||
Require(TextureCacheUsage().sampledEntries == capacity && TextureCacheUsage().sampledBytes <= Budget, "the texture admitted on its second use left the cache over its budget");
|
||||
}
|
||||
|
||||
void earlyMissTests(const Context& context) {
|
||||
Block block(context);
|
||||
ClearCachedTextures(context.device);
|
||||
@@ -718,6 +864,10 @@ int main(int argc, char** argv) {
|
||||
try {
|
||||
const bool memory = argc > 1 && std::string(argv[1]) == "memory";
|
||||
const std::string mode = argc > 1 ? argv[1] : "";
|
||||
if (mode == "device-selection-cpu") {
|
||||
deviceSelectionTests();
|
||||
return 0;
|
||||
}
|
||||
const bool resources = mode == "resource-admission" || mode == "resource-oversized" || mode == "resource-clear";
|
||||
const bool disableStorageImageMultisample = mode == "without-storage-image-multisample";
|
||||
#ifdef _WIN32
|
||||
@@ -729,10 +879,15 @@ int main(int argc, char** argv) {
|
||||
try {
|
||||
device = std::make_unique<Device>(!disableStorageImageMultisample);
|
||||
} catch (const std::exception& error) {
|
||||
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr) throw;
|
||||
const char* requestedName = std::getenv("ANYPS5_GPU");
|
||||
if (std::getenv("ANYPS5_REQUIRE_VULKAN") != nullptr || (requestedName != nullptr && *requestedName != '\0')) throw;
|
||||
std::printf("skipped, no usable Vulkan device: %s\n", error.what());
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
if (mode == "device-selection") {
|
||||
device->VerifySelection();
|
||||
return 0;
|
||||
}
|
||||
auto context = device->GetContext();
|
||||
if (disableStorageImageMultisample) {
|
||||
PlaceholderImages placeholders(context);
|
||||
@@ -774,7 +929,10 @@ int main(int argc, char** argv) {
|
||||
rescanTests(context);
|
||||
viewEvictionTests(context);
|
||||
viewTests(context);
|
||||
scanTests(context);
|
||||
frameTests(context);
|
||||
shrinkTests(context);
|
||||
secondUseTests(context);
|
||||
earlyMissTests(context);
|
||||
collidingKeyTests(context);
|
||||
ClearCachedTextures(context.device);
|
||||
|
||||
@@ -274,6 +274,22 @@ void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, s
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> Scheduled(std::span<const std::uint32_t> original) {
|
||||
std::vector<std::uint32_t> code(original.begin(), original.end());
|
||||
code.insert(code.begin() + 25, 0x7e3c0280u);
|
||||
code.insert(code.begin() + 24, 0x7e3e0280u);
|
||||
const auto relocated = [](std::int32_t word) { return word + (word >= 24 ? 1 : 0) + (word >= 25 ? 1 : 0); };
|
||||
for (std::int32_t word = 0; word < static_cast<std::int32_t>(original.size()); ++word) {
|
||||
const auto instruction = original[static_cast<std::size_t>(word)];
|
||||
const auto opcode = instruction & 0xffff0000u;
|
||||
if (opcode != 0xbf820000u && opcode != 0xbf850000u) continue;
|
||||
const auto target = word + 1 + static_cast<std::int16_t>(instruction & 0xffffu);
|
||||
const auto offset = relocated(target) - relocated(word) - 1;
|
||||
code[static_cast<std::size_t>(relocated(word))] = opcode | (static_cast<std::uint32_t>(offset) & 0xffffu);
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
void CheckReversedSubtractRefused(AgcDriver::VulkanDevice& device) {
|
||||
std::string refusal;
|
||||
try {
|
||||
@@ -306,6 +322,14 @@ int main() {
|
||||
Check(Expected64);
|
||||
Run(*device, Wave64Code, 64, device->ComputeTarget(32));
|
||||
Check(Expected64);
|
||||
const auto scheduled32 = Scheduled(Wave32Code);
|
||||
const auto scheduled64 = Scheduled(Wave64Code);
|
||||
Run(*device, scheduled32, 32, device->ComputeTarget(32));
|
||||
Check(Expected32);
|
||||
Run(*device, scheduled64, 64, device->Target());
|
||||
Check(Expected64);
|
||||
Run(*device, scheduled64, 64, device->ComputeTarget(32));
|
||||
Check(Expected64);
|
||||
CheckReversedSubtractRefused(*device);
|
||||
std::puts("setpc tests passed");
|
||||
return 0;
|
||||
|
||||
@@ -321,9 +321,10 @@ int main() {
|
||||
}
|
||||
Require(refused, "unsafe primitive routing was not refused");
|
||||
}
|
||||
bool unknownRefused = false;
|
||||
try { Draw(*device, ColumnPixelCode, exported, replace()); } catch (const std::exception& error) { unknownRefused = std::string(error.what()).find("proven set") != std::string::npos; }
|
||||
Require(unknownRefused, "unbounded stencil reference was not refused");
|
||||
Draw(*device, ColumnPixelCode, exported, replace());
|
||||
Check(all, "unbounded stencil export");
|
||||
Draw(*device, PlainPixelCode, plain, equal(5u));
|
||||
Check([](std::uint32_t x, std::uint32_t) { return x == 5u; }, "unbounded stencil export replaces each column's own value");
|
||||
bool earlyRefused = false;
|
||||
try { Draw(*device, alternating, 0x1012u, replace()); } catch (const std::exception& error) { earlyRefused = std::string(error.what()).find("late tests") != std::string::npos; }
|
||||
Require(earlyRefused, "early stencil testing was not refused");
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "../SubvectorLoopFixture.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
@@ -17,6 +18,7 @@ namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
using SubvectorLoopFixture::ExecKeptCode;
|
||||
|
||||
constexpr std::uint32_t Threads = 64;
|
||||
constexpr std::uint32_t Columns = 9;
|
||||
@@ -25,16 +27,6 @@ constexpr std::uint32_t Marker = 0x5a5a5a5au;
|
||||
alignas(256) std::array<std::uint32_t, Threads> Input{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * Columns> Output{};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 50> ExecResetCode{
|
||||
0x34020082, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x7e140280, 0x7e160280, 0x7e180280, 0x7e1a0280,
|
||||
0x7e1c0280, 0xbe940380, 0xbea603ff, 0x5a5a5a5a, 0x7daa0880, 0xbda6000b, 0x80148114, 0x4a141481,
|
||||
|
||||
@@ -42,4 +42,29 @@ int APS5_VABI sceAjmDecWVorbisCreateSetupPacket(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmBatchGetLog() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmInstanceAvailable() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t APS5_VABI sceAjmInstanceCodecType(uint32_t instance_id) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmInstanceExtend(uint32_t context, uint32_t codec, uint64_t flags, uint32_t instance) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmInstanceSwitch(uint32_t context, uint32_t codec, uint64_t flags, uint32_t instance) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,6 @@ extern "C" {
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -187,7 +186,7 @@ std::atomic<std::uint32_t> g_nextContext{1};
|
||||
std::atomic<std::uint32_t> g_nextInstance{1};
|
||||
std::atomic<std::uint32_t> g_nextBatch{1};
|
||||
std::mutex& g_batchLock = *new std::mutex();
|
||||
std::set<std::uint32_t>& g_batches = *new std::set<std::uint32_t>();
|
||||
std::map<std::uint32_t, std::uint32_t>& g_batches = *new std::map<std::uint32_t, std::uint32_t>();
|
||||
|
||||
enum class JobKind : std::uint32_t {
|
||||
Initialize = 1,
|
||||
@@ -1833,7 +1832,7 @@ int APS5_VABI sceAjmBatchJobGetStatistics(AjmBatchInfo* info, float interval, vo
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int priority, AjmBatchError* error, uint32_t* batch) {
|
||||
(void)context;
|
||||
if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT;
|
||||
(void)priority;
|
||||
if (!info || !batch) return SCE_AJM_ERROR_INVALID_PARAMETER;
|
||||
AJM_TRACE("[ajm] batch start: context %u, priority %d, %llu of %llu buffer bytes used\n", context, priority, static_cast<unsigned long long>(info->offset), static_cast<unsigned long long>(info->size));
|
||||
@@ -1863,18 +1862,20 @@ int APS5_VABI sceAjmBatchStart(uint32_t context, const AjmBatchInfo* info, int p
|
||||
const auto id = g_nextBatch.fetch_add(1, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard lock(g_batchLock);
|
||||
g_batches.insert(id);
|
||||
g_batches.emplace(id, context);
|
||||
}
|
||||
*batch = id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout, AjmBatchError* error) {
|
||||
(void)context;
|
||||
if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT;
|
||||
(void)timeout;
|
||||
{
|
||||
std::lock_guard lock(g_batchLock);
|
||||
if (g_batches.erase(batch) == 0) return SCE_AJM_ERROR_INVALID_BATCH;
|
||||
const auto found = g_batches.find(batch);
|
||||
if (found == g_batches.end() || found->second != context) return SCE_AJM_ERROR_INVALID_BATCH;
|
||||
g_batches.erase(found);
|
||||
}
|
||||
if (error) std::memset(error, 0, sizeof(*error));
|
||||
return 0;
|
||||
@@ -1883,7 +1884,8 @@ int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout
|
||||
int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) {
|
||||
if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT;
|
||||
std::lock_guard lock(g_batchLock);
|
||||
return g_batches.contains(batch) ? 0 : SCE_AJM_ERROR_INVALID_BATCH;
|
||||
const auto found = g_batches.find(batch);
|
||||
return found != g_batches.end() && found->second == context ? 0 : SCE_AJM_ERROR_INVALID_BATCH;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) {
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
|
||||
APS5_DUMMY_FUN
|
||||
int APS5_VABI sceAppCheckerAbort() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAppCheckerExecute() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAppCheckerExecuteEx() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -96,7 +96,7 @@ int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) {
|
||||
|
||||
int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_point) {
|
||||
if (!mount_point) return SCE_APP_CONTENT_ERROR_PARAMETER;
|
||||
if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX;
|
||||
if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED;
|
||||
const auto directory = ResolvePath_nid_no_patch(DOWNLOAD_MOUNT_POINT);
|
||||
if (std::filesystem::create_directories(directory)) RecordWrittenPath_nid_no_patch(directory);
|
||||
ClearDirectory(directory);
|
||||
@@ -104,8 +104,8 @@ int APS5_VABI sceAppContentDownloadDataFormat(const AppContentMountPoint* mount_
|
||||
}
|
||||
|
||||
int APS5_VABI sceAppContentDownloadDataGetAvailableSpaceKb(const AppContentMountPoint* mount_point, size_t* available_space_kb) {
|
||||
if (!available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER;
|
||||
if (!mount_point || std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX;
|
||||
if (!mount_point || !available_space_kb) return SCE_APP_CONTENT_ERROR_PARAMETER;
|
||||
if (std::strncmp(mount_point->data, DOWNLOAD_MOUNT_POINT, sizeof(mount_point->data)) != 0) return SCE_APP_CONTENT_ERROR_NOT_MOUNTED;
|
||||
const std::uint64_t quotaKb = GetAppDownloadDataSizeMiB_nid_postfix() * 1024u;
|
||||
if (quotaKb == 0) {
|
||||
*available_space_kb = 0;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -196,16 +197,15 @@ int32_t APS5_VABI sceAudioInIsPadEmbeddedMic(int32_t handle) {
|
||||
}
|
||||
|
||||
int32_t APS5_VABI sceAudioInGetGain(int32_t handle, float* gain) {
|
||||
(void)handle;
|
||||
(void)gain;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
if (!gain) throw std::runtime_error("sceAudioInGetGain: null gain");
|
||||
*gain = 1.0f;
|
||||
std::lock_guard lock(g_mutex);
|
||||
return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
int32_t APS5_VABI sceAudioInGetRerouteCount(int32_t handle) {
|
||||
(void)handle;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
std::lock_guard lock(g_mutex);
|
||||
return find(handle) ? 0 : AUDIO_IN_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,10 +1,28 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
|
||||
APS5_DUMMY_FUN
|
||||
int APS5_VABI sceAudioLatencyEstimationAddData() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioLatencyEstimationGetDelayTime() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioLatencyEstimationInitialize() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioLatencyEstimationRelease() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ add_library(libSceAudioOut SHARED EXCLUDE_FROM_ALL
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2User.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2Mastering.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/AudioOut2System.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Unimplemented.cpp
|
||||
)
|
||||
|
||||
target_include_directories(libSceAudioOut PRIVATE
|
||||
|
||||
@@ -0,0 +1,488 @@
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAudioOut2UserCreateEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoPortDestroy(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoContextDestroy(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutOpenEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoPortSetAttributes(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoContextAdvance(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoContextPush(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoContextGetQueueLevel(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetSystemDebugState(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoPortGetState(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutGetHandleStatusInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo2(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutChangeAppModuleState(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutGetSimulatedHandleStatusInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutGetSimulatedBusUsableStatusByBusType(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutAttachToApplicationByPid(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutDetachFromApplicationByPid(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutPtOpen(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutPtClose(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutPtGetLastOutputTime(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutExPtOpen(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutExPtClose(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutExPtGetLastOutputTime(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutExGetLastOutputTime(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutGetInfoOpenNum(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutGetInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutMasteringInit(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutMasteringSetParam(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutMasteringGetState(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutMasteringTerm(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetPadSpkVolume(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetPadJackVolume(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetUsbVolume(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetCaesarVolume(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetHeadphoneOutMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutGetFocusEnablePid(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetMainOutput(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetVolumeDown(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetChatBalance(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetRecMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetAllMute(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSetSparkEQ(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutArbitrationInitialize(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2ArbitrationInitialize(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutEnable3DAudioByArbitration(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutAssignHRTF(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutEnable3DAudioForUser(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutEnableMonoHpOutput(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutEnableTVVirtual(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutEnableTVCorrection(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutEnableVrControllerLowPowerMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutEnableCinematicVrRotation(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutCustomHrtfIsExist(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutExConfigureOutputMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutExSystemInfoIsSupportedAudioOutExMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutExGetSystemInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutExGetOutputInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSysConfigureOutputMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSysGetMonitorInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSysHdmiMonitorInfoIsSupportedAudioOutMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSysGetSystemInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOutSysGetOutputInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoContextQueryMemory(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoContextCreate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoContextSetAttributes(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusInit(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusSetPortConnections(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusSetConnections(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusSetPortStatuses(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusSetHeadphoneOutMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusUserLoginFor3DAudio(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusSetDeviceConnections(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusSetMorpheusParam(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusSetCaesarParam(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MbusSetSparkParam(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2AllAudioMainToHeadphoneForDiag(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoPortCreate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoPortGetStateEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2ReportRegisterHandler(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2ReportUnregisterHandler(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2GetTvCorrectionInfo(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2GetIpcHandle(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2GetSystemStateEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2MasteringInitEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2SetDebugHeadphoneMode(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2DebugStateCtrl(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoInit(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2LoTerminate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2ContextBedWrite(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2PortCreateEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2InitializeEx(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2Terminate(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2PortRegister(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2PortUnregister(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2GetAmbisonicsEncodeCoefficients(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAudioOut2GetHrtfIdForCronos(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,4 +18,19 @@ int APS5_VABI sceAutoMounterClientInit() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAutoMounterClientRegisterCallback() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAutoMounterClientTerm() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAutoMounterClientUnregisterCallback() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
Loaded 100 of 489 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user