Merge remote-tracking branch 'origin/main' into fix/windows-runtime-module-start

This commit is contained in:
Savvanis Spyros committed 2026-10-20 15:27:06 +03:00
commit 079e6ca316
489 files changed
+21305 -1705

No files matched your search

+3 -13
View File
@@ -9,6 +9,7 @@ on:
- core/relinker/**
- demo/web/**
- tools/build_web.py
- tools/package_release.py
- tools/nid_names.py
- tools/progress.py
- tools/progress_history.py
@@ -42,21 +43,10 @@ jobs:
with:
ref: main
fetch-depth: 0
- name: Build web converter
shell: bash
run: |
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk"
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.3
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm --target relinker_web --parallel 2
- name: Test web converter
run: python3 tools/test_web.py --build build-wasm
- name: Download latest release libraries
- name: Download matching release converter and libraries
env:
GITHUB_TOKEN: ${{ github.token }}
run: python3 tools/build_web.py --build build-wasm --libraries
run: python3 tools/build_web.py --build build-wasm --release
- run: python3 tools/progress.py build-wasm/web
- run: python3 tools/progress_history.py build-wasm/web
- uses: actions/upload-pages-artifact@v5
+1 -1
View File
@@ -16,7 +16,7 @@ concurrency:
jobs:
labels:
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 30
permissions:
contents: read
pull-requests: write
+35 -3
View File
@@ -41,6 +41,7 @@ jobs:
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
submodules: recursive
- name: Point the WinLibs CMake at the CA bundle of Git for Windows
if: matrix.platform == 'windows'
@@ -86,7 +87,7 @@ jobs:
run: cmake --build build
- name: Run tests (Linux)
if: matrix.platform == 'linux'
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error
run: ctest --test-dir build --output-on-failure --timeout 120 --stop-on-failure --no-tests=error -E '^precise_wait$'
env:
ANYPS5_REQUIRE_VULKAN: 1
GALLIUM_OVERRIDE_CPU_CAPS: avx
@@ -110,9 +111,38 @@ jobs:
if-no-files-found: error
compression-level: 0
web:
name: Build and test web converter
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
- name: Build web converter
shell: bash
run: |
git clone --depth 1 https://github.com/emscripten-core/emsdk.git "$RUNNER_TEMP/emsdk"
"$RUNNER_TEMP/emsdk/emsdk" install 6.0.3
"$RUNNER_TEMP/emsdk/emsdk" activate 6.0.3
source "$RUNNER_TEMP/emsdk/emsdk_env.sh"
emcmake cmake -S . -B build-wasm -DANYPS5_BUILD_WASM=ON -DBUILD_TESTING=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm --target relinker_web --parallel 2
- name: Test web converter
run: python3 tools/test_web.py --build build-wasm
- name: Package web converter
shell: bash
run: python3 tools/package_release.py --platform web --build build-wasm --output dist --version "$RELEASE_TAG" --revision "$(git rev-parse HEAD)"
- uses: actions/upload-artifact@v7
with:
name: release-web
path: dist/*
if-no-files-found: error
compression-level: 0
publish:
name: Publish release
needs: build
needs: [build, web]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
@@ -122,6 +152,8 @@ jobs:
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.sha }}
- uses: actions/download-artifact@v8
with:
pattern: release-*
@@ -130,7 +162,7 @@ jobs:
- name: Check release files and collect documentation
shell: bash
run: |
for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG"; do
for file in "prx-windows-$RELEASE_TAG.zip" "prx-windows-$RELEASE_TAG.tar.gz" "prx-linux-$RELEASE_TAG.zip" "prx-linux-$RELEASE_TAG.tar.gz" "relinker-$RELEASE_TAG.exe" "relinker-$RELEASE_TAG" "web-$RELEASE_TAG.zip"; do
test -s "dist/$file"
done
python tools/package_release.py --docs docs/user --output dist
+17 -1
View File
@@ -44,6 +44,9 @@ if(BUILD_TESTING)
get_filename_component(testCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY)
foreach(runtimeName IN ITEMS libgcc_s_seh-1.dll libstdc++-6.dll libwinpthread-1.dll)
if(NOT EXISTS "${testCompilerDirectory}/${runtimeName}")
if(ANYPS5_RELINKER_ONLY)
continue()
endif()
message(FATAL_ERROR "Cannot locate MinGW runtime: ${runtimeName}")
endif()
file(COPY "${testCompilerDirectory}/${runtimeName}" DESTINATION "${ANYPS5_TEST_OUTPUT_DIRECTORY}")
@@ -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
+44 -6
View File
@@ -2,6 +2,7 @@
#include <climits>
#include <cstddef>
#include <memory>
#include <stdexcept>
#define STB_IMAGE_STATIC
@@ -19,8 +20,8 @@ constexpr std::uint32_t MAX_DIMENSION = 0xFFFF;
class Handle {
public:
Handle() : handle(tj3Init(TJINIT_COMPRESS)) {
if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed");
explicit Handle(int type) : handle(tj3Init(type)) {
if (!handle) throw std::runtime_error(type == TJINIT_COMPRESS ? "Jpeg::Encode: encoder initialization failed" : "Jpeg::DecodeScaled: decoder initialization failed");
}
~Handle() { tj3Destroy(handle); }
Handle(const Handle&) = delete;
@@ -63,7 +64,7 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
throw std::invalid_argument("Jpeg::Encode: invalid restart interval");
}
const int subsampling = toSubsampling(sampling);
Handle handle;
Handle handle(TJINIT_COMPRESS);
if (tj3Set(handle, TJPARAM_QUALITY, quality) < 0 || tj3Set(handle, TJPARAM_SUBSAMP, channels == 1 ? TJSAMP_GRAY : subsampling) < 0) {
throw std::runtime_error("Jpeg::Encode: encoder configuration failed");
}
@@ -80,6 +81,43 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
return {buffer.data, buffer.data + size};
}
std::uint32_t ScaledDimension(std::uint32_t dimension, std::uint32_t denominator) {
if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) {
throw std::invalid_argument("Jpeg::ScaledDimension: the scale must be 1/1, 1/2, 1/4 or 1/8");
}
const tjscalingfactor factor{1, static_cast<int>(denominator)};
return static_cast<std::uint32_t>(TJSCALED(static_cast<std::uint64_t>(dimension), factor));
}
std::optional<Image> DecodeScaled(std::span<const std::uint8_t> jpeg, std::uint32_t denominator) {
if (denominator != 1 && denominator != 2 && denominator != 4 && denominator != 8) {
throw std::invalid_argument("Jpeg::DecodeScaled: the scale must be 1/1, 1/2, 1/4 or 1/8");
}
if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt;
Handle handle(TJINIT_DECOMPRESS);
if (tj3Set(handle, TJPARAM_STOPONWARNING, 1) < 0) throw std::runtime_error("Jpeg::DecodeScaled: decoder configuration failed");
if (tj3DecompressHeader(handle, jpeg.data(), jpeg.size()) < 0) return std::nullopt;
const int colorspace = tj3Get(handle, TJPARAM_COLORSPACE);
if (colorspace == TJCS_CMYK || colorspace == TJCS_YCCK) {
throw std::runtime_error("Jpeg::DecodeScaled: scaled decoding of a CMYK or YCCK image is not implemented");
}
const int width = tj3Get(handle, TJPARAM_JPEGWIDTH);
const int height = tj3Get(handle, TJPARAM_JPEGHEIGHT);
if (width <= 0 || height <= 0) return std::nullopt;
if (tj3SetScalingFactor(handle, tjscalingfactor{1, static_cast<int>(denominator)}) < 0) {
throw std::runtime_error("Jpeg::DecodeScaled: the decoder refused the scaling factor");
}
const std::uint32_t channels = colorspace == TJCS_GRAY ? 1 : 3;
Image image{ScaledDimension(static_cast<std::uint32_t>(width), denominator), ScaledDimension(static_cast<std::uint32_t>(height), denominator), channels, {}};
image.pixels.resize(static_cast<std::size_t>(image.width) * image.height * channels);
if (tj3Decompress8(handle, jpeg.data(), jpeg.size(), image.pixels.data(), 0, channels == 1 ? TJPF_GRAY : TJPF_RGB) < 0) {
return std::nullopt;
}
return image;
}
std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) {
if (jpeg.empty() || jpeg.size() > INT_MAX) return std::nullopt;
@@ -97,12 +135,12 @@ std::optional<Image> Decode(std::span<const std::uint8_t> jpeg) {
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* decoded = stbi_load_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels, 0);
const std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> decoded(
stbi_load_from_memory(jpeg.data(), static_cast<int>(jpeg.size()), &width, &height, &channels, 0), stbi_image_free);
if (!decoded) return std::nullopt;
Image image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), static_cast<std::uint32_t>(channels), {}};
image.pixels.assign(decoded, decoded + static_cast<std::size_t>(image.width) * image.height * image.channels);
stbi_image_free(decoded);
image.pixels.assign(decoded.get(), decoded.get() + static_cast<std::size_t>(image.width) * image.height * image.channels);
return image;
}
+68
View File
@@ -1,5 +1,7 @@
#include "Decoder/Jpeg.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <stdexcept>
@@ -108,5 +110,71 @@ int main() {
const std::vector<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8};
Require(!Decoder::Jpeg::Decode(garbage).has_value());
Require(Decoder::Jpeg::ScaledDimension(7, 1) == 7 && Decoder::Jpeg::ScaledDimension(7, 2) == 4);
Require(Decoder::Jpeg::ScaledDimension(7, 4) == 2 && Decoder::Jpeg::ScaledDimension(7, 8) == 1);
Require(Decoder::Jpeg::ScaledDimension(32, 8) == 4 && Decoder::Jpeg::ScaledDimension(33, 8) == 5 && Decoder::Jpeg::ScaledDimension(1, 8) == 1);
Require(Decoder::Jpeg::ScaledDimension(0xFFFF, 2) == 0x8000);
for (const std::uint32_t denominator : {0u, 3u, 16u}) {
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::ScaledDimension(7, denominator); }));
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator); }));
}
const auto boxError = [](const std::vector<std::uint8_t>& source, std::uint32_t sourceWidth, std::uint32_t sourceHeight,
std::uint32_t channels, const Decoder::Jpeg::Image& scaled, std::uint32_t denominator) {
long error = 0;
for (std::uint32_t y = 0; y < scaled.height; ++y) {
for (std::uint32_t x = 0; x < scaled.width; ++x) {
for (std::uint32_t c = 0; c < channels; ++c) {
long sum = 0;
long count = 0;
for (std::uint32_t sy = y * denominator; sy < std::min(sourceHeight, (y + 1) * denominator); ++sy) {
for (std::uint32_t sx = x * denominator; sx < std::min(sourceWidth, (x + 1) * denominator); ++sx) {
sum += source[(static_cast<std::size_t>(sy) * sourceWidth + sx) * channels + c];
++count;
}
}
const auto average = static_cast<std::uint8_t>((sum + count / 2) / count);
error += Difference(average, scaled.pixels[(static_cast<std::size_t>(y) * scaled.width + x) * channels + c]);
}
}
}
return error / static_cast<long>(static_cast<std::size_t>(scaled.width) * scaled.height * channels);
};
for (const std::uint32_t denominator : {1u, 2u, 4u, 8u}) {
const auto scaled = Decoder::Jpeg::DecodeScaled(rgbJpeg, denominator);
Require(scaled.has_value() && scaled->channels == 3);
Require(scaled->width == width / denominator && scaled->height == height / denominator);
Require(scaled->pixels.size() == static_cast<std::size_t>(scaled->width) * scaled->height * 3);
Require(boxError(rgb, width, height, 3, *scaled, denominator) < 4);
const auto grayScaled = Decoder::Jpeg::DecodeScaled(grayJpeg, denominator);
Require(grayScaled.has_value() && grayScaled->channels == 1);
Require(grayScaled->width == width / denominator && grayScaled->height == height / denominator);
Require(boxError(gray, width, height, 1, *grayScaled, denominator) < 4);
}
std::vector<std::uint8_t> odd(31 * 23 * 3);
for (std::uint32_t y = 0; y < 23; ++y) {
for (std::uint32_t x = 0; x < 31; ++x) {
std::uint8_t* pixel = &odd[(y * 31 + x) * 3];
pixel[0] = static_cast<std::uint8_t>(x * 8);
pixel[1] = static_cast<std::uint8_t>(y * 11);
pixel[2] = static_cast<std::uint8_t>(200 - x * 2);
}
}
const auto oddJpeg = Decoder::Jpeg::Encode(odd, 31, 23, 3, 95, Decoder::Jpeg::Sampling::Yuv420);
for (const std::uint32_t denominator : {2u, 4u, 8u}) {
const auto scaled = Decoder::Jpeg::DecodeScaled(oddJpeg, denominator);
Require(scaled.has_value() && scaled->channels == 3);
Require(scaled->width == Decoder::Jpeg::ScaledDimension(31, denominator) && scaled->height == Decoder::Jpeg::ScaledDimension(23, denominator));
Require(boxError(odd, 31, 23, 3, *scaled, denominator) < 8);
}
Require(!Decoder::Jpeg::DecodeScaled({}, 2).has_value());
Require(!Decoder::Jpeg::DecodeScaled(garbage, 2).has_value());
const std::vector<std::uint8_t> truncated(rgbJpeg.begin(), rgbJpeg.begin() + static_cast<std::ptrdiff_t>(rgbJpeg.size() / 2));
Require(!Decoder::Jpeg::DecodeScaled(truncated, 2).has_value());
return 0;
}
+3 -3
View File
@@ -152,12 +152,12 @@ std::optional<Image> Decode(std::span<const std::uint8_t> png) {
int width = 0;
int height = 0;
int channels = 0;
stbi_uc* decoded = stbi_load_from_memory(png.data(), static_cast<int>(png.size()), &width, &height, &channels, 4);
const std::unique_ptr<stbi_uc, decltype(&stbi_image_free)> decoded(
stbi_load_from_memory(png.data(), static_cast<int>(png.size()), &width, &height, &channels, 4), stbi_image_free);
if (!decoded) return std::nullopt;
Image image{static_cast<std::uint32_t>(width), static_cast<std::uint32_t>(height), {}};
image.pixels.assign(decoded, decoded + static_cast<std::size_t>(image.width) * image.height * 4);
stbi_image_free(decoded);
image.pixels.assign(decoded.get(), decoded.get() + static_cast<std::size_t>(image.width) * image.height * 4);
return image;
}
+30
View File
@@ -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)
+2 -2
View File
@@ -1155,8 +1155,8 @@ struct ImeKeycode {
std::uint64_t timestamp;
};
using ImeTextFilter = std::int32_t (*)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
using ImeExtKeyboardFilter = int (*)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
using ImeTextFilter = std::int32_t (APS5_VABI *)(char16_t* out_text, std::uint32_t* out_text_length, const char16_t* source_text, std::uint32_t source_text_length);
using ImeExtKeyboardFilter = int (APS5_VABI *)(const ImeKeycode* source_keycode, std::uint16_t* out_keycode, std::uint32_t* out_status, void* reserved);
struct ImeExtendedParam {
std::uint32_t option;
+57 -17
View File
@@ -1,54 +1,84 @@
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <atomic>
#include <mutex>
#include <unordered_set>
#include <unordered_map>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include <atomic>
// Audio co-processor batches complete immediately without producing output; audio is silent.
static constexpr int SCE_ACM_ERROR_INVALID_PARAMETER = static_cast<int>(0x80E30002);
static std::atomic<uint32_t> g_nextId{1};
namespace {
constexpr int SCE_ACM_ERROR_BAD_CONTEXT = static_cast<int>(0x81940002);
constexpr int SCE_ACM_ERROR_BAD_POINTER = static_cast<int>(0x81940006);
std::mutex g_mutex;
std::unordered_set<AcmContextId> g_contexts;
std::atomic<uint32_t> g_nextId{1};
}
extern "C" {
int APS5_VABI sceAcmBatchStartBuffer(AcmContextId context, const void* batch_commands, size_t batch_size, AcmBatchError* batch_error, AcmBatchId* batch) {
(void)context;
(void)batch_commands;
(void)batch_size;
(void)batch_error;
if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER;
*batch = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!batch) return SCE_ACM_ERROR_BAD_POINTER;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error));
*batch = id;
return 0;
}
int APS5_VABI sceAcmBatchStartBuffers(AcmContextId context, uint32_t batch_info_count, const AcmBatchInfo* const batch_info[], AcmBatchError* batch_error, AcmBatchId* batch) {
(void)context;
(void)batch_info_count;
(void)batch_info;
(void)batch_error;
if (!batch) return SCE_ACM_ERROR_INVALID_PARAMETER;
*batch = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!batch) return SCE_ACM_ERROR_BAD_POINTER;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
AcmBatchId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (batch_error) std::memset(batch_error, 0, sizeof(*batch_error));
*batch = id;
return 0;
}
int APS5_VABI sceAcmBatchWait(AcmContextId context, AcmBatchId batch, uint32_t timeout) {
(void)context;
(void)batch;
(void)timeout;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
if (!g_contexts.contains(context)) return SCE_ACM_ERROR_BAD_CONTEXT;
return 0;
}
int APS5_VABI sceAcmContextCreate(AcmContextId* context) {
if (!context) return SCE_ACM_ERROR_INVALID_PARAMETER;
*context = g_nextId.fetch_add(1, std::memory_order_relaxed);
if (!context) return SCE_ACM_ERROR_BAD_POINTER;
AcmContextId id = g_nextId.fetch_add(1, std::memory_order_relaxed);
while (id == 0) id = g_nextId.fetch_add(1, std::memory_order_relaxed);
{
std::lock_guard<std::mutex> lock(g_mutex);
g_contexts.insert(id);
}
*context = id;
return 0;
}
int APS5_VABI sceAcmContextDestroy(AcmContextId context) {
(void)context;
if (context == 0) return SCE_ACM_ERROR_BAD_CONTEXT;
std::lock_guard<std::mutex> lock(g_mutex);
auto it = g_contexts.find(context);
if (it == g_contexts.end()) return SCE_ACM_ERROR_BAD_CONTEXT;
g_contexts.erase(it);
return 0;
}
// Builds a convolution-reverb command into a batch; batches execute as no-ops, so nothing is encoded.
int APS5_VABI sceAcm_ConvReverb_SharedInput(void) {
return 0;
}
@@ -66,4 +96,14 @@ int APS5_VABI sceAcmBatchJobPriority(void) {
return 0;
}
int APS5_VABI sceAcm_ConvReverb_SharedIr() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAcm_IFFT() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
+1
View File
@@ -428,6 +428,7 @@ configure_windows_unwind(agc_shader_helpers_tests)
if(BUILD_TESTING)
set_target_properties(agc_shader_helpers_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
add_test(NAME agc_shader_helpers COMMAND agc_shader_helpers_tests)
set_tests_properties(agc_shader_helpers PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_shader_helpers PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
@@ -1,7 +1,9 @@
#include "prx/libSceAgc/Command/include/Packet.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <limits>
#include <stdexcept>
#include <string>
@@ -96,8 +98,8 @@ std::uint32_t* WriteRegisterRange(CommandBuffer* buffer, std::uint32_t opcode, s
Require(count != 0 && count <= 0x3fffu && offset <= 0xffffu && count <= 0x10000u - offset, function, "invalid register range");
std::vector<std::uint32_t> snapshot;
if (values != nullptr) {
CheckAddress(reinterpret_cast<std::uintptr_t>(values), 4, function);
snapshot.assign(values, values + count);
snapshot.resize(count);
std::memcpy(snapshot.data(), values, static_cast<std::size_t>(count) * sizeof(std::uint32_t));
}
auto* packet = Allocate(buffer, count + 2u, function);
packet[0] = Header(opcode, count + 2u);
+8 -2
View File
@@ -474,8 +474,14 @@ void testRegisterRange() {
check(packet == storage.words.data() + 6 && storage.buffer.cursor_up == storage.words.data() + 12, "incorrect populated register range allocation");
check(storage.words == expected, "register values were not copied correctly");
const auto* misaligned = reinterpret_cast<const std::uint32_t*>(reinterpret_cast<const unsigned char*>(values.data()) + 1);
expectFailure([&] { sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 4); });
check(storage.words == expected && storage.buffer.cursor_up == storage.words.data() + 12, "misaligned register values modified command buffer");
packet = sceAgcCbSetShRegisterRangeDirect(&storage.buffer, 0x8c, misaligned, 3);
expected[12] = 0xc0037600u;
expected[13] = 0x8c;
expected[14] = 0x02000000u;
expected[15] = 0x03000000u;
expected[16] = 0x04000000u;
check(packet == storage.words.data() + 12 && storage.buffer.cursor_up == storage.words.data() + 17, "incorrect misaligned register range allocation");
check(storage.words == expected, "misaligned register values were not read byte for byte");
}
void testPacketPayloadAddress() {
@@ -8,6 +8,8 @@
extern "C" {
std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer*, ShaderRegister);
std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t);
std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t);
std::uint32_t* APS5_VABI sceAgcCbSetUcRegisterRangeDirect(CommandBuffer*, std::uint32_t, const std::uint32_t*, std::uint32_t);
std::uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer*, ShaderRegister);
std::uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer*, ShaderRegister);
std::uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer*, ShaderRegister);
@@ -92,11 +94,37 @@ void testConfig() {
expectFailure([&] { sceAgcDcbSetCfRegisterDirect(nullptr, {0, 1}); });
expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), 0); });
expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0xffffu, values.data(), 2); });
const auto* misaligned = reinterpret_cast<const std::uint32_t*>(reinterpret_cast<const unsigned char*>(values.data()) + 1);
expectFailure([&] { sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, misaligned, 1); });
check(storage.words == before && storage.buffer.cursor_up == reserved + 4, "invalid config register write modified buffer");
}
void testRangeFromAnyByte() {
const std::array writers{sceAgcCbSetShRegisterRangeDirect, sceAgcCbSetUcRegisterRangeDirect, sceAgcDcbSetCfRegisterRangeDirect};
const std::array headers{0xc0047600u, 0xc0047900u, 0xc0046800u};
alignas(16) std::array<unsigned char, 32> bytes{};
for (std::size_t i = 0; i < bytes.size(); ++i) bytes[i] = static_cast<unsigned char>(0x10u + i);
struct Case {
std::size_t offset;
std::array<std::uint32_t, 4> values;
};
const std::array cases{
Case{1, {0x14131211u, 0x18171615u, 0x1c1b1a19u, 0x201f1e1du}},
Case{2, {0x15141312u, 0x19181716u, 0x1d1c1b1au, 0x21201f1eu}},
Case{3, {0x16151413u, 0x1a191817u, 0x1e1d1c1bu, 0x2221201fu}},
Case{13, {0x201f1e1du, 0x24232221u, 0x28272625u, 0x2c2b2a29u}},
};
for (std::size_t i = 0; i < writers.size(); ++i) {
for (const auto& entry : cases) {
Storage storage;
storage.words.fill(0xabcdef01u);
const auto* values = reinterpret_cast<const std::uint32_t*>(bytes.data() + entry.offset);
auto* packet = writers[i](&storage.buffer, 0x40u, values, 4);
check(packet == storage.words.data() && storage.buffer.cursor_up == packet + 6, "unaligned register range allocation mismatch");
check(packet[0] == headers[i] && packet[1] == 0x40u && std::equal(entry.values.begin(), entry.values.end(), packet + 2), "unaligned register values were not read byte for byte");
check(packet[6] == 0xabcdef01u, "unaligned register range overwrote the following word");
}
}
}
void testIndirect() {
const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect};
const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize};
@@ -210,6 +238,7 @@ int main() {
try {
testDirect();
testConfig();
testRangeFromAnyByte();
testIndirect();
testIndirectPlaceholder();
testDirectList();
+65 -4
View File
@@ -266,6 +266,7 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationScaler.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PresentationPass.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/PerformanceOverlay.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Execution/src/Screenshot.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Draw.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/DepthSurface.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleTarget.cpp
@@ -358,6 +359,7 @@ add_shader_recompiler(shader_recompiler_agc_driver libc
target_link_libraries(libSceAgcDriver PRIVATE
shader_recompiler_agc_driver
decoder_png
SDL2-static
libc
libkernel
@@ -367,6 +369,7 @@ target_link_libraries(libSceAgcDriver PRIVATE
if(APS5_ENABLE_TRACY)
target_link_libraries(libSceAgcDriver PUBLIC TracyClient)
endif()
target_link_options(libSceAgcDriver PRIVATE LINKER:--exclude-libs,$<TARGET_FILE_NAME:decoder_png>)
if(BUILD_TESTING)
add_test_executable(agc_driver_draw_program_trace_tests
@@ -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() == &sum;
}
bool halfExtractions() {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
auto& low = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(3u), &lane});
auto& high = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(5u), &lane});
auto& lowHalf = builder.Emit(IrOpcode::BitwiseAnd32, IrType::U32, {&low, &builder.Constant(0xffffu)});
auto& highHalf = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)});
auto& merged = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&lowHalf, &highHalf});
auto& top = builder.Emit(IrOpcode::ShiftRightLogical32, IrType::U32, {&merged, &builder.Constant(16u)});
auto& field = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(16u), &builder.Constant(16u)});
auto& bottom = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&merged, &builder.Constant(0u), &builder.Constant(16u)});
auto& topUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&top});
auto& fieldUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&field});
auto& bottomUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&bottom});
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
ConstantFolder().Fold(program);
const IrValue* topResult = topUser.Argument(0)->Resolve();
const IrValue* fieldResult = fieldUser.Argument(0)->Resolve();
const IrValue* bottomResult = bottomUser.Argument(0)->Resolve();
const bool topMasked = topResult->Opcode() == IrOpcode::BitwiseAnd32 && topResult->Argument(0)->Resolve() == &high && topResult->Argument(1)->Resolve()->ImmediateU32() == 0xffffu;
const bool fieldThrough = fieldResult->Opcode() == IrOpcode::BitFieldUExtract && fieldResult->Argument(0)->Resolve() == &high && fieldResult->Argument(1)->Resolve()->ImmediateU32() == 0u;
const bool bottomThrough = bottomResult->Opcode() == IrOpcode::BitFieldUExtract && bottomResult->Argument(0)->Resolve() == &low;
return topMasked && fieldThrough && bottomThrough;
}
bool halvesRejoin() {
IrProgram program;
auto& block = program.CreateBlock();
program.SetEntryBlock(block);
program.BlockOrder().push_back(&block);
IrBuilder builder(program);
builder.SetInsertionPoint(block);
auto& lane = builder.Emit(IrOpcode::LaneId, IrType::U32, {});
auto& word = builder.Emit(IrOpcode::IAdd32, IrType::U32, {&builder.Constant(7u), &lane});
auto& low = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(0u), &builder.Constant(16u)});
auto& high = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&word, &builder.Constant(16u), &builder.Constant(16u)});
auto& raised = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&high, &builder.Constant(16u)});
auto& joined = builder.Emit(IrOpcode::BitwiseOr32, IrType::U32, {&low, &raised});
auto& shifted = builder.Emit(IrOpcode::ShiftLeftLogical32, IrType::U32, {&word, &builder.Constant(16u)});
auto& back = builder.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&shifted, &builder.Constant(16u), &builder.Constant(16u)});
auto& joinedUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&joined});
auto& backUser = builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&back});
static_cast<void>(builder.Emit(IrOpcode::Return, IrType::Void, {}));
ConstantFolder().Fold(program);
const IrValue* rejoined = joinedUser.Argument(0)->Resolve();
const IrValue* field = backUser.Argument(0)->Resolve();
const bool fieldBack = field->Opcode() == IrOpcode::BitFieldUExtract && field->Argument(0)->Resolve() == &word && field->Argument(1)->Resolve()->ImmediateU32() == 0u;
return rejoined == &word && fieldBack;
}
bool 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();
+102 -10
View File
@@ -12,6 +12,7 @@
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "ShaderDiskCache.hpp"
#include "ShaderCacheDirectory.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
@@ -51,6 +52,7 @@
#include <random>
#include <initializer_list>
#include <iostream>
#include <limits>
#include <map>
#include <optional>
#include <set>
@@ -1673,7 +1675,12 @@ void DisabledColorTests() {
Require(!state.hasColorTarget && state.color.address == 0 && state.color.bytes == 0, "disabled color writes accessed a color surface");
Require(state.renderExtent.width == 64 && state.renderExtent.height == 4, "attachment-free framebuffer lost screen scissor extent");
queue.context[0xd] = 0;
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "empty framebuffer extent");
const auto emptyScreen = AgcDriver::Graphics::DecodeState(queue);
Require(emptyScreen.renderExtent.width == 1 && emptyScreen.renderExtent.height == 1, "an empty screen scissor without targets did not get a one-pixel framebuffer");
Require(emptyScreen.scissor.extent.width == 0 && emptyScreen.scissor.extent.height == 0, "an empty screen scissor without targets rasterized pixels");
queue.context[0xd] = 64u;
const auto emptyRows = AgcDriver::Graphics::DecodeState(queue);
Require(emptyRows.renderExtent.width == 64 && emptyRows.scissor.extent.height == 0, "a screen scissor without rows rasterized pixels");
queue = makeState();
queue.context[0x8e] = 3;
const auto partial = AgcDriver::Graphics::DecodeState(queue);
@@ -2596,6 +2603,28 @@ void DepthBoundsBiasTests() {
queue.context[0x009] = bits(0.75f);
auto state = AgcDriver::Graphics::DecodeState(queue);
Require(state.depthTest && state.depthBoundsTest && state.minDepthBounds == 0.25f && state.maxDepthBounds == 0.75f, "depth bounds decode changed");
const auto infinity = std::numeric_limits<float>::infinity();
for (const auto bounds : {std::array{0.0f, infinity}, std::array{-infinity, 1.0f}, std::array{-infinity, infinity}}) {
queue.context[0x008] = bits(bounds[0]);
queue.context[0x009] = bits(bounds[1]);
state = AgcDriver::Graphics::DecodeState(queue);
Require(state.depthBoundsTest && state.minDepthBounds == bounds[0] && state.maxDepthBounds == bounds[1], "infinite depth bounds changed");
AgcDriver::Graphics::Context context{};
expectFailure([&] { AgcDriver::Graphics::ValidateDepthBounds(context, state); }, "depth bounds outside [0, 1]");
context.depthRangeUnrestricted = true;
AgcDriver::Graphics::ValidateDepthBounds(context, state);
}
for (const auto offset : {0x008u, 0x009u}) {
queue.context[0x008] = bits(0.25f);
queue.context[0x009] = bits(0.75f);
queue.context[offset] = bits(std::numeric_limits<float>::quiet_NaN());
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "NaN depth bound");
}
queue.context[0x008] = bits(0.25f);
queue.context[0x009] = bits(0.75f);
queue.context[0x00b] = bits(infinity);
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "non-finite register");
queue.context[0x00b] = bits(0.0f);
queue.context[0x1b3] = 2;
queue.context[0x1b4] = 2;
const auto rejection = AgcDriver::Graphics::DrawRejection(queue, false);
@@ -4814,6 +4843,7 @@ AgcDriver::Graphics::Context mockContext() {
context.limits.maxPerStageDescriptorStorageBuffers = 16;
context.limits.maxPerStageResources = 128;
context.limits.maxDescriptorSetStorageBuffers = 32;
context.limits.maxImageArrayLayers = 2048;
return context;
}
@@ -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(), &region, sizeof(region)) == sizeof(region) && region.Type == MEM_MAPPED,
"long-path persisted SPIR-V is not mapped");
requireSameVariant(variant, *persisted, "long-path persist round trip");
CompiledVariant loaded;
require(ShaderDiskCache::Load(key, loaded), "a long-path persisted cache entry did not load");
requireSameVariant(variant, loaded, "long-path load round trip");
const auto packPath = directory / (std::wstring(75, L'p') + L".pack");
files.push_back(packPath);
require(WriteFileAtomically(packPath, packBytes({{key, EncodeEntry(key, variant)}})), "cannot publish the long-path cache pack");
CachePack pack(packPath);
require(pack.Valid(), "a long-path cache pack was rejected: " + pack.Error());
PackLoadTimings timings;
CompiledVariant decoded;
require(pack.Load(key, decoded, timings) == LoadStatus::Loaded, "a long-path cache pack entry did not load");
requireSameVariant(variant, decoded, "long-path cache pack round trip");
std::cout << "long-path shader cache persistence and pack tests passed\n";
}
#endif
struct ComputeRequest {
std::vector<std::uint32_t> code{0xe0700000u, 0x80000000u, 0xbf810000u};
std::array<std::uint32_t, 4> userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu};
@@ -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;
@@ -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,
+25
View File
@@ -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;
}
}
+9 -7
View File
@@ -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) {
+15 -2
View File
@@ -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;
}
}
+3 -3
View File
@@ -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;
+7 -7
View File
@@ -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