Merge branch 'main' of https://github.com/boykopovar/AnyPS5 into fix/agc-release-mem-system-counter

# Conflicts:
#	docs/dev/debt/libSceAgcDriver.md
This commit is contained in:
Savvanis Spyros committed 2026-10-21 14:15:53 +03:00
commit 123768abcd
1186 files changed
+81449 -4917

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
+3
View File
@@ -49,3 +49,6 @@
[submodule "3rdparty/ZArchive"]
path = 3rdparty/ZArchive
url = https://github.com/Exzap/ZArchive.git
[submodule "3rdparty/vgmstream"]
path = 3rdparty/vgmstream
url = https://github.com/vgmstream/vgmstream.git
Vendored Submodule
+1
Submodule 3rdparty/vgmstream added at 7c520e8f9b.
+39 -2
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}")
@@ -62,6 +65,18 @@ include(cmake/SystemDeps.cmake)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(CheckCXXSourceCompiles)
function(anyps5_check_cxx_links_standard_library)
set(CMAKE_TRY_COMPILE_TARGET_TYPE EXECUTABLE)
check_cxx_source_compiles("#include <string>\nint main(int argc, char**) { try { throw std::string(static_cast<std::size_t>(argc), 'x'); } catch (const std::string& text) { return static_cast<int>(text.size()) - argc; } }" ANYPS5_CXX_LINKS_STANDARD_LIBRARY)
endfunction()
anyps5_check_cxx_links_standard_library()
if(NOT ANYPS5_CXX_LINKS_STANDARD_LIBRARY)
message(FATAL_ERROR "The C++ compiler ${CMAKE_CXX_COMPILER} cannot link a program that uses the C++ standard library. "
"Set CMAKE_CXX_COMPILER to the C++ driver (g++ or clang++, not gcc or clang) in a new build directory. "
"Supported toolchains are listed in docs/dev/BUILD.md.")
endif()
if(ANYPS5_ENABLE_ASAN)
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(FATAL_ERROR "ANYPS5_ENABLE_ASAN is supported only on Linux")
@@ -108,6 +123,10 @@ if(BUILD_TESTING)
core/shader/recompiler/IntermediateRepresentation/include)
add_test(NAME shader_ir_use_rewrite COMMAND shader_ir_use_rewrite_tests)
set_tests_properties(shader_ir_use_rewrite PROPERTIES TIMEOUT 20)
add_test_executable(pad_command_file_tests core/libs/prx/libScePad/tests/InputCommandFile.cpp)
target_include_directories(pad_command_file_tests PRIVATE core/libs)
add_test(NAME pad_command_file COMMAND pad_command_file_tests)
set_tests_properties(pad_command_file PROPERTIES TIMEOUT 20)
if(Python3_Interpreter_FOUND)
add_test(NAME shader_cache_native_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/core/shader/recompiler/tests/test_shader_cache_native_paths.py
@@ -155,6 +174,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_wrapped_declarations.py)
add_test(NAME conventions_ci_gate COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_ci_gate.py)
add_test(NAME conventions_git_errors COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_git.py)
add_test(NAME conventions_doc_links COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_doc_links.py)
add_test(NAME conventions_unknown_nids COMMAND ${Python3_EXECUTABLE}
@@ -163,6 +184,8 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_conventions_debt_length.py)
add_test(NAME test_registration COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_test_registration.py)
add_test(NAME test_script_registration COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/check_test_registration.py --scripts)
add_test(NAME build_eligibility COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_eligibility.py)
add_test(NAME check_skips COMMAND ${Python3_EXECUTABLE}
@@ -172,7 +195,13 @@ if(BUILD_TESTING)
add_test(NAME check_main_sync COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_check_main_sync.py)
add_test(NAME configure_seed COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py)
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedTests)
add_test(NAME configure_seed_paths COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedPathTests)
add_test(NAME configure_seed_round_trips COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ConfigureSeedRoundTripTests)
add_test(NAME configure_seed_parsing COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_configure_seed.py ChecksParsingTests)
add_test(NAME stubgen COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_stubgen.py)
add_test(NAME debt_split COMMAND ${Python3_EXECUTABLE}
@@ -183,23 +212,31 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_hw_oracle.py)
add_test(NAME hardware_oracle_compare COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/hw-oracle/test_compare_results.py)
set_tests_properties(hardware_oracle_compare PROPERTIES TIMEOUT 20)
set_tests_properties(hardware_oracle_compare PROPERTIES TIMEOUT 120)
add_test(NAME pr_overlap_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_exports.py)
add_test(NAME pr_overlap_wrapped_exports COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_wrapped_exports.py)
add_test(NAME pr_overlap_own_branch COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_own_branch.py)
add_test(NAME pr_overlap_errors COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pr_overlap_errors.py)
add_test(NAME nid_database_cache COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_nid_names.py)
add_test(NAME release_package COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_package_release.py)
add_test(NAME web_release COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_web_release.py)
set_tests_properties(web_release PROPERTIES TIMEOUT 20)
add_test(NAME web_library_assets COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_build_web.py)
set_tests_properties(web_library_assets PROPERTIES TIMEOUT 20)
add_test(NAME pad_input COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_input.py)
set_tests_properties(pad_input PROPERTIES TIMEOUT 20)
add_test(NAME pad_timeline COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_pad_timeline.py)
set_tests_properties(pad_timeline PROPERTIES TIMEOUT 20)
add_test(NAME trace_digest COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tools/tests/test_trace_digest.py)
add_test(NAME conventions_bom_shebang COMMAND ${Python3_EXECUTABLE}
+2 -1
View File
@@ -5,10 +5,11 @@
- Follow the [coding conventions](docs/dev/CONVENTIONS.md): naming, no comments except [technical debt](docs/dev/TechnicalDebt.md), Conventional Commits.
- Every function either does exactly what it is supposed to or throws. Unimplemented exports call `NotImplemented_nid_no_patch(__func__)` (see [libSceAudioIn](core/libs/prx/libSceAudioIn/Export.cpp)).
- Pull requests that add or change shader instruction semantics say where they come from: measured on hardware (which GPU and what was checked, e.g. with the [hardware oracle](docs/dev/HW_ORACLE.md)) or the exact source (ISA section, LLVM, ACO or Mesa file). Reviewers check the semantics on hardware. Cases the source doesn't settle throw, and behaviour not verified on hardware is recorded in [technical debt](docs/dev/TechnicalDebt.md).
- An export's signature or behaviour taken from a title's call sites or from a library's disassembly follows [reading titles and libraries](docs/dev/REVERSE_ENGINEERING.md): a call site settles the arguments, not what the library does.
- A silent stub is allowed only when it unblocks a title and only affects the UI; add it under Silent stubs in its library's [technical debt](docs/dev/debt/) file.
- Missing imports fail at startup with a clear error; don't replace them with fallbacks that keep running.
- Implement the general behaviour of a function, not what one title happens to need.
- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries (NpCppWebApi over NpWebApi2). The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx).
- Don't add replacements for modules that titles ship themselves in `sce_module/`, `sce_modules/` or `prx/`: engine or middleware modules (Cohtml, FMOD, GOG Galaxy) and SDK libraries that only wrap other system libraries. The relinker converts and loads the title's own module, and a host library with the same name is left out of `DT_NEEDED`. Only system libraries, which reach the kernel or the hardware, are reimplemented in [core/libs/prx](core/libs/prx). A host copy may still exist for a wrapped library that some dumps lack entirely, like libSceNpCppWebApi: the title's own module wins whenever one is shipped, so it is only ever consulted by titles that ship nothing.
- Avoid non-standard extensions (`__attribute__`, etc.) where standard C++ is enough. Helper symbols that must not become NIDs use the `_nid_no_patch` or `_nid_no_patch_cut` suffix.
- The relinker uses only the C++20 standard library.
- Third-party code is added as a submodule under `3rdparty/` and built from source, not found on the system. With `ANYPS5_USE_SYSTEM_DEPS=ON` the packages packaged by the distributions (SDL2, freetype, Vulkan-Headers, SPIRV-Headers, glslang and SPIRV-Tools) come from the system instead; that resolution lives in [cmake/SystemDeps.cmake](cmake/SystemDeps.cmake) and the default build never reads the system.
@@ -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;
}
+56
View File
@@ -82,6 +82,9 @@ add_library(libc SHARED EXCLUDE_FROM_ALL)
set(LIBS_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR})
add_subdirectory(${LIBC_DIR} ${CMAKE_CURRENT_BINARY_DIR}/prx/libc)
target_include_directories(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
if(WIN32)
target_compile_options(libc PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp")
endif()
configure_windows_unwind(libc)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(libc PRIVATE LINKER:-z,now)
@@ -139,6 +142,9 @@ foreach(LIB_DIR ${LIBS_DIRS})
configure_windows_unwind(${LIB_NAME})
endif()
if(WIN32)
target_compile_options(${LIB_NAME} PRIVATE "SHELL:-include ${CMAKE_CURRENT_SOURCE_DIR}/prx/libc/include/HostStandardStreams.hpp")
endif()
if(LIB_NAME STREQUAL "libkernel")
target_link_libraries(${LIB_NAME} PUBLIC libc)
else()
@@ -354,6 +360,26 @@ 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_np_party_tests tests/GuestNpParty.cpp)
target_include_directories(guest_np_party_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_party_tests PRIVATE libSceNpParty libc)
configure_windows_unwind(guest_np_party_tests)
add_test(NAME guest_np_party COMMAND guest_np_party_tests)
if(WIN32)
set_tests_properties(guest_np_party PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpParty>")
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 +424,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 +494,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)
@@ -537,6 +583,16 @@ if(BUILD_TESTING)
set_tests_properties(prx_thread_local_destructors PROPERTIES TIMEOUT 300)
endif()
add_test_executable(guest_np_cpp_web_api_intrusive_ptr_tests tests/GuestNpCppWebApiIntrusivePtr.cpp)
target_include_directories(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(guest_np_cpp_web_api_intrusive_ptr_tests PRIVATE libSceNpCppWebApi libc)
configure_windows_unwind(guest_np_cpp_web_api_intrusive_ptr_tests)
add_test(NAME guest_np_cpp_web_api_intrusive_ptr COMMAND guest_np_cpp_web_api_intrusive_ptr_tests)
if(WIN32)
set_tests_properties(guest_np_cpp_web_api_intrusive_ptr PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceNpCppWebApi>")
endif()
add_test_executable(savedata_core_write_replacement_tests tests/SaveDataWriteReplacement.cpp)
target_include_directories(savedata_core_write_replacement_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(savedata_core_write_replacement_tests PRIVATE libSceSaveData)
+38 -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;
@@ -1460,6 +1460,26 @@ struct NpWebApi2ResponseInformationOption {
std::size_t response_data_size;
};
struct NpWebApi2IntInitializeArgs {
std::int32_t lib_http_ctx_id;
std::int32_t reserved;
std::uint64_t pool_size;
const char* name;
std::uint64_t struct_size;
};
static_assert(sizeof(NpWebApi2IntInitializeArgs) == 32);
struct NpWebApi2IntInitialize2Args {
std::int32_t lib_http_ctx_id;
std::int32_t reserved;
std::uint64_t pool_size;
const char* name;
std::uint32_t push_config_group;
std::int32_t reserved2;
std::uint64_t struct_size;
};
static_assert(sizeof(NpWebApi2IntInitialize2Args) == 40);
struct GameUpdateCheckParam {
std::size_t size;
std::uint32_t option;
@@ -1901,6 +1921,15 @@ struct SystemServiceDisplaySafeAreaInfo {
std::uint8_t reserved[128];
};
struct SystemServicePlayerDialogParam {
std::uint64_t size;
std::int32_t mode;
std::int32_t user_id;
std::uint64_t target_account_id;
std::uint8_t reserved[40];
};
static_assert(sizeof(SystemServicePlayerDialogParam) == 0x40);
struct SystemServiceHdrToneMapLuminance {
float max_full_frame_tone_map_luminance;
float max_tone_map_luminance;
@@ -1941,6 +1970,13 @@ struct SystemGestureTouchRecognizerInformation {
struct SystemGestureTouchEvent { std::uint8_t reserve[168]; };
struct SystemGestureTouchPadData {
std::int32_t pad_handle;
std::int32_t report_number;
const PadData* pad_data_buffer;
std::uint8_t reserve[8];
};
struct UserServiceLoginUserIdList { int user_id[4]; };
struct UserServiceRegisteredUserIdList { int user_id[16]; };
+63 -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,20 @@ 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;
}
int APS5_VABI sceAcmBatchJobNotification() {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
+8 -7
View File
@@ -72,13 +72,14 @@ std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf,
}
std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t videoOutHandle, std::int32_t displayBufferIndex, std::uint32_t flipMode, std::int64_t flipArg) {
(void)buf;
(void)videoOutHandle;
(void)displayBufferIndex;
(void)flipMode;
(void)flipArg;
NotImplemented_nid_no_patch(__func__);
return nullptr;
auto* packet = Agc::Command::Allocate(buf, AgcDriver::FlipPacketWords, __func__);
packet[0] = AgcDriver::FlipPacketHeader;
packet[1] = videoOutHandle;
packet[2] = static_cast<std::uint32_t>(displayBufferIndex);
packet[3] = flipMode;
packet[4] = static_cast<std::uint32_t>(static_cast<std::uint64_t>(flipArg));
packet[5] = static_cast<std::uint32_t>(static_cast<std::uint64_t>(flipArg) >> 32u);
return packet;
}
std::uint32_t* APS5_VABI sceAgcAcbSetWorkloadsActive(CommandBuffer* buf, std::uint32_t streamId, const std::uint32_t* workloadIds, std::uint32_t workloadCount) {
+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);
@@ -42,16 +42,14 @@ std::uint32_t* APS5_VABI sceAgcDcbSetIndexSize_0600(CommandBuffer* buf, std::uin
return packet;
}
std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint32_t dataOffsetInBytes) {
(void)buf;
(void)dataOffsetInBytes;
NotImplemented_nid_no_patch(__func__);
return nullptr;
std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex) {
if (attributeAddress != 0) Agc::Command::CheckAddress(attributeAddress, 16, __func__);
Agc::Command::CheckBits(attributeIndex, 0xffffu, __func__);
return Agc::Command::Emit(buf, 0x91u, {static_cast<std::uint32_t>(attributeAddress), static_cast<std::uint32_t>(attributeAddress >> 32u), attributeIndex}, __func__);
}
std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize() {
NotImplemented_nid_no_patch(__func__);
return 0;
return 16;
}
}
+13
View File
@@ -0,0 +1,13 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
int APS5_VABI sceAgcSuspendPointAndCheckStatus(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
+36 -2
View File
@@ -16,6 +16,7 @@ extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std:
extern "C" std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbWaitUntilSafeForRendering(CommandBuffer* buf, std::uint32_t handle, std::uint32_t index);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetFlip(CommandBuffer* buf, std::uint32_t handle, std::int32_t index, std::uint32_t mode, std::int64_t argument);
extern "C" int APS5_VABI sceAgcSuspendPoint();
extern "C" int APS5_VABI sceAgcInit(std::uint32_t version);
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults();
@@ -47,6 +48,8 @@ extern "C" std::uint32_t* APS5_VABI sceAgcAcbPushMarker(CommandBuffer* buf, cons
extern "C" std::uint32_t* APS5_VABI sceAgcAcbPopMarker(CommandBuffer* buf);
extern "C" std::uint32_t* APS5_VABI sceAgcAcbSetMarker(CommandBuffer* buf, const char* str, std::uint32_t color);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexBuffer(CommandBuffer* buf, std::uint64_t indexAddress);
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetIndexIndirectArgs(CommandBuffer* buf, std::uint64_t attributeAddress, std::uint32_t attributeIndex);
extern "C" std::uint32_t APS5_VABI sceAgcDcbSetIndexIndirectArgsGetSize();
extern "C" std::uint32_t* APS5_VABI sceAgcDcbSetBaseIndirectArgs(CommandBuffer* buf, std::uint32_t shaderType, const volatile void* indirectBaseAddr);
extern "C" std::uint32_t* APS5_VABI sceAgcSetNop(CommandBuffer* buf, std::uint32_t sizeDw);
extern "C" int APS5_VABI sceAgcDebugRaiseException(std::uint32_t exceptionId);
@@ -324,6 +327,20 @@ void testIndexBuffer() {
throw std::runtime_error("misaligned index buffer was accepted");
}
void testIndexIndirectArgs() {
Storage storage;
const auto* packet = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x4444u);
const std::array<std::uint32_t, 4> expected{0xc0029100u, 0x22222220u, 0x11111111u, 0x4444u};
check(packet == storage.words.data() && std::equal(expected.begin(), expected.end(), packet), "index indirect args packet differs from the console's");
check(sceAgcDcbSetIndexIndirectArgsGetSize() == expected.size() * sizeof(std::uint32_t), "index indirect args size differs from its packet");
const auto* cleared = sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0, 0);
check(cleared == packet + expected.size() && cleared[0] == expected[0] && cleared[1] == 0 && cleared[2] == 0 && cleared[3] == 0, "null index attribute base was not written as 0");
const auto before = storage.words;
expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222228u, 0); });
expectFailure([&] { sceAgcDcbSetIndexIndirectArgs(&storage.buffer, 0x1111111122222220u, 0x10000u); });
check(storage.words == before && storage.buffer.cursor_up == cleared + expected.size(), "rejected index indirect args modified the buffer");
}
void testIndirectBase() {
Storage storage;
alignas(8) std::uint32_t arguments[4]{};
@@ -426,6 +443,16 @@ void testFlip() {
full.buffer.cursor_down = full.words.data() + 3;
expectFailure([&] { sceAgcAcbWaitUntilSafeForRendering(&full.buffer, 1, 0); });
check(full.buffer.cursor_up == full.words.data(), "failed compute rendering wait advanced cursor");
Storage acbFlip;
acbFlip.words.fill(0xdeadbeefu);
auto* computeFlip = sceAgcAcbSetFlip(&acbFlip.buffer, 0xfedcba98u, -2, 0x12345678u, -0x123456789abcdefLL);
check(computeFlip == acbFlip.words.data() && std::equal(expected.begin(), expected.end(), computeFlip), "compute flip packet mismatch");
check(acbFlip.buffer.cursor_up == computeFlip + 6 && computeFlip[6] == 0xdeadbeefu, "compute flip overran allocation");
expectFailure([] { sceAgcAcbSetFlip(nullptr, 1, 0, 1, 0); });
Storage fullFlip;
fullFlip.buffer.cursor_down = fullFlip.words.data() + 5;
expectFailure([&] { sceAgcAcbSetFlip(&fullFlip.buffer, 1, 0, 1, 0); });
check(fullFlip.buffer.cursor_up == fullFlip.words.data(), "failed compute flip advanced cursor");
check(sceAgcSuspendPoint() == 0, "empty suspend failed");
}
@@ -463,8 +490,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() {
@@ -773,6 +806,7 @@ int main(int argc, char** argv) {
testWorkloads();
testWorkloadStreams();
testIndexBuffer();
testIndexIndirectArgs();
testIndirectBase();
testContextState();
testFlip();
@@ -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();
+309 -8
View File
@@ -172,6 +172,25 @@ foreach(agcPresentationStage IN ITEMS Vert Frag)
)
list(APPEND agcPresentationShaderHeaders ${agcPresentationShaderHeader})
endforeach()
foreach(agcSampleOrderVariant IN ITEMS "R8;comp;-DTEXEL_FORMAT=r8ui" "R16;comp;-DTEXEL_FORMAT=r16ui" "R32;comp;-DTEXEL_FORMAT=r32ui" "RG32;comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;comp;-DTEXEL_FORMAT=rgba32ui" "Vertex;vert;-DPLAIN=1" "Depth;frag;-DPLAIN=1" "StencilExport;frag;-DSTENCIL_EXPORT=1" "StencilBits;frag;-DSTENCIL_BITS=1")
list(GET agcSampleOrderVariant 0 agcSampleOrderName)
list(GET agcSampleOrderVariant 1 agcSampleOrderExtension)
list(GET agcSampleOrderVariant 2 agcSampleOrderDefine)
string(TOUPPER "${agcSampleOrderName}" agcSampleOrderSymbol)
set(agcSampleOrderShaderSource ${CMAKE_CURRENT_SOURCE_DIR}/Graphics/shaders/SampleOrder.${agcSampleOrderExtension})
set(agcSampleOrderShaderSpv ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}.spv)
set(agcSampleOrderShaderHeader ${agcTextureDetileGeneratedDir}/SampleOrder${agcSampleOrderName}_spv.h)
add_custom_command(
OUTPUT "${agcSampleOrderShaderHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTextureDetileGeneratedDir}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 ${agcSampleOrderDefine} -o "${agcSampleOrderShaderSpv}" "${agcSampleOrderShaderSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderShaderSpv} -DOUTPUT=${agcSampleOrderShaderHeader}
-DSYMBOL=SAMPLE_ORDER_${agcSampleOrderSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcSampleOrderShaderSource}" glslang-standalone
VERBATIM
)
list(APPEND agcSampleOrderShaderHeaders ${agcSampleOrderShaderHeader})
endforeach()
foreach(agcOverlayStage IN ITEMS Vert Frag)
string(TOLOWER "${agcOverlayStage}" agcOverlayExtension)
string(TOUPPER "${agcOverlayStage}" agcOverlaySymbol)
@@ -266,6 +285,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
@@ -296,9 +316,11 @@ set(agcDriverSources
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/MultisampleReadback.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Texture.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/PassHazards.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/SampleOrder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/BarrierBatch.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/UnitShadow.cpp
${CMAKE_CURRENT_SOURCE_DIR}/Graphics/src/Sampler.cpp
${agcSampleOrderShaderHeaders}
${agcTextureDetileShaderHeader}
${agcReadbackHeaders}
${agcCmaskClearShaderHeader}
@@ -358,6 +380,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 +390,9 @@ target_link_libraries(libSceAgcDriver PRIVATE
if(APS5_ENABLE_TRACY)
target_link_libraries(libSceAgcDriver PUBLIC TracyClient)
endif()
if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_options(libSceAgcDriver PRIVATE LINKER:--exclude-libs,$<TARGET_FILE_NAME:decoder_png>)
endif()
if(BUILD_TESTING)
add_test_executable(agc_driver_draw_program_trace_tests
@@ -643,7 +669,11 @@ if(BUILD_TESTING)
add_test(NAME agc_write_tracking COMMAND agc_write_tracking_tests)
set_tests_properties(agc_write_tracking PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_write_tracking PROPERTIES ENVIRONMENT_MODIFICATION
add_test(NAME agc_write_tracking_metadata_commit COMMAND agc_write_tracking_tests --metadata-commit)
set_tests_properties(agc_write_tracking_metadata_commit PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_resident_direct_remap COMMAND agc_write_tracking_tests --resident-direct-remap)
set_tests_properties(agc_resident_direct_remap PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 20)
set_tests_properties(agc_write_tracking agc_write_tracking_metadata_commit agc_resident_direct_remap PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
@@ -685,6 +715,15 @@ if(BUILD_TESTING)
add_test(NAME agc_fragment_barycentric COMMAND agc_fragment_barycentric_tests)
endif()
add_test_executable(agc_float32_flush_control_tests EXCLUDE_FROM_ALL tests/Float32FlushControl.cpp)
target_include_directories(agc_float32_flush_control_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_float32_flush_control_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_float32_flush_control_tests)
if(BUILD_TESTING)
set_target_properties(agc_float32_flush_control_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_float32_flush_control COMMAND agc_float32_flush_control_tests)
endif()
add_test_executable(agc_pipeline_link_optimization_tests EXCLUDE_FROM_ALL tests/PipelineLinkOptimization.cpp)
target_include_directories(agc_pipeline_link_optimization_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_pipeline_link_optimization_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -712,6 +751,21 @@ if(BUILD_TESTING)
add_test(NAME agc_draw_inputs_in_place_driver COMMAND agc_draw_inputs_in_place_driver_tests)
endif()
add_test_executable(agc_subgroup_barrier_tests EXCLUDE_FROM_ALL tests/SubgroupBarrier.cpp)
target_include_directories(agc_subgroup_barrier_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
target_link_libraries(agc_subgroup_barrier_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_subgroup_barrier_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_subgroup_barrier_tests)
if(BUILD_TESTING)
set_target_properties(agc_subgroup_barrier_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_subgroup_barrier COMMAND agc_subgroup_barrier_tests)
set_tests_properties(agc_subgroup_barrier PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
if(WIN32)
set_tests_properties(agc_subgroup_barrier PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_device_fault_tests EXCLUDE_FROM_ALL tests/DeviceFault.cpp)
target_include_directories(agc_device_fault_tests PRIVATE ${LIBS_INCLUDE_DIR} ${ANYPS5_VULKAN_HEADERS_INCLUDE_DIR})
set_target_properties(agc_device_fault_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -789,6 +843,19 @@ if(BUILD_TESTING)
endif()
endif()
add_test_executable(agc_f32_denorm_preserve_tests EXCLUDE_FROM_ALL tests/F32DenormPreserve.cpp)
target_link_libraries(agc_f32_denorm_preserve_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_f32_denorm_preserve_tests)
if(BUILD_TESTING)
set_target_properties(agc_f32_denorm_preserve_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_f32_denorm_preserve COMMAND agc_f32_denorm_preserve_tests)
set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1")
if(WIN32)
set_tests_properties(agc_f32_denorm_preserve PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_spirv_branch_targets_tests EXCLUDE_FROM_ALL tests/SpirvBranchTargets.cpp)
target_link_libraries(agc_spirv_branch_targets_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_spirv_branch_targets_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -802,6 +869,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)
@@ -815,6 +895,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)
@@ -933,6 +1026,10 @@ add_test_executable(agc_vop1_control_decode_tests EXCLUDE_FROM_ALL tests/Vop1Con
target_link_libraries(agc_vop1_control_decode_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vop1_control_decode_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_vop1_control_decode_tests)
add_test_executable(agc_vertex_input_validation_tests EXCLUDE_FROM_ALL tests/VertexInputValidation.cpp)
target_link_libraries(agc_vertex_input_validation_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vertex_input_validation_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_vertex_input_validation_tests)
add_test_executable(agc_vop2_encoding_decode_tests EXCLUDE_FROM_ALL tests/Vop2EncodingDecode.cpp)
target_link_libraries(agc_vop2_encoding_decode_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_vop2_encoding_decode_tests PROPERTIES CXX_EXTENSIONS OFF)
@@ -1104,6 +1201,12 @@ target_link_libraries(agc_formatted_buffer_byte_stores_tests PRIVATE shader_reco
set_target_properties(agc_formatted_buffer_byte_stores_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_formatted_buffer_byte_stores_tests)
add_test_executable(agc_mesh_output_budget_tests EXCLUDE_FROM_ALL tests/MeshOutputBudget.cpp)
target_include_directories(agc_mesh_output_budget_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_mesh_output_budget_tests PRIVATE shader_recompiler_agc_driver libc)
set_target_properties(agc_mesh_output_budget_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_mesh_output_budget_tests)
add_test_executable(agc_shader_disk_cache_tests EXCLUDE_FROM_ALL tests/ShaderDiskCache.cpp)
target_include_directories(agc_shader_disk_cache_tests PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_shader_disk_cache_tests PRIVATE shader_recompiler_agc_driver libc)
@@ -1116,6 +1219,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)
@@ -1329,6 +1438,11 @@ if(BUILD_TESTING)
set_tests_properties(agc_unchanged_cpu_stamps PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(WIN32)
add_test(NAME agc_partial_watch_restoration COMMAND agc_driver_recorder_tests --partial-watch-restoration-only)
set_tests_properties(agc_partial_watch_restoration PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_storage_kept_unit_settle COMMAND agc_driver_recorder_tests --kept-unit-settle-only)
set_tests_properties(agc_storage_kept_unit_settle PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
@@ -1336,6 +1450,12 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_storage_baseline_settled COMMAND agc_driver_recorder_tests --settled-baseline-only)
add_test(NAME agc_storage_clear_hand_over COMMAND agc_driver_recorder_tests --clear-hand-over-only)
set_tests_properties(agc_storage_clear_hand_over PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_storage_clear_hand_over PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_storage_integer_clear COMMAND agc_driver_recorder_tests --integer-clear-only)
set_tests_properties(agc_storage_integer_clear PROPERTIES SKIP_RETURN_CODE 77)
if(WIN32)
@@ -1390,8 +1510,20 @@ if(BUILD_TESTING)
set_tests_properties(agc_copied_direct_ordering PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_storage_writeback_blocks COMMAND agc_driver_recorder_tests --writeback-blocks-only)
set_tests_properties(agc_storage_writeback_blocks PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30 ENVIRONMENT "APS5_PIN_WAIT_MS=3000")
if(WIN32)
set_tests_properties(agc_storage_writeback_blocks PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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)
@@ -1515,6 +1647,7 @@ target_include_directories(agc_host_import_retire_tests PRIVATE ${LIBS_INCLUDE_D
target_link_libraries(agc_host_import_retire_tests PRIVATE libSceAgcDriver libc)
set_target_properties(agc_host_import_retire_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_host_import_retire_tests)
target_sources(agc_host_import_retire_tests PRIVATE tests/HostImportFailure.cpp)
if(BUILD_TESTING)
set_target_properties(agc_host_import_retire_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
add_test(NAME agc_host_import_retire COMMAND agc_host_import_retire_tests)
@@ -1724,6 +1857,7 @@ add_test_executable(agc_driver_bda_device_tests EXCLUDE_FROM_ALL
tests/BdaShader.cpp
tests/BdaExecution.cpp
tests/BdaContracts.cpp
tests/HostImportFailure.cpp
)
target_include_directories(agc_driver_bda_device_tests PRIVATE
@@ -1735,6 +1869,15 @@ target_include_directories(agc_driver_bda_device_tests PRIVATE
target_link_libraries(agc_driver_bda_device_tests PRIVATE libSceAgcDriver SDL2-static libc)
set_target_properties(agc_driver_bda_device_tests PROPERTIES CXX_EXTENSIONS OFF)
configure_windows_unwind(agc_driver_bda_device_tests)
if(BUILD_TESTING)
add_test(NAME agc_driver_host_import_failure_handles COMMAND agc_driver_bda_device_tests --host-import-failures)
set_tests_properties(agc_driver_host_import_failure_handles 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_host_import_failure_handles PROPERTIES
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
add_test_executable(agc_driver_mesh_tests EXCLUDE_FROM_ALL tests/Mesh.cpp)
target_include_directories(agc_driver_mesh_tests PRIVATE
@@ -1782,6 +1925,14 @@ foreach(agcExecutionTestName IN LISTS ANYPS5_AGC_EXECUTION_TESTS)
message(FATAL_ERROR "ANYPS5_AGC_EXECUTION_TESTS names ${agcExecutionTestName}, which has no source in tests/execution")
endif()
endforeach()
if(BUILD_TESTING)
add_test(NAME agc_driver_indirect_thread_dimensions_cpu COMMAND agc_driver_indirect_thread_dimensions_tests)
set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT "APS5_NO_GPU_THREAD_DIMENSIONS=1" SKIP_RETURN_CODE 77 TIMEOUT 60)
if(WIN32)
set_tests_properties(agc_driver_indirect_thread_dimensions_cpu PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endif()
if(ANYPS5_ENABLE_SPIRV_TOOLS)
target_compile_definitions(agc_driver_stencil_null_feedback_tests PRIVATE ANYPS5_ENABLE_SPIRV_TOOLS=1)
target_link_libraries(agc_driver_stencil_null_feedback_tests PRIVATE SPIRV-Tools-static)
@@ -1828,10 +1979,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
@@ -1849,7 +2001,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>")
@@ -1920,6 +2072,27 @@ foreach(stage IN ITEMS vert frag)
endforeach()
target_include_directories(agc_driver_provoking_vertex_tests PRIVATE ${agcTestShaderGeneratedRoot})
foreach(agcSampleOrderTestVariant IN ITEMS "R8;SampleOrderTexels.comp;-DTEXEL_FORMAT=r8ui" "R16;SampleOrderTexels.comp;-DTEXEL_FORMAT=r16ui" "R32;SampleOrderTexels.comp;-DTEXEL_FORMAT=r32ui" "RG32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rg32ui" "RGBA32;SampleOrderTexels.comp;-DTEXEL_FORMAT=rgba32ui" "FillVertex;SampleOrderFill.vert;-DPLAIN=1" "FillDepth;SampleOrderFill.frag;-DPLAIN=1" "FillStencil;SampleOrderFill.frag;-DSTENCIL=1" "ReadDepth;SampleOrderRead.comp;-DPLAIN=1" "ReadStencil;SampleOrderRead.comp;-DSTENCIL=1" "ReadColor;SampleOrderRead.comp;-DCOLOR=1")
list(GET agcSampleOrderTestVariant 0 agcSampleOrderTestName)
list(GET agcSampleOrderTestVariant 1 agcSampleOrderTestFile)
list(GET agcSampleOrderTestVariant 2 agcSampleOrderTestDefine)
string(TOUPPER "${agcSampleOrderTestName}" agcSampleOrderTestSymbol)
set(agcSampleOrderTestSource ${CMAKE_CURRENT_SOURCE_DIR}/tests/shaders/${agcSampleOrderTestFile})
set(agcSampleOrderTestSpv ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}.spv)
set(agcSampleOrderTestHeader ${agcTestShaderGeneratedRoot}/SampleOrderTest${agcSampleOrderTestName}_spv.h)
add_custom_command(
OUTPUT "${agcSampleOrderTestHeader}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${agcTestShaderGeneratedRoot}"
COMMAND "$<TARGET_FILE:glslang-standalone>" -V --target-env vulkan1.1 ${agcSampleOrderTestDefine} -o "${agcSampleOrderTestSpv}" "${agcSampleOrderTestSource}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${agcSampleOrderTestSpv} -DOUTPUT=${agcSampleOrderTestHeader}
-DSYMBOL=SAMPLE_ORDER_TEST_${agcSampleOrderTestSymbol}_SPV -P "${CMAKE_CURRENT_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${agcSampleOrderTestSource}" glslang-standalone
VERBATIM
)
target_sources(agc_driver_sample_order_tests PRIVATE ${agcSampleOrderTestHeader})
endforeach()
target_include_directories(agc_driver_sample_order_tests PRIVATE ${agcTestShaderGeneratedRoot})
target_include_directories(agc_driver_prepared_exports_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
target_include_directories(agc_driver_pipeline_journal_tests PRIVATE $<TARGET_PROPERTY:shader_recompiler_agc_driver,INTERFACE_INCLUDE_DIRECTORIES>)
@@ -1929,6 +2102,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>)
@@ -1968,6 +2142,12 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_readonly_resummarize agc_driver_eop_interrupts PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_small_copy_sync COMMAND agc_driver_tests --small-copy-sync)
set_tests_properties(agc_driver_small_copy_sync PROPERTIES TIMEOUT 30 ENVIRONMENT "APS5_COPY_SYNC=1" SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_small_copy_sync PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_shader_mode_register COMMAND agc_shader_mode_register_tests)
set_tests_properties(agc_shader_mode_register PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 20)
if(WIN32)
@@ -2011,7 +2191,7 @@ if(BUILD_TESTING)
if(WIN32)
set_tests_properties(agc_shader_bindless agc_shader_selected_records_image_limit agc_shader_selected_records_pair_limit agc_shader_selected_records_direct_image_limit agc_shader_selected_records_direct_pair_limit agc_shader_srt_read_patching agc_shader_type_zero_image_materialization agc_shader_frontend_pair agc_shader_frontend_pair_off agc_shader_layout_pair agc_shader_layout_pair_off agc_shader_entry_lane_proof_budget PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
foreach(agcTest IN ITEMS agc_vertex_input_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_pass_hazards_tests agc_barrier_batch_tests agc_shader_disk_cache_tests agc_shader_device_profile_tests agc_placeholder_images_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests agc_driver_bda_device_tests agc_driver_mesh_tests agc_driver_cmask_fast_clear_tests agc_formatted_buffer_once_tests agc_formatted_buffer_byte_stores_tests ${agcExecutionTests} agc_fma_mix_once_tests agc_f16_single_convert_tests)
foreach(agcTest IN ITEMS agc_vertex_input_tests agc_mesh_output_budget_tests agc_shared_write_tests agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_ir_reclaim_tests agc_vector_window_tests agc_constant_bit_test_tests agc_integer_narrowing_tests agc_scalar_select_mask_tests agc_vertex_input_validation_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()
@@ -2024,6 +2204,27 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
add_test(NAME agc_driver_guarded_direct_root COMMAND agc_driver_guarded_null_pointer_tests --direct-root-only)
set_tests_properties(agc_driver_guarded_direct_root 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_guarded_direct_root PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_bda_device_selection_cpu COMMAND agc_driver_bda_device_tests --device-selection-cpu)
set_tests_properties(agc_driver_bda_device_selection_cpu PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_bda_device_selection_cpu PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_draw_unsupported_buffer_type_skip_refused COMMAND agc_driver_draw_unsupported_buffer_type_tests --skip-refused)
set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_SKIP_REFUSED_DRAWS=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_draw_unsupported_buffer_type_skip_refused PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_sparse_indirect_indices_unreadable COMMAND agc_driver_sparse_indirect_indices_tests --unreadable)
set_tests_properties(agc_driver_sparse_indirect_indices agc_driver_sparse_indirect_indices_unreadable PROPERTIES
ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
@@ -2045,6 +2246,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)
@@ -2060,9 +2262,54 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_partially_resident_pending_stores PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_shader_disk_cache_large_file COMMAND agc_shader_disk_cache_tests --large-file)
if(Python3_Interpreter_FOUND)
set(housekeepingModes plain)
if(WIN32)
list(APPEND housekeepingModes extended extended-long device)
else()
list(APPEND housekeepingModes plain-long)
endif()
foreach(housekeepingMode IN LISTS housekeepingModes)
set(housekeepingTest agc_shader_cache_housekeeping_${housekeepingMode})
add_test(NAME ${housekeepingTest} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_housekeeping.py
$<TARGET_FILE:agc_shader_disk_cache_tests> ${housekeepingMode})
set_tests_properties(${housekeepingTest} PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(${housekeepingTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endif()
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES SKIP_RETURN_CODE 77 RUN_SERIAL TRUE WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 240)
if(WIN32)
set_tests_properties(agc_shader_disk_cache_large_file PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
add_test(NAME agc_shader_disk_cache_long_paths COMMAND agc_shader_disk_cache_tests --long-host-paths)
add_test(NAME agc_shader_disk_cache_host_stderr COMMAND agc_shader_disk_cache_tests --host-stderr)
set_tests_properties(agc_shader_disk_cache_host_stderr PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_shader_disk_cache_long_paths PROPERTIES TIMEOUT 30 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(Python3_Interpreter_FOUND)
set(prebuiltDiscoveryModes plain relative plain-long)
if(WIN32)
list(APPEND prebuiltDiscoveryModes extended extended-forward device extended-long)
endif()
foreach(prebuiltDiscoveryMode IN LISTS prebuiltDiscoveryModes)
set(prebuiltDiscoveryTest agc_shader_prebuilt_discovery_${prebuiltDiscoveryMode})
add_test(NAME ${prebuiltDiscoveryTest} COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_shader_cache_prebuilt_discovery.py
$<TARGET_FILE:agc_shader_disk_cache_tests> ${prebuiltDiscoveryMode})
set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES TIMEOUT 20)
if(WIN32)
set_tests_properties(${prebuiltDiscoveryTest} PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
endforeach()
endif()
add_test(NAME agc_driver_large_tracked_snapshot COMMAND agc_driver_partially_resident_snapshot_tests --large-tracked)
set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_large_tracked_snapshot PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
if(WIN32)
add_test(NAME agc_driver_untracked_snapshot_hint COMMAND agc_driver_partially_resident_snapshot_tests --untracked-hint)
@@ -2094,6 +2341,13 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_native_multisample_depth PROPERTIES TIMEOUT 30)
set_tests_properties(agc_driver_sample_order PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_sample_order_grid_reference COMMAND agc_driver_sample_order_tests --grid-reference)
add_test(NAME agc_driver_sample_order_recipe_depth_locations COMMAND agc_driver_sample_order_tests --recipe-depth-locations)
set_tests_properties(agc_driver_sample_order_grid_reference agc_driver_sample_order_recipe_depth_locations PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
if(WIN32)
set_tests_properties(agc_driver_sample_order_grid_reference agc_driver_sample_order_recipe_depth_locations PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_native_depth_retirement COMMAND agc_driver_native_multisample_depth_tests --retirement)
set_tests_properties(agc_driver_native_depth_retirement PROPERTIES TIMEOUT 30 ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_native_depth_clears COMMAND agc_driver_native_multisample_depth_tests --clears)
@@ -2105,12 +2359,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)
@@ -2305,12 +2564,22 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_sampled_cache_budget_memory PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77)
add_test(NAME agc_driver_sampled_cache_budget_without_storage_image_multisample COMMAND agc_driver_sampled_cache_budget_tests without-storage-image-multisample)
set_tests_properties(agc_driver_sampled_cache_budget_without_storage_image_multisample PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection-cpu)
set_tests_properties(agc_sampled_cache_device_selection PROPERTIES SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_driver_sampled_cache_device_selection COMMAND agc_driver_sampled_cache_budget_tests device-selection)
set_tests_properties(agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_sampled_cache_device_selection agc_driver_sampled_cache_device_selection PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_resource_cache_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-admission)
add_test(NAME agc_driver_resource_cache_oversized_storage_admission COMMAND agc_driver_sampled_cache_budget_tests resource-oversized)
add_test(NAME agc_driver_resource_cache_storage_clear COMMAND agc_driver_sampled_cache_budget_tests resource-clear)
set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
add_test(NAME agc_driver_resource_cache_replaced_texture COMMAND agc_driver_sampled_cache_budget_tests resource-replaced)
add_test(NAME agc_driver_resource_cache_lookup_changed COMMAND agc_driver_sampled_cache_budget_tests resource-lookup-changed)
set_tests_properties(agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" SKIP_RETURN_CODE 77 TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_sampled_cache_budget_memory agc_driver_sampled_cache_budget_without_storage_image_multisample agc_driver_resource_cache_storage_admission agc_driver_resource_cache_oversized_storage_admission agc_driver_resource_cache_storage_clear agc_driver_resource_cache_replaced_texture agc_driver_resource_cache_lookup_changed PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
# The driver draw test again with the draw key off: the draw cache then keys on the decoded vertex
@@ -2344,6 +2613,14 @@ if(BUILD_TESTING)
add_test(NAME agc_descriptor_cache COMMAND agc_driver_graphics_tests --descriptor-cache)
add_test(NAME agc_driver_address_space_teardown COMMAND agc_driver_graphics_tests --address-space-teardown)
set_tests_properties(agc_driver_address_space_teardown PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1" WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}")
add_test(NAME agc_driver_pm4_rewind_trace COMMAND agc_driver_pm4_tests rewind-trace)
set_tests_properties(agc_driver_pm4_rewind_trace PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=8;APS5_TRACE_FRAMES=0:1;APS5_TRACE_DIR=${CMAKE_CURRENT_BINARY_DIR}/pm4-rewind-trace" TIMEOUT 30)
set_property(TEST agc_driver_pm4_rewind_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION
"APS5_TRACE_SECONDS=unset:" "APS5_TRACE_SUMMARY_EVERY=unset:")
if(WIN32)
set_property(TEST agc_driver_pm4_rewind_trace APPEND PROPERTY ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_driver_pm4_failure_inline COMMAND agc_driver_pm4_tests failure)
set_tests_properties(agc_driver_pm4_failure_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0" SKIP_RETURN_CODE 77)
if(WIN32)
@@ -2372,8 +2649,10 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT "ANYPS5_NO_SHADER_CACHE=1;APS5_PIPELINED_DRAWS=0;APS5_VERIFY_DRAW_RECIPE=1;APS5_VERIFY_REGISTER_KEYS=1" SKIP_RETURN_CODE 77)
set_tests_properties(agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES FAIL_REGULAR_EXPRESSION "FATAL:")
set_property(TEST agc_driver_draw_cache_reuse agc_driver_draw_cache_reuse_off APPEND PROPERTY ENVIRONMENT "APS5_VERIFY_DRAW_RECIPE=1" "APS5_VERIFY_REGISTER_KEYS=1")
add_test(NAME agc_driver_draw_data_hit_user_words_verify COMMAND agc_driver_draw_data_hit_user_words_tests)
set_tests_properties(agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT "APS5_VERIFY_DRAW_DATA_HITS=1" SKIP_RETURN_CODE 77)
if(WIN32)
set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline PROPERTIES ENVIRONMENT_MODIFICATION
set_tests_properties(agc_driver_draw_cache_reuse_off agc_driver_draw_cache_reuse_verify agc_driver_draw_cache_reuse_inline agc_driver_draw_data_hit_user_words_verify PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
@@ -2389,6 +2668,7 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_pipelined_dispatch_item PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
set_tests_properties(agc_driver_queued_index_read_order PROPERTIES TIMEOUT 60)
foreach(agcTest IN ITEMS agc_spirv_narrow_constant_store_tests agc_spirv13_image_tests agc_spirv_optimizer_tests agc_driver_constant_store_tests)
if(NOT TARGET ${agcTest})
@@ -2528,6 +2808,7 @@ if(AGC_BUILD_VISUAL_TEST)
target_link_libraries(agc_driver_visual_test PRIVATE
shader_recompiler_agc_driver
decoder_png
SDL2-static
libc
libkernel
@@ -2611,8 +2892,25 @@ add_executable(agc_pm4_replay EXCLUDE_FROM_ALL
target_include_directories(agc_pm4_replay PRIVATE ${LIBS_INCLUDE_DIR})
target_link_libraries(agc_pm4_replay PRIVATE libSceAgcDriver libc)
set_target_properties(agc_pm4_replay PROPERTIES CXX_EXTENSIONS OFF)
if(MINGW)
target_link_options(agc_pm4_replay PRIVATE -municode)
endif()
configure_windows_unwind(agc_pm4_replay)
if(MINGW)
get_filename_component(replayCompilerDirectory "${CMAKE_CXX_COMPILER}" DIRECTORY)
foreach(replayTool IN ITEMS agc_shader_replay agc_pm4_replay)
add_custom_command(TARGET ${replayTool} POST_BUILD
COMMAND "${CMAKE_COMMAND}" -E copy_if_different
"${replayCompilerDirectory}/libgcc_s_seh-1.dll"
"${replayCompilerDirectory}/libstdc++-6.dll"
"${replayCompilerDirectory}/libwinpthread-1.dll"
"$<TARGET_FILE_DIR:${replayTool}>"
VERBATIM
)
endforeach()
endif()
if(BUILD_TESTING)
add_test_executable(agc_driver_pm4_replay_tests tests/Pm4Replay.cpp tools/Pm4Analysis.cpp)
target_include_directories(agc_driver_pm4_replay_tests PRIVATE ${LIBS_INCLUDE_DIR})
@@ -2658,10 +2956,13 @@ if(BUILD_TESTING)
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_sample_location_grid_fallback COMMAND agc_driver_graphics_tests --sample-location-grid-fallback)
set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES TIMEOUT 30)
add_test(NAME agc_driver_sample_order_planning COMMAND agc_driver_graphics_tests --sample-order)
set_tests_properties(agc_driver_sample_order_planning PROPERTIES TIMEOUT 30)
if(WIN32)
set_tests_properties(agc_compute_descriptor_layout_flags PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_covered_centroid_state PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_sample_location_grid_fallback PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
set_tests_properties(agc_driver_sample_order_planning PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
endif()
add_test(NAME agc_compute_pipeline_capture COMMAND agc_compute_pipeline_capture_tests)
set_tests_properties(agc_compute_pipeline_capture PROPERTIES TIMEOUT 30)
@@ -3118,6 +3419,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);
@@ -3,6 +3,8 @@
#include "prx/libSceAgcDriver/Execution/include/QueueState.hpp"
#include <cstdint>
#include <stdexcept>
#include <string>
namespace AgcDriver::DriverDetail {
@@ -10,6 +12,16 @@ double TraceMs();
std::uint64_t SkippedShader();
class RefusedDraw final : public std::runtime_error {
public:
RefusedDraw(std::uint64_t address, const std::string& what) : std::runtime_error(what), address(address) {}
std::uint64_t address;
};
bool SkipRefusedDraws();
void ReportRefusedDraw(const RefusedDraw& refused);
void require(bool condition, const char* reason);
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset);
@@ -45,6 +45,7 @@ struct DispatchVariant {
std::vector<std::int8_t> runBases;
std::uint64_t relocatableWords = 0;
std::vector<std::pair<std::uint32_t, std::uint32_t>> shaderDataPatches;
std::uint64_t userDataMask = ~std::uint64_t{0};
std::shared_ptr<const ShaderRecompiler::ShaderVertexStageInfo> vertexInfo;
std::shared_ptr<ShaderMemory> memory;
@@ -11,6 +11,7 @@
#include <cstdint>
#include <list>
#include <memory>
#include <utility>
#include <vector>
#include "prx/libc/include/general/AtomicSharedPtr.hpp"
@@ -45,6 +46,9 @@ void ReadUserWords(const QueueState& queue, DrawProgram& program, bool staticAbi
void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32_t pointerBase, bool pointerRequired, bool staticAbi);
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment);
void AppendProgramMemory(std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<DrawProgram>& programs);
std::vector<std::pair<std::size_t, std::size_t>> UserDataWords(const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, std::uint32_t location);
struct PreparedGraphicsStage {
std::shared_ptr<const ShaderSnapshot> snapshot;
PreparedShaders::Entry entry;
@@ -122,6 +126,7 @@ struct StageCapture {
std::uint64_t forgetSerial = 0;
std::uint32_t pushOffset = 0;
ShaderRecompiler::SrtUserDataUse userDataUse;
std::uint64_t userDataMask = ~std::uint64_t{0};
};
struct RelocatableUserWord {
@@ -32,7 +32,7 @@ public:
static void FollowEpoch(std::uint64_t token);
static unsigned Committers();
void Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress = 0, std::vector<std::byte> labelBytes = {});
void Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress = 0, std::vector<std::byte> labelBytes = {}, std::vector<Range> reads = {});
void Drain(DrainReason reason, std::uint32_t opcode = 0x100);
void Stop();
static std::uint64_t DrainRequestsByOpcode(std::uint32_t opcode);
@@ -40,6 +40,8 @@ public:
std::size_t Outstanding() const { return outstanding.load(std::memory_order_acquire); }
bool Overlaps(std::uint64_t address, std::size_t bytes);
std::optional<std::uint64_t> PendingLabel(std::uint64_t address, std::size_t bytes);
static bool HeldReads(std::uint64_t address, std::size_t bytes);
void ReleaseRunningReads();
private:
struct Item {
@@ -47,6 +49,7 @@ private:
std::vector<Range> writes;
std::uint64_t labelAddress = 0;
std::vector<std::byte> labelBytes;
std::vector<Range> reads;
};
DrawPipeline() = default;
void run();
@@ -72,6 +72,7 @@ public:
static void tagVariantRuns(DispatchVariant& variant, std::span<const ShaderRecompiler::MemoryRegion> regions, const ShaderRecompiler::SrtUserDataUse& use, bool eligible, std::uint32_t hiddenWords, std::span<const std::shared_ptr<const ShaderRecompiler::SrtGuestWords>> supplied = {});
static std::string dumpRequest(std::uint64_t address, const ShaderRecompiler::RecompileRequest& request, std::string_view suffix = {});
DrawEntryCounters DrawCacheCounters();
EntryCounters DispatchCacheCounters();
struct DispatchCacheFootprint {
std::size_t entries = 0;
std::uint64_t variantBytes = 0;
@@ -163,7 +164,7 @@ private:
std::shared_ptr<DrawDecode> decodeDraw(const QueueState& queue, const Submission& submission, const Graphics::Context& context);
void resolveDrawDecode(const QueueState& queue, const Submission& submission, const Graphics::Context& context, std::shared_ptr<const DrawDecode>& decode, bool registerKey, std::uint64_t drawKey, bool profile);
void lookupDraw(const Submission& submission, const std::shared_ptr<VulkanDevice>& localDevice, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, bool useDrawEntries, bool registerKey, bool profile, std::uint64_t& drawKey, std::shared_ptr<DrawEntry>& entry, std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool& drawHit, bool& verifyHit, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, const std::shared_ptr<DrawEntry>& dataEntry, DrawDataCandidates& dataCandidates, std::vector<StageDataHit>& stageData);
std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected);
std::shared_ptr<const ShaderRecompiler::RecompileResult> materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics);
static void recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads);
void cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawParameters& drawParameters, const std::optional<Graphics::IndirectDrawPath>& indirectCpu, const std::vector<DrawProgram>& programs, const std::vector<StageCapture>& stageCaptures, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, const std::vector<std::vector<Graphics::DecodeRead>>& decodeReads, bool verifyHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, std::vector<std::shared_ptr<DispatchVariant>>& fresh, std::uint64_t drawKey, bool registerKey, const std::shared_ptr<const DrawDecode>& decode, DrawPhaseTiming& phaseTiming, const std::vector<bool>& reused, const std::shared_ptr<DrawEntry>& entry, std::uint64_t shapeKey);
static bool drawPrecheck();
@@ -371,8 +372,7 @@ private:
std::atomic<std::uint64_t> dataPendingMisses{0};
using ValueSet = std::pair<std::vector<std::pair<std::uint64_t, std::uint64_t>>, std::vector<std::uint32_t>>;
std::unordered_map<std::uint64_t, std::deque<ValueSet>> priorValueSets;
std::unordered_map<std::uint64_t, std::deque<std::uint64_t>> priorValueSets;
std::unordered_map<std::uint64_t, std::shared_ptr<DrawEntry>> drawCache;
std::unordered_map<std::uint64_t, std::uint64_t> drawShapes;
@@ -1,6 +1,7 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DRIVER_DRAWWRITERANGES_HPP
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Shaders.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <cstdint>
@@ -13,6 +14,7 @@ namespace AgcDriver::DriverDetail {
std::vector<std::pair<std::uint64_t, std::uint64_t>> DrawWriteRanges(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> stages, bool exact);
std::vector<std::pair<std::uint64_t, std::uint64_t>> DispatchWriteRanges(const ShaderRecompiler::RecompileResult& compiled, bool& bounded);
bool ExactDrawWrites();
std::vector<std::pair<std::uint64_t, std::uint64_t>> QueuedDrawReadRanges(const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> stages);
}
@@ -23,6 +23,7 @@ std::optional<DwordPatternFill> MatchDwordPatternFill(std::span<const std::uint3
std::optional<DwordPatternFill> MatchConstantDwordFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<DwordPatternFill> MatchClampedGlobalFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<Graphics::MemoryValueFill> MatchMemoryValueBufferFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
std::optional<DwordPatternFill> MatchSgprPatternFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute);
bool IsByteFill(const std::array<std::uint32_t, 4>& pattern);
std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>& pattern, bool coversStorageKeys, bool memoryValueFill);
@@ -30,7 +30,7 @@ enum TrySite { TryLabel = 0, TryFlush, TryReap, TryPoll, TryIdle, TryCapture, Tr
extern std::atomic<std::uint64_t> triesFailed[TrySites];
extern std::atomic<std::uint64_t> storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, dmaFills, copiesToHostMemory;
extern std::atomic<std::uint64_t> storesOnGpu, storesBehindCompletions, storesOnCpu, storesDrained, dmaFills, copiesToHostMemory, copiesBehindPendingSources;
extern std::map<std::uint32_t, std::uint64_t> drainCounts;
@@ -0,0 +1,20 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_FLOAT32FLUSHCONTROL_HPP
#include <vulkan/vulkan.h>
namespace AgcDriver {
struct Float32FlushControl {
bool probe = false;
bool declared = false;
};
inline Float32FlushControl Float32FlushControlFor(VkDriverId driver, VkBool32 flushToZero, VkShaderFloatControlsIndependence independence) {
const bool declared = flushToZero == VK_TRUE && independence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE;
return {declared || driver == VK_DRIVER_ID_MOLTENVK, declared};
}
}
#endif
@@ -64,6 +64,10 @@ void Unwatch(std::uint64_t address, std::size_t bytes);
void Rewatch(std::span<const std::pair<std::uint64_t, std::uint64_t>> ranges);
bool ImportWatched(std::uint64_t address, std::size_t bytes, const std::function<bool()>& import, bool staysWatched = true);
void SetUnwatchHook(void (*hook)(std::uint64_t address, std::size_t bytes));
#ifdef _WIN32
using FreshCollectHook = bool (*)(std::uintptr_t address, std::size_t bytes, void (*written)(void*, std::uintptr_t, std::uintptr_t), void* context);
void SetFreshCollectHook(FreshCollectHook hook);
#endif
bool Watched(std::uint64_t address, std::size_t bytes);
std::uint64_t CollectWrites(std::uint64_t address, std::size_t bytes);
bool UnchangedSince(std::uint64_t address, std::size_t bytes, std::uint64_t generation);
@@ -10,6 +10,10 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <condition_variable>
#include <exception>
#include <functional>
#include <limits>
#include <map>
#include <memory>
#include <mutex>
@@ -18,6 +22,7 @@
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <utility>
#include <vector>
@@ -267,6 +272,18 @@ private:
Clock::duration folded{};
};
struct TraceHost {
std::string cpu;
std::uint32_t threads = 0;
std::uint64_t memoryMiB = 0;
};
struct TraceTitle {
std::string id;
std::string name;
std::string contentVersion;
};
struct TraceSummary {
std::string commit;
std::optional<bool> dirty;
@@ -281,6 +298,135 @@ struct TraceSummary {
double gpuBusyMs = 0;
std::string trace;
std::optional<std::pair<double, double>> interval;
std::optional<TraceHost> host;
std::optional<TraceTitle> title;
};
struct TraceBenchInterval {
double startS = 0;
double endS = 0;
std::uint64_t firstPresent = 0;
std::uint32_t width = 0;
std::uint32_t height = 0;
double gpuBusyMs = 0;
std::vector<std::uint32_t> frameUs;
};
using TraceBenchIntervals = std::vector<std::shared_ptr<const TraceBenchInterval>>;
class TraceBench {
public:
static constexpr std::size_t MaximumIntervals = 240;
static void Keep(TraceBenchIntervals& kept, std::shared_ptr<const TraceBenchInterval> interval, std::uint64_t& dropped) {
kept.push_back(std::move(interval));
if (kept.size() <= MaximumIntervals) return;
const auto excess = kept.size() - MaximumIntervals;
kept.erase(kept.begin(), kept.begin() + static_cast<std::ptrdiff_t>(excess));
dropped += excess;
}
static bool Parse(const char* bench, bool summaryEvery) {
if (bench == nullptr) return false;
if (std::string_view(bench) != "1") throw std::invalid_argument("APS5_TRACE_BENCH=" + std::string(bench) + " is not 1");
if (!summaryEvery) throw std::invalid_argument("APS5_TRACE_BENCH=1 needs APS5_TRACE_SUMMARY_EVERY");
return true;
}
};
struct TraceBenchSnapshot {
TraceSummary summary;
TraceBenchIntervals intervals;
std::uint64_t dropped = 0;
};
class TraceBenchWriter {
public:
explicit TraceBenchWriter(std::function<void(const TraceBenchSnapshot&)> writer) : writer(std::move(writer)) {
if (!this->writer) throw std::invalid_argument("invalid benchmark writer");
worker = std::thread([this] { run(); });
}
TraceBenchWriter(const TraceBenchWriter&) = delete;
TraceBenchWriter& operator=(const TraceBenchWriter&) = delete;
~TraceBenchWriter() {
Stop();
}
void Stop() {
std::lock_guard stopLock(stopMutex);
{
std::lock_guard lock(mutex);
stopping = true;
}
changed.notify_one();
if (worker.joinable()) worker.join();
}
void Write(TraceBenchSnapshot snapshot) {
{
std::lock_guard lock(mutex);
if (failure) std::rethrow_exception(failure);
if (stopping) throw std::runtime_error("benchmark writer is stopping");
if (pending) ++superseded;
pending = std::move(snapshot);
}
changed.notify_one();
}
void Flush() {
std::unique_lock lock(mutex);
idle.wait(lock, [&] { return failure || (!pending && !writing); });
if (failure) std::rethrow_exception(failure);
}
std::uint64_t Superseded() {
std::lock_guard lock(mutex);
return superseded;
}
private:
void run() noexcept {
try {
for (;;) {
TraceBenchSnapshot snapshot;
{
std::unique_lock lock(mutex);
changed.wait(lock, [&] { return stopping || pending.has_value(); });
if (!pending) return;
snapshot = std::move(*pending);
pending.reset();
writing = true;
}
writer(snapshot);
{
std::lock_guard lock(mutex);
writing = false;
}
idle.notify_all();
}
} catch (...) {
{
std::lock_guard lock(mutex);
failure = std::current_exception();
writing = false;
}
idle.notify_all();
}
}
std::function<void(const TraceBenchSnapshot&)> writer;
std::mutex stopMutex;
std::mutex mutex;
std::condition_variable changed;
std::condition_variable idle;
std::optional<TraceBenchSnapshot> pending;
std::uint64_t superseded = 0;
bool writing = false;
bool stopping = false;
std::exception_ptr failure;
std::thread worker;
};
struct TraceGpuRange {
@@ -429,6 +575,7 @@ public:
AppendString(output, summary.driver);
output += ",\"os\":";
AppendString(output, summary.os);
AppendMachine(output, summary);
output += ",\"resolution\":";
AppendString(output, std::to_string(summary.width) + "x" + std::to_string(summary.height));
output += ",\"window\":{\"spec\":";
@@ -502,7 +649,71 @@ public:
return output;
}
static std::vector<std::uint32_t> FrameMicroseconds(std::span<const Clock::time_point> presents) {
std::vector<std::uint32_t> frames;
for (std::size_t index = 1; index < presents.size(); ++index) {
const auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(presents[index] - presents[index - 1]).count();
frames.push_back(static_cast<std::uint32_t>(std::clamp<std::int64_t>(elapsed, 0, std::numeric_limits<std::uint32_t>::max())));
}
return frames;
}
static std::string Bench(const TraceSummary& summary, std::span<const std::shared_ptr<const TraceBenchInterval>> intervals, std::uint64_t intervalsDropped) {
std::string output = "{\"format\":\"aps5-bench/1\",\"commit\":";
AppendString(output, summary.commit);
output += ",\"dirty\":";
output += summary.dirty.has_value() ? (*summary.dirty ? "true" : "false") : "null";
output += ",\"env\":{";
for (std::size_t index = 0; index < summary.environment.size(); ++index) {
if (index != 0) output += ',';
AppendString(output, summary.environment[index].first);
output += ':';
AppendString(output, summary.environment[index].second);
}
output += "},\"gpu\":";
AppendString(output, summary.gpu);
output += ",\"driver\":";
AppendString(output, summary.driver);
output += ",\"os\":";
AppendString(output, summary.os);
AppendMachine(output, summary);
output += ",\"intervals_dropped\":" + std::to_string(intervalsDropped) + ",\"intervals\":[";
for (std::size_t index = 0; index < intervals.size(); ++index) {
const auto& interval = *intervals[index];
if (index != 0) output += ',';
output += "\n{\"start_s\":" + number(interval.startS) + ",\"end_s\":" + number(interval.endS);
output += ",\"first_present\":" + std::to_string(interval.firstPresent);
output += ",\"resolution\":";
AppendString(output, std::to_string(interval.width) + "x" + std::to_string(interval.height));
output += ",\"gpu_busy_ms\":" + number(interval.gpuBusyMs) + ",\"frame_us\":[";
for (std::size_t frame = 0; frame < interval.frameUs.size(); ++frame) {
if (frame != 0) output += ',';
output += std::to_string(interval.frameUs[frame]);
}
output += "]}";
}
output += "\n]}\n";
return output;
}
private:
static void AppendMachine(std::string& output, const TraceSummary& summary) {
if (summary.host) {
output += ",\"cpu\":";
AppendString(output, summary.host->cpu);
output += ",\"cpu_threads\":" + std::to_string(summary.host->threads);
output += ",\"memory_mib\":" + std::to_string(summary.host->memoryMiB);
}
if (summary.title) {
output += ",\"title_id\":";
AppendString(output, summary.title->id);
output += ",\"title\":";
AppendString(output, summary.title->name);
output += ",\"content_version\":";
AppendString(output, summary.title->contentVersion);
}
}
static double microseconds(Clock::duration elapsed) {
return std::chrono::duration<double, std::micro>(elapsed).count();
}
@@ -521,7 +732,7 @@ private:
class PerformanceTrace {
public:
static bool Configured() {
static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr;
static const bool configured = std::getenv("APS5_TRACE_FRAMES") != nullptr || std::getenv("APS5_TRACE_SECONDS") != nullptr || std::getenv("APS5_TRACE_SUMMARY_EVERY") != nullptr || std::getenv("APS5_TRACE_BENCH") != nullptr;
return configured;
}
@@ -89,7 +89,8 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue);
void Execute(std::span<const std::uint32_t> packet, QueueState& queue);
inline bool IndirectRegisterOpcode(std::uint32_t opcode) { return opcode == 0x63 || opcode == 0x64 || opcode == 0x9f; }
std::vector<std::uint32_t> ReadIndirectRegisters(std::span<const std::uint32_t> packet);
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue);
bool IndirectRegistersValid(std::span<const std::uint32_t> pairs);
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue, bool skipInvalid = false);
bool AccessesMemory(std::uint32_t header);
// Whether an ACQUIRE_MEM packet asks only for GPU cache actions (no CPU-visible memory
// synchronization): such a packet needs a pipeline barrier, not a device drain.
@@ -42,7 +42,7 @@ public:
{
std::lock_guard lock(mutex);
if (failure) std::rethrow_exception(failure);
if (stopping) throw std::runtime_error("profile output is stopping");
if (stopping) return false;
if (text.empty()) return true;
if (text.size() > capacity - bytes) {
++dropped;
@@ -69,6 +69,7 @@ public:
} else if (static_cast<std::size_t>(size) > capacity) {
std::lock_guard lock(mutex);
if (failure) std::rethrow_exception(failure);
if (stopping) return;
++dropped;
changed.notify_one();
} else {
@@ -0,0 +1,18 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SCREENSHOT_HPP
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <span>
#include <string_view>
namespace AgcDriver::Screenshot {
void Request();
bool Take();
std::filesystem::path Write(std::string_view titleId, std::uint32_t width, std::uint32_t height, std::span<const std::byte> bgra);
}
#endif
@@ -0,0 +1,14 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPBARRIER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_SUBGROUPBARRIER_HPP
#include <vulkan/vulkan.h>
namespace AgcDriver {
inline bool SubgroupControlBarrierUsable(VkDriverId driver) {
return driver != VK_DRIVER_ID_AMD_PROPRIETARY;
}
}
#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);
};
@@ -11,6 +11,7 @@
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include <array>
#include <chrono>
#include <functional>
#include <memory>
@@ -45,6 +46,7 @@ public:
// Starts the journal replay if the device has not (a headless device starts it at its first
// draw or dispatch) and joins it (tests; the teardown joins it anyway).
void WaitForPipelineJournalReplay();
void SavePipelineCache();
// Compute pipelines this device built, by dispatches and by the journal replay (tests).
std::uint64_t ComputePipelinesCreated() const;
~VulkanDevice();
@@ -54,6 +56,7 @@ public:
std::string DriverInfo() const;
bool CalibrateClock(double& gpuNs, std::chrono::steady_clock::time_point& cpu) const;
ShaderRecompiler::SpirvTarget Target() const;
Graphics::SampleOrder HostSampleOrder(const Graphics::State& graphics) const;
std::optional<ShaderRecompiler::GeometryStageLimits> GeometryLimits() const;
std::uint32_t MaxFragmentInputComponents() const;
const std::uint32_t* WorkGroupCountLimit() const;
@@ -103,6 +106,7 @@ public:
// Pending-label table lookup and open-batch overlap test for WAIT_REG_MEM (see Recorder).
std::optional<Graphics::Recorder::LabelHit> PendingLabel(std::uint64_t address, std::size_t bytes, std::uint64_t afterStamp, Graphics::Recorder::LabelRefusal* refusal = nullptr) const;
bool OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) const;
void SealPassWriteOnly();
bool RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const;
bool StoresPendingOver(std::uint64_t address, std::size_t bytes) const;
// Fills [address, address + bytes) of host-imported guest memory with a repeating 16-byte pattern,
@@ -167,6 +171,7 @@ public:
};
using CopyWriterNote = std::function<void(std::span<const std::byte> value, std::uint64_t generation)>;
CopyOutcome CopyBuffer(std::uint64_t destination, std::uint64_t source, std::size_t bytes, std::size_t cpuMax, std::size_t gpuMax, std::size_t knownMax, std::uint64_t programAddress, std::uint32_t queue, const CopyWriterNote& noteWriter);
bool GpuCopyPossible(std::uint64_t destination, std::uint64_t source, std::size_t bytes) const;
// APS5_COPY_VERIFY=1 totals: CPU copies verified, whose source bytes changed over the sync (a
// producer the rule missed), whose destination bytes were overwritten (a store the rule
// missed), and whose destination an unsignaled batch read in place (the rule's own answer
@@ -272,7 +277,7 @@ public:
int cpuReason;
double argumentReadMs;
};
IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr<PreparedDispatch> prepared = nullptr, std::shared_ptr<const Recipe>* recipe = nullptr);
IndirectOutcome DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::uint64_t programAddress = 0, std::shared_ptr<PreparedDispatch> prepared = nullptr, std::shared_ptr<const Recipe>* recipe = nullptr, const std::array<std::uint32_t, 3>& threadGroup = {});
// A value-equal dispatch-cache hit with a recipe (design_cpu_final M4), in two steps.
// PrepareRecipe, WITHOUT GuestMemory::GpuMutex: the pre-checks (recipes on, the recipe's
// device is this one, its template and pipeline objects still alive), the template's
@@ -294,7 +299,7 @@ public:
// with the recipe's, else the process aborts. `refreshByWords`: the driver's data-only hit
// (`shader` carries live flat-SRT words the recipe's hash does not name), so the template's
// data refresh is decided by the per-word compare instead of the hash.
RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr<RecipeHit>& hit, IndirectOutcome& outcome, const std::shared_ptr<PreparedDispatch>& verify = nullptr, bool refreshByWords = false);
RecipeOutcome DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr<RecipeHit>& hit, IndirectOutcome& outcome, const std::shared_ptr<PreparedDispatch>& verify = nullptr, bool refreshByWords = false, const std::array<std::uint32_t, 3>& threadGroup = {});
// Whether dispatch-cache hits use recipes (APS5_NO_DISPATCH_RECIPE unset) and whether every
// hit is verified (APS5_VERIFY_RECIPE=1).
static bool DispatchRecipes();
@@ -314,6 +319,7 @@ public:
// `recipe`, when given, receives the DrawRecipe a recorded, cacheable, reusable, direct draw
// built for its draw-cache entry (design_cpu_final M8); null otherwise.
void Draw(const Graphics::State& graphics, const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> shaders, std::span<const Graphics::GuestMemorySnapshot> snapshots = {}, std::shared_ptr<const DrawRecipe>* recipe = nullptr);
bool DrawInputsInPlace() const;
std::optional<std::string> KnownDrawRejection(const Graphics::State& graphics, std::span<const Graphics::CompiledShader> shaders) const;
void ColorMetadataPass(const Graphics::ColorMetadataPass& pass);
// A draw-cache hit recorded from its recipe (Graphics::DrawWithRecipe), under the mutex after
@@ -339,6 +345,8 @@ public:
std::uint32_t computeSubgroupSize(const ShaderRecompiler::RecompileResult& shader) const;
VkPipelineShaderStageCreateFlags computeStageFlags(const ShaderRecompiler::RecompileResult& shader) const;
std::shared_ptr<ComputePipelineObjects> createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
bool completionStoresOver(std::uint64_t address, std::size_t bytes) const;
bool copyWaitsOnCompletions(std::uint64_t address, std::size_t bytes) const;
private:
VulkanDevice(const PresentationWindow* window, bool compileOnly);
@@ -347,7 +355,7 @@ private:
ShaderRecompiler::SpirvTarget buildTarget() const;
Graphics::Context buildContext() const;
// Body of Dispatch and DispatchIndirect: `arguments` 0 dispatches x, y, z groups.
IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe);
IndirectOutcome dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe, const std::array<std::uint32_t, 3>& threadGroup = {});
// The stage-A pre-sync over `surfaces` (see PrepareDispatch): the serial waited for, 0 none.
std::uint64_t presync(std::span<const Graphics::PresyncSurface> surfaces);
std::shared_ptr<ComputePipelineObjects> computePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress);
@@ -358,6 +366,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();
@@ -5,6 +5,7 @@ namespace AgcDriver {
// Starts sampling the calling thread when APS5_SAMPLE_WORKER names an output file.
void StartWorkerSampler();
void StartCommitterSampler();
void StopWorkerSampler();
}
@@ -23,7 +23,11 @@ public:
if (bytes == 0 || bytes > size || address < base || address - base > size - bytes) return false;
const auto first = (address - base) / BlockBytes;
const auto last = (address - base + bytes - 1) / BlockBytes;
return std::none_of(excluded.begin() + first, excluded.begin() + last + 1, [](auto generation) { return generation != 0; });
const auto firstPage = (address - base) % BlockBytes / PageBytes;
const auto lastPage = (address - base + bytes - 1) % BlockBytes / PageBytes;
if (first == last) return (excluded[first] & pageMask(firstPage, lastPage + 1)) == 0;
if ((excluded[first] & pageMask(firstPage, PagesPerBlock)) != 0 || (excluded[last] & pageMask(0, lastPage + 1)) != 0) return false;
return std::none_of(excluded.begin() + first + 1, excluded.begin() + last, [](auto mask) { return mask != 0; });
}
bool ExcludedSpan(std::uint64_t address, std::size_t bytes, std::uint64_t* begin, std::uint64_t* end) const {
@@ -40,10 +44,12 @@ public:
if (!excludedBlocks(address, bytes, first, last)) return false;
bool changed = false;
for (auto block = first; block <= last; ++block) {
changed = changed || excluded[block] == 0;
const auto pages = (std::min<std::uint64_t>(BlockBytes, size - block * BlockBytes) + PageBytes - 1) / PageBytes;
const auto mask = pageMask(0, pages);
changed = changed || excluded[block] != mask;
auto& stamp = excludedGenerations[block];
stamp = std::max(stamp, std::max<std::uint64_t>(generation, 1));
excluded[block] = 1;
excluded[block] = mask;
}
return changed;
}
@@ -58,21 +64,28 @@ public:
const auto offset = address - base;
if (offset >= size) return false;
bytes = static_cast<std::size_t>(std::min<std::uint64_t>(bytes, size - offset));
const auto first = offset / BlockBytes + (offset % BlockBytes != 0);
const auto firstPage = offset / PageBytes + (offset % PageBytes != 0);
const auto stop = offset + bytes;
const auto end = stop / BlockBytes + (stop == size && stop % BlockBytes != 0);
const auto endPage = stop / PageBytes + (stop == size && stop % PageBytes != 0);
if (firstPage >= endPage) return false;
const auto first = firstPage / PagesPerBlock;
const auto last = (endPage - 1) / PagesPerBlock;
bool changed = false;
for (auto block = first; block < end; ++block) {
if (excluded[block] != 0 && excludedGenerations.at(block) < generation) {
for (auto block = first; block <= last; ++block) {
const auto mask = pageMask(block == first ? firstPage % PagesPerBlock : 0, block == last ? (endPage - 1) % PagesPerBlock + 1 : PagesPerBlock);
if ((excluded[block] & mask) != 0 && excludedGenerations.at(block) < generation) {
changed = true;
excluded[block] = 0;
excludedGenerations.erase(block);
excluded[block] &= static_cast<std::uint16_t>(~mask);
if (excluded[block] == 0) excludedGenerations.erase(block);
}
}
return changed;
}
private:
static std::uint16_t pageMask(std::size_t first, std::size_t end) {
return static_cast<std::uint16_t>(((1u << end) - 1u) & ~((1u << first) - 1u));
}
bool excludedBlocks(std::uint64_t address, std::size_t bytes, std::size_t& first, std::size_t& last) const {
if (bytes == 0 || size == 0) return false;
if (address < base) {
@@ -88,9 +101,11 @@ private:
return true;
}
static constexpr std::size_t BlockBytes = 65536;
static constexpr std::size_t PageBytes = 4096;
static constexpr std::size_t PagesPerBlock = BlockBytes / PageBytes;
std::uint64_t base = 0;
std::size_t size = 0;
std::vector<std::uint8_t> excluded;
std::vector<std::uint16_t> excluded;
std::unordered_map<std::uint64_t, std::uint64_t> excludedGenerations;
};
@@ -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);
@@ -1,7 +1,11 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include <cstdio>
#include <cstdlib>
#include <mutex>
#include <set>
#include <stdexcept>
#include <string_view>
namespace AgcDriver::DriverDetail {
@@ -15,6 +19,23 @@ std::uint64_t SkippedShader() {
return skipped;
}
bool SkipRefusedDraws() {
static const bool skip = std::getenv("APS5_SKIP_REFUSED_DRAWS") != nullptr;
return skip;
}
void ReportRefusedDraw(const RefusedDraw& refused) {
static std::mutex reportedMutex;
static std::set<std::uint64_t> reported;
{
std::lock_guard lock(reportedMutex);
if (!reported.insert(refused.address).second) return;
}
const std::string_view what = refused.what();
const auto line = what.substr(0, what.find('\n'));
std::fprintf(stderr, "[gpu] APS5_SKIP_REFUSED_DRAWS: skipping the draws that use shader 0x%llx: %.*s\n", static_cast<unsigned long long>(refused.address), static_cast<int>(line.size()), line.data());
}
void require(bool condition, const char* reason) {
if (!condition) {
throw std::runtime_error(std::string("AGC driver: ") + reason);
@@ -5,11 +5,14 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/BufferCopy.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DwordPatternFill.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "Optimization/ResourceProgram.hpp"
#include <algorithm>
#include <cstdlib>
@@ -117,9 +120,14 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
const bool pipelineCandidate = !resolving && !fromAhead && DispatchItemsEnabled() && DrawPipeline::Active() && indirectArguments == 0 && deferredLabels().labels.empty() && !matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) && !MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute);
const bool partialLastGroup = partialGroups != std::array<std::uint32_t, 3>{};
if (resolving) {
if (indirectArguments == 0 && Graphics::MatchMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) return;
if (!partialLastGroup && matchesFillKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute)) return;
if (!partialLastGroup && indirectArguments == 0 && (matchesCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute) || MatchesStreamCopyKernel(std::span(snapshot.code).subspan(codeOffset), userData, compute))) return;
} else {
if (indirectArguments == 0 && Graphics::TakeMultisampledSingleClear(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads)) {
pendingDispatchPhases().outcome = DispatchOutcome::FillHle;
return;
}
if (indirectArguments == 0 && (packet[4] & 0x20u) == 0) {
if (const auto fill = Graphics::MatchMemoryValueFill(std::span(snapshot.code).subspan(codeOffset), userData, compute.numThreads, {packet[1], packet[2], packet[3]})) Graphics::NoteMemoryValueFill(*fill);
}
@@ -145,11 +153,16 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
}
}
if (partialLastGroup) compute.partialThreads = partialGroups;
std::array<std::uint32_t, 3> threadGroup{};
if (indirectArguments != 0 && (packet[4] & 0x20u) != 0) {
for (std::uint32_t axis = 0; axis < 3; ++axis) threadGroup[axis] = std::max(readRegister(queue.shader, 0x207 + axis) & 0xffffu, 1u);
compute.partialThreads = {~0u, ~0u, ~0u};
}
ShaderRecompiler::RecompileRequest request{
{ShaderRecompiler::ShaderStage::Compute, address, std::span(snapshot.code).subspan(codeOffset), snapshot.headerAddress, snapshot.header},
{(packet[4] & 0x8000u) != 0 ? 32u : 64u, 0, userData, compute, std::nullopt, std::nullopt, memory, DispatchFloatMode(snapshot, queue.shader)},
localDevice->ComputeTarget((packet[4] & 0x8000u) != 0 ? 32u : 64u),
{0, 0, 0, 128}
{0, 0, 0, threadGroup != std::array<std::uint32_t, 3>{} ? 0u : 128u}
};
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
static double captureMs = 0, keyMs = 0, recompileMs = 0, deviceMs = 0;
@@ -357,7 +370,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);
@@ -382,8 +401,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;
@@ -423,7 +442,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
words += text;
}
}
std::fprintf(stderr, "[dispatch-io] shader 0x%llx%s\n", static_cast<unsigned long long>(address), words.c_str());
std::fprintf(stderr, "[dispatch-io] queue=0x%x submission=%llu shader 0x%llx%s\n", submission.queue, static_cast<unsigned long long>(submission.serial), static_cast<unsigned long long>(address), words.c_str());
}
const auto rethrow = [&](const std::exception& error) {
@@ -443,7 +462,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;
@@ -506,6 +525,8 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
auto* const attachTo = stampValidate() ? nullptr : keepVariant != nullptr ? keepVariant.get() : attachVariant.get();
bool writersNoted = false;
const bool noteWrites = writeEvidenceEnabled() || traceCapSync();
const auto keysFill = indirectArguments == 0 && !partialLastGroup ? MatchSgprPatternFill(packet, std::span(snapshot.code).subspan(codeOffset), userData, compute) : std::nullopt;
const auto filledKeys = keysFill ? KeysFillMemo(keysFill->pattern, false, true) : std::nullopt;
std::optional<Graphics::DepthPlaneClear> depthPlaneClear;
if (indirectArguments == 0 && !partialLastGroup) {
@@ -557,10 +578,12 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
writersNoted = true;
}
phaseTiming.Phase(PhaseNoteWriters);
auto* const keysRecorder = filledKeys ? Graphics::Recorder::Active() : nullptr;
const auto keysNoteBefore = keysRecorder != nullptr ? keysRecorder->LastWriteNote(keysFill->base, keysFill->bytes) : 0;
try {
if (recipeHit != nullptr) {
VulkanDevice::IndirectOutcome outcome{0, 0};
const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit);
const auto result = localDevice->DispatchRecipe(compiled, groups[0], groups[1], groups[2], indirectArguments, address, recipeHit, outcome, VulkanDevice::VerifyRecipes() ? prepared : nullptr, dataHit, threadGroup);
if (result == RecipeOutcome::Rebuild) {
recipeHit = nullptr;
@@ -569,7 +592,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
}
if (indirectArguments != 0) countIndirect(outcome.cpuReason, outcome.argumentReadMs);
} else if (indirectArguments != 0) {
const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr);
const auto outcome = localDevice->DispatchIndirect(compiled, indirectArguments, snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr, threadGroup);
countIndirect(outcome.cpuReason, outcome.argumentReadMs);
} else {
localDevice->Dispatch(compiled, groups[0], groups[1], groups[2], snapshots, address, std::move(prepared), attachTo != nullptr ? &builtRecipe : nullptr);
@@ -577,6 +600,10 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
} catch (const std::exception& error) {
if (rethrow(error)) return;
}
if (keysRecorder != nullptr && keysRecorder == Graphics::Recorder::Active()) {
const auto note = keysRecorder->LastWriteNote(keysFill->base, keysFill->bytes);
if (note != 0 && note != keysNoteBefore) Graphics::NoteKeysFillOnGpu(keysFill->base, keysFill->bytes, *filledKeys);
}
if (builtRecipe != nullptr) {
if (dataHit) {
@@ -54,6 +54,11 @@ std::uint64_t Driver::DispatchCacheBytes() {
return bytes;
}
EntryCounters Driver::DispatchCacheCounters() {
std::lock_guard lock(dispatchCacheMutex);
return entryCounters;
}
Driver::DispatchCacheFootprint Driver::DispatchCacheSize() {
std::lock_guard lock(dispatchCacheMutex);
return {dispatchCache.size(), dispatchCacheVariantBytes};
@@ -7,9 +7,24 @@ namespace AgcDriver::DriverDetail {
void Driver::classifyDiffering(std::uint64_t program, std::uint64_t key, const DispatchVariant& old, const DispatchVariant& fresh, const ShaderRecompiler::ResourceCapture* capture, EntryCounters& counters) {
++counters.differingClassified;
++counters.differingByProgram[program];
const auto valueSet = [](const DispatchVariant& variant) {
std::uint64_t hash = 0xcbf29ce484222325ull;
const auto mix = [&](std::uint64_t value) {
hash ^= value;
hash *= 0x100000001b3ull;
};
mix(variant.runs.size());
for (const auto& [begin, end] : variant.runs) {
mix(begin);
mix(end);
}
mix(variant.words.size());
for (const auto word : variant.words) mix(word);
return hash;
};
auto& ring = priorValueSets[key];
if (std::any_of(ring.begin(), ring.end(), [&](const ValueSet& set) { return set.first == fresh.runs && set.second == fresh.words; })) ++counters.differingMatchedPrior;
ring.emplace_front(old.runs, old.words);
if (std::find(ring.begin(), ring.end(), valueSet(fresh)) != ring.end()) ++counters.differingMatchedPrior;
ring.push_front(valueSet(old));
while (ring.size() > 3) ring.pop_back();
if (priorValueSets.size() > dispatchCacheEntries()) priorValueSets.clear();
if (old.runs != fresh.runs || old.words.size() != fresh.words.size()) {
@@ -48,7 +48,8 @@ void Driver::countIndirect(int path, double readMs) {
int Driver::indirectArgumentsPath(std::span<const std::uint32_t> packet, std::uint64_t arguments) {
static const bool gpuIndirect = std::getenv("APS5_NO_GPU_INDIRECT") == nullptr;
if (!gpuIndirect) return IndirectDisabled;
if ((packet.back() & 0x20u) != 0) return IndirectThreadDimensions;
static const bool gpuThreadDimensions = std::getenv("APS5_NO_GPU_THREAD_DIMENSIONS") == nullptr;
if ((packet.back() & 0x20u) != 0 && !gpuThreadDimensions) return IndirectThreadDimensions;
if (arguments % 4 != 0) return IndirectMisaligned;
return IndirectGpu;
}
@@ -2,6 +2,7 @@
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawProgramTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Memory/DrawWriteRanges.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Synchronization/DeferredLabels.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
@@ -36,7 +37,16 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
};
auto drawParameters = Pm4::ResolveDraw(packet, queue);
const bool pipelined = DrawPipeline::Active();
if (pipelined && drawParameters.indirect) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Indirect);
static const bool pipelinedIndirect = std::getenv("APS5_NO_PIPELINED_INDIRECT") == nullptr;
if (pipelined && drawParameters.indirect) {
const auto& indirect = *drawParameters.indirect;
auto& pipeline = DrawPipeline::Queue0();
const bool overlaps = pipeline.Busy() && (pipeline.Overlaps(indirect.arguments, static_cast<std::size_t>(std::max<std::uint64_t>(indirect.RangeBytes(), indirect.recordBytes))) || (indirect.countIndirect && pipeline.Overlaps(indirect.countAddress, 4)));
if (!pipelinedIndirect || overlaps) pipeline.Drain(DrawPipeline::DrainReason::Indirect);
}
const auto drainBeforeInline = [&] {
if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Drain(DrawPipeline::DrainReason::Indirect);
};
bool traceIndirect = false;
if (const auto verdict = precheckDraw(queue, submission, packet, drawParameters, rejected, traceIndirect)) return *verdict;
phaseTiming.Phase(DrawRowPrecheck);
@@ -206,13 +216,9 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
phaseTiming.Phase(DrawRowDecode);
const auto locate = [&](std::uint32_t location) -> std::optional<std::pair<std::size_t, std::size_t>> {
if (location == 0x280u) return std::nullopt;
for (std::size_t i = 0; i < programs.size(); ++i) {
if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue;
const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr);
if (word < programs[i].userData.size()) return std::make_pair(i, static_cast<std::size_t>(word));
}
return std::nullopt;
const auto words = UserDataWords(programs, roles, location);
if (words.empty()) return std::nullopt;
return words.front();
};
if (drawParameters.indirect) {
auto& indirect = *drawParameters.indirect;
@@ -227,13 +233,11 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
}
std::vector<ShaderRecompiler::MemoryRegion> memory;
memory.reserve(2 * programs.size() + 32);
AppendProgramMemory(memory, programs);
std::vector<ShaderRecompiler::LinkedProgram> linked;
linked.reserve(programs.size());
for (std::size_t i = 0; i < programs.size(); ++i) {
const auto& program = programs[i];
for (const auto& region : program.memory) {
if (!region.bytes.empty()) memory.push_back(region);
}
linked.push_back({roles[i], program.binary, program.userDataBase, program.firstUserSgpr, program.userData});
}
timing.Mark("prepare");
@@ -370,6 +374,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
};
std::optional<Graphics::IndirectDrawPath> indirectCpu;
std::vector<std::uint32_t> pushOffsets(programs.size(), 0);
std::optional<ShaderRecompiler::MeshBarycentricLayout> meshBarycentrics;
std::vector<std::size_t> resultIndex(programs.size(), 0);
for (std::size_t i = 0; i < programs.size(); ++i) {
if (roles[i] == Role::GeometryBack) continue;
@@ -397,7 +402,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
break;
}
}
compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected));
compiledStages.push_back(materializeDrawStage(i, pushCursorBytes, queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[i] = compiledStages.back().get();
if (candidate != dataCandidates[i].end()) verifyDataStage(i, *candidate->first);
@@ -461,7 +466,21 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
phaseTiming.Phase(DrawRowRectList);
};
if (graphics.rectList) buildRectList();
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active && graphics.stages.path == Graphics::ShaderPath::Geometry) {
require(localDevice->Target().mesh.has_value(), "a pixel shader on the mesh path reads barycentrics, and this device has neither VK_KHR_fragment_shader_barycentric nor mesh shaders (VK_EXT_mesh_shader), so they cannot be provided or emulated");
require(!graphics.rectList && graphics.stages.mesh && programs.size() == 2 && programResults[0] != nullptr && programResults[1] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics needs exactly one mesh program before it to emulate them");
meshBarycentrics = ShaderRecompiler::MeshBarycentricLayoutOf(*programResults[1]);
if (programResults[0]->meshBarycentrics != meshBarycentrics) {
const auto pushBytes = programResults[0]->pushConstants.size();
const auto exports = programResults[0]->parameterExports;
compiledStages.push_back(materializeDrawStage(0, pushOffsets[0], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics));
if (!rejected.empty()) return DrawVerdict::Rejected;
programResults[0] = compiledStages.back().get();
stages[0].program = programResults[0];
require(programResults[0]->pushConstants.size() == pushBytes && programResults[0]->parameterExports == exports, "pairing the mesh program with its pixel shader changed the push constants or the parameter exports the pixel shader was built for");
}
require(programResults[0]->meshBarycentrics == meshBarycentrics, "the mesh program was compiled for a different barycentric layout than its pixel shader");
} else if (!programs.empty() && programResults.back() != nullptr && programResults.back()->barycentricEmulation.active) {
require(!graphics.rectList && graphics.stages.path == Graphics::ShaderPath::Vertex && programs.size() == 2 && programResults[0] != nullptr && stages.size() == 2, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric needs a vertex shader before it");
const auto geometryLimits = localDevice->GeometryLimits();
if (geometryLimits || !localDevice->Target().tessellation) {
@@ -469,7 +488,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
stages.insert(stages.begin() + 1, Graphics::CompiledShader{Stage::Geometry, &results.back(), 0});
} else {
require(graphics.topology == VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST && !graphics.primitiveRestart, "a pixel shader that reads barycentrics without VK_KHR_fragment_shader_barycentric or geometry shaders needs a triangle list without primitive restart");
auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents());
auto patches = ShaderRecompiler::BuildBarycentricTessellationShaders(*programResults[0], *programResults[1], localDevice->Target(), localDevice->MaxFragmentInputComponents(), graphics.provokingVertexMode == VK_PROVOKING_VERTEX_MODE_LAST_VERTEX_EXT ? 2u : 0u);
const auto first = results.size();
results.push_back(std::move(patches.control));
results.push_back(std::move(patches.evaluation));
@@ -519,22 +538,28 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
std::vector<Pm4::DrawArguments> records;
for (std::uint32_t record = 0; record < count; ++record) records.push_back(Pm4::ReadDrawArguments(indirect, record));
Graphics::CountIndirectDraw(*indirectCpu, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - readStart).count());
const auto baseVertexWord = locate(indirect.baseVertexLocation);
const auto startInstanceWord = locate(indirect.startInstanceLocation);
const auto drawIndexWord = indirect.drawIndexEnabled ? locate(indirect.drawIndexLocation) : std::nullopt;
const auto baseVertexWords = UserDataWords(programs, roles, indirect.baseVertexLocation);
const auto startInstanceWords = UserDataWords(programs, roles, indirect.startInstanceLocation);
const auto drawIndexWords = indirect.drawIndexEnabled ? UserDataWords(programs, roles, indirect.drawIndexLocation) : std::vector<std::pair<std::size_t, std::size_t>>{};
const auto drawable = [&](const Pm4::DrawArguments& arguments) { return arguments.count != 0 && arguments.instances != 0 && (!drawParameters.indexed || arguments.firstVertexOrIndex < drawParameters.indexCount); };
std::size_t lastDrawable = records.size();
for (std::size_t record = 0; record < records.size(); ++record) {
if (drawable(records[record])) lastDrawable = record;
}
for (std::uint32_t record = 0; record < records.size(); ++record) {
const auto& arguments = records[record];
if (traceIndirect) std::fprintf(stderr, "[draw] record %u: count %u instances %u first %u vertexOffset %u startInstance %u\n", record, arguments.count, arguments.instances, arguments.firstVertexOrIndex, arguments.vertexOffset, arguments.firstInstance);
if (arguments.count == 0 || arguments.instances == 0) continue;
std::set<std::size_t> patched;
const auto patch = [&](const std::optional<std::pair<std::size_t, std::size_t>>& word, std::uint32_t value) {
if (!word) return;
writablePrograms[word->first].userData[word->second] = value;
patched.insert(word->first);
const auto patch = [&](const std::vector<std::pair<std::size_t, std::size_t>>& words, std::uint32_t value) {
for (const auto& [program, word] : words) {
writablePrograms[program].userData[word] = value;
patched.insert(program);
}
};
patch(baseVertexWord, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex);
patch(startInstanceWord, arguments.firstInstance);
patch(drawIndexWord, record);
patch(baseVertexWords, indirect.recordBytes == 20 ? arguments.vertexOffset : arguments.firstVertexOrIndex);
patch(startInstanceWords, arguments.firstInstance);
patch(drawIndexWords, record);
if (graphics.stages.tessellation) {
for (std::size_t i = 0; i < programs.size(); ++i) {
if (programs[i].binary.stage == Stage::TessellationControl || programs[i].binary.stage == Stage::TessellationEvaluation) patched.insert(i);
@@ -558,7 +583,7 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
auto& result = results[resultIndex[programIndex]];
const auto pushBytes = result.pushConstants.size();
decodeVertexInfo(programIndex);
result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected);
result = *materializeDrawStage(programIndex, pushOffsets[programIndex], queue, submission, programs, graphics, pixel, vertexInfos, memory, linked, programs[programIndex].binary.stage == Stage::TessellationControl || programs[programIndex].binary.stage == Stage::TessellationEvaluation ? direct : drawParameters, localDevice, *shaderMemory, stageCaptures, recompiled, drawHit, matched, matchedRegions, profile, dumpTarget, dumpSlot1, captures, phaseTiming, phaseMs, rejected, meshBarycentrics);
if (!rejected.empty()) return DrawVerdict::Rejected;
require(result.pushConstants.size() == pushBytes, "patched program changed its push constant layout");
}
@@ -569,6 +594,24 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
rejected = std::move(*known);
return DrawVerdict::Rejected;
}
if (pipelined && pipelinedIndirect && record == lastDrawable && !gpuLock.owns_lock() && deferredLabels().labels.empty()) {
phaseTiming.Phase(DrawRowVectors);
auto writes = drawWriteRanges(graphics, stages);
const auto commitQueue = submission.queue;
const auto commitPacket = (packet[0] >> 8u) & 0xffu;
const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed);
auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), direct, stages = std::move(stages), snapshots = std::move(snapshots), commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] {
DrawPipeline::FollowEpoch(epoch);
GuestMemory::SetCurrentPacket(commitPacket, commitQueue);
commitDraw(localDevice, commitQueue, decode->state, direct, stages, snapshots, nullptr, {}, 0);
};
auto held = std::make_shared<decltype(commit)>(std::move(commit));
DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes));
phaseTiming.Phase(DrawRowLockWait);
timing.Mark("draw_enqueued");
return drawn();
}
drainBeforeInline();
lockForDraw();
noteDrawWriters(stages, submission.queue);
phaseTiming.Phase(DrawRowVectors);
@@ -602,17 +645,19 @@ DrawVerdict Driver::draw(QueueState& queue, std::span<const std::uint32_t> packe
const auto commitQueue = submission.queue;
const auto commitPacket = (packet[0] >> 8u) & 0xffu;
const auto epoch = DrawPipeline::EpochToken().load(std::memory_order_relaxed);
auto reads = QueuedDrawReadRanges(drawParameters, stages);
auto commit = [this, localDevice = std::move(localDevice), decode = std::move(decode), drawParameters, stages = std::move(stages), snapshots = std::move(snapshots), recipe = std::move(recipe), recipeStages = std::move(recipeStages), drawKey, commitQueue, commitPacket, epoch, programs = std::move(programs), results = std::move(results), memory = std::move(memory), linked = std::move(linked), stageCaptures = std::move(stageCaptures), matched = std::move(matched), matchedRegions = std::move(matchedRegions), fresh = std::move(fresh), decodeReads = std::move(decodeReads), shaderMemory = std::move(shaderMemory), entry = std::move(entry), dataEntry = std::move(dataEntry), compiledStages = std::move(compiledStages), stageData = std::move(stageData)] {
DrawPipeline::FollowEpoch(epoch);
GuestMemory::SetCurrentPacket(commitPacket, commitQueue);
commitDraw(localDevice, commitQueue, decode->state, drawParameters, stages, snapshots, recipe, recipeStages, drawKey);
};
auto held = std::make_shared<decltype(commit)>(std::move(commit));
DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes));
DrawPipeline::Queue0().Enqueue([held] { (*held)(); }, std::move(writes), 0, {}, std::move(reads));
phaseTiming.Phase(DrawRowLockWait);
timing.Mark("draw_enqueued");
return drawn();
}
if (drawParameters.indirect) drainBeforeInline();
lockForDraw();
noteDrawWriters(stages, submission.queue);
phaseTiming.Phase(DrawRowVectors);
@@ -8,12 +8,13 @@
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <span>
#include <sstream>
#include <stdexcept>
namespace AgcDriver::DriverDetail {
std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected) {
std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDrawStage(std::size_t i, std::uint32_t pushOffset, const QueueState& queue, const Submission& submission, const std::vector<DrawProgram>& programs, const Graphics::State& graphics, const ShaderRecompiler::ShaderPixelStageInfo& pixel, const std::vector<std::optional<ShaderRecompiler::ShaderVertexStageInfo>>& vertexInfos, std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<ShaderRecompiler::LinkedProgram>& linked, const Pm4::DrawParameters& drawParameters, const std::shared_ptr<VulkanDevice>& localDevice, ShaderMemory& shaderMemory, std::vector<StageCapture>& stageCaptures, std::vector<bool>& recompiled, bool drawHit, const std::vector<std::shared_ptr<DispatchVariant>>& matched, const std::vector<std::vector<ShaderRecompiler::MemoryRegion>>& matchedRegions, bool profile, std::uint64_t dumpTarget, std::uint64_t dumpSlot1, std::uint64_t& captures, DrawPhaseTiming& phaseTiming, std::array<double, DrawDriverPhaseCount>& phaseMs, std::string& rejected, const std::optional<ShaderRecompiler::MeshBarycentricLayout>& meshBarycentrics) try {
PerformanceTimer timing("Shader.DrawStage");
using Stage = ShaderRecompiler::ShaderStage;
phaseTiming.Phase(DrawRowVectors);
@@ -24,7 +25,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
{waveSize, program.firstUserSgpr, program.userData, std::nullopt, program.binary.stage == Stage::Fragment ? std::optional(pixel) : std::nullopt, vertexInfos[i], memory, RegisteredFloatMode(*program.snapshot)},
localDevice->Target(),
{0, 0, pushOffset, (graphics.stages.mesh ? ShaderRecompiler::MeshDrawPushOffsetBytes : graphics.stages.tessellation && pushOffset < Graphics::PipelinePushSlotBytes ? ShaderRecompiler::TessellationLevelPushOffsetBytes : graphics.depthRemap.mode != 0u ? ShaderRecompiler::DepthRemapPushOffsetBytes : Graphics::PipelinePushSlotBytes) - pushOffset % Graphics::PipelinePushSlotBytes},
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}}
ShaderRecompiler::GraphicsCompileContext{program.firstUserSgpr, linked, graphics.stages.mesh, graphics.stages.tessellation, {drawParameters.indexAddress, drawParameters.indexCount, drawParameters.indexSize, drawParameters.instanceCount}, program.binary.stage == Stage::Mesh ? meshBarycentrics : std::nullopt}
};
const auto waitedBefore = traceCapSync() || profile ? Graphics::Recorder::ThreadWaitedMs() : 0.0;
auto invocation = InvocationFor(*program.snapshot, program.codeOffset, request);
@@ -32,12 +33,14 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
auto& stageCapture = stageCaptures[i];
stageCapture.forgetSerial = GuestMemory::ForgetSerial();
stageCapture.pushOffset = pushOffset;
const auto named = [&](const std::exception& error) {
const auto named = [&](const std::exception& error, std::span<const ShaderRecompiler::MemoryRegion> regions) {
static const bool dumpShaders = std::getenv("APS5_DUMP_SHADERS") != nullptr;
std::string dump;
if (dumpShaders) {
try {
dump = dumpRequest(program.binary.codeAddress, request);
auto failed = request;
failed.context.memory = regions;
dump = dumpRequest(program.binary.codeAddress, failed, "_failed");
} catch (...) {}
}
std::ostringstream where;
@@ -51,7 +54,7 @@ std::shared_ptr<const ShaderRecompiler::RecompileResult> Driver::materializeDraw
try {
return CaptureInvocation(shaderMemory, *program.snapshot, program.codeOffset, request, invocation);
} catch (const std::exception& error) {
throw named(error);
throw named(error, shaderMemory.Regions());
}
}();
@@ -99,12 +102,23 @@ 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");
const auto walked = capture->userDataUse.asValue | capture->userDataUse.asBase;
auto mask = walked == 0 ? ~std::uint64_t{0} : walked | stageCapture.compiled->userDataRegisterReads;
if (vertexInfos[i]) {
for (const auto reg : {static_cast<std::uint32_t>(vertexInfos[i]->fetchAttribReg), static_cast<std::uint32_t>(vertexInfos[i]->fetchBufferReg)}) {
mask |= reg + 1u < 64u ? std::uint64_t{3} << reg : ~std::uint64_t{0};
}
}
stageCapture.userDataMask = mask;
phaseTiming.Phase(DrawRowRecompile);
return stageCapture.compiled;
} catch (const std::exception& error) {
if (!SkipRefusedDraws()) throw;
throw RefusedDraw(programs[i].binary.codeAddress, error.what());
}
void Driver::recordStageDataPositions(DispatchVariant& variant, const ShaderRecompiler::ResourceCapture& capture, const std::vector<Graphics::DecodeRead>& decodeReads) {
@@ -157,6 +171,7 @@ void Driver::cacheDrawStages(bool useDrawEntries, bool drawHit, const Pm4::DrawP
variant->shader = programs[i].snapshot;
variant->forgetSerial = stageCapture.forgetSerial;
variant->pushOffset = stageCapture.pushOffset;
variant->userDataMask = stageCapture.userDataMask;
if (vertexInfos[i]) variant->vertexInfo = std::make_shared<const ShaderRecompiler::ShaderVertexStageInfo>(*vertexInfos[i]);
std::vector<ShaderRecompiler::MemoryRegion> regions(stageCapture.regions.begin(), stageCapture.regions.end());
@@ -46,11 +46,34 @@ void InitializeMerged(const QueueState& queue, DrawProgram& program, std::uint32
const auto address = static_cast<std::uint64_t>(low) | (static_cast<std::uint64_t>(high) << 32u);
require(address != 0 || !pointerRequired, "merged shader user-data address is null");
if (address == 0) return;
GuestMemory::CheckRange(reinterpret_cast<const void*>(address), 8, 4);
if (pointerRequired) GuestMemory::CheckRange(reinterpret_cast<const void*>(address), 8, 4);
program.userData[0] = low;
program.userData[1] = high;
}
void AppendProgramMemory(std::vector<ShaderRecompiler::MemoryRegion>& memory, const std::vector<DrawProgram>& programs) {
for (const auto& program : programs) {
for (const auto& region : program.memory) {
if (region.bytes.empty()) continue;
const bool listed = std::any_of(memory.begin(), memory.end(), [&](const ShaderRecompiler::MemoryRegion& known) { return known.guestAddress == region.guestAddress && known.bytes.data() == region.bytes.data() && known.bytes.size() == region.bytes.size(); });
if (!listed) memory.push_back(region);
}
}
}
std::vector<std::pair<std::size_t, std::size_t>> UserDataWords(const std::vector<DrawProgram>& programs, const std::vector<ShaderRecompiler::ProgramRole>& roles, std::uint32_t location) {
using Role = ShaderRecompiler::ProgramRole;
require(roles.size() == programs.size(), "draw program roles are incomplete");
std::vector<std::pair<std::size_t, std::size_t>> words;
if (location == 0x280u) return words;
for (std::size_t i = 0; i < programs.size(); ++i) {
if (roles[i] == Role::Fragment || roles[i] == Role::GeometryBack || location < programs[i].userDataBase) continue;
const auto word = location - programs[i].userDataBase + (8u - programs[i].firstUserSgpr);
if (word < programs[i].userData.size()) words.emplace_back(i, static_cast<std::size_t>(word));
}
return words;
}
void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const ShaderRegistry& registry, bool staticAbi, bool includeFragment) {
using Stage = ShaderRecompiler::ShaderStage;
using Role = ShaderRecompiler::ProgramRole;
@@ -95,11 +118,21 @@ void DecodeGraphicsPrograms(DrawDecode& decoded, const QueueState& queue, const
programs.push_back(prepare(programAddress(base), type, stage, resources, users));
roles.push_back(role);
};
const auto& graphics = product->state;
auto& graphics = product->state;
if (graphics.stages.path == Graphics::ShaderPath::Tessellation) {
if (!staticAbi || programAddress(0x148) != 0) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local);
append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull);
InitializeMerged(queue, programs.back(), 0x102, true, staticAbi);
const auto localAddress = programAddress(0x148);
const auto local = !staticAbi || localAddress != 0 ? ProgramSnapshot(registry, localAddress, {3, 5}) : nullptr;
const bool mergedHull = local != nullptr && !local->header.empty() && local->type == 3;
require(graphics.stages.tessellation.has_value(), "tessellation draw has no tessellation configuration");
graphics.stages.tessellation->mergedHull = mergedHull;
if (mergedHull) {
append(0x148, 3, Stage::Local, 0x10b, 0x10c, Role::Local);
append(0x148, 3, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull);
} else {
if (local != nullptr) append(0x148, 5, Stage::Local, 0x10b, 0x10c, Role::Local);
append(0x108, 7, Stage::TessellationControl, 0x10b, 0x10c, Role::Hull);
}
InitializeMerged(queue, programs.back(), 0x102, !mergedHull, staticAbi);
append(0x0c8, 2, Stage::TessellationEvaluation, 0x08b, 0x08c, Role::Domain);
} else if (graphics.stages.path == Graphics::ShaderPath::Geometry) {
const auto frontAddress = programAddress(0xc8);
@@ -134,7 +167,11 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
product->pixel.reversedBlend = product->state.reversedBlend;
product->pixel.alphaToCoverage = product->state.alphaToCoverage;
product->pixel.quadPixelMask = Graphics::QuadPixelMask(product->state);
product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state);
if (product->state.sampleLocationGrid) {
const auto localDevice = device.Load();
if (localDevice != nullptr) product->pixel.sampleOrder = localDevice->HostSampleOrder(product->state);
}
product->pixel.quadSampleMask = Graphics::QuadSampleMasks(product->state, product->pixel.sampleOrder);
if (product->pixel.stencilRefExportEnable && Graphics::StencilReferenceUnused(product->state)) {
const auto localDevice = device.Load();
const auto capabilities = localDevice == nullptr ? std::vector<std::uint32_t>{} : localDevice->Target().supportedCapabilities;
@@ -28,6 +28,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
if (graphics.stages.tessellation) {
const auto& tess = *graphics.stages.tessellation;
for (const auto value : {tess.inputControlPoints, tess.outputControlPoints, tess.domain, tess.partitioning, tess.outputTopology, tess.patchesPerGroup, tess.primitiveGroupSize, static_cast<std::uint32_t>(tess.breakAtInstance)}) mix(value);
mix(tess.mergedHull);
}
mix(graphics.rectList);
mix(programs.size());
@@ -67,6 +68,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(static_cast<std::uint64_t>(pixel.conservativeZExport));
mix(pixel.orderedPixelShader);
mix(pixel.stencilRefExportEnable);
mix(pixel.stencilOperationValueExport);
for (const auto value : pixel.targetOutputMode) mix(value);
for (const auto value : pixel.targetExportMapping) mix(value);
for (const auto value : pixel.targetExportPacking) mix(static_cast<std::uint64_t>(value));
@@ -80,6 +82,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
mix(pixel.iteratesSamples);
mix(static_cast<std::uint64_t>(pixel.positionLocation));
for (const auto location : pixel.sampleLocations) mix(location);
for (const auto order : pixel.sampleOrder) mix(order);
std::lock_guard cacheLock(drawCacheMutex);
++drawEntryCounters.lookups;
const auto found = drawCache.find(drawKey);
@@ -213,9 +216,16 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr<Vulk
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::DrawCache);
std::optional<SampledReadScope> sampling;
for (std::size_t i = 0; i < programs.size(); ++i) {
if (roles[i] == Role::GeometryBack || programs[i].userData != dataEntry->decode->programs[i].userData) continue;
if (roles[i] == Role::GeometryBack || programs[i].userData.size() != dataEntry->decode->programs[i].userData.size()) continue;
const auto& now = programs[i].userData;
const auto& was = dataEntry->decode->programs[i].userData;
auto& candidates = dataCandidates[i];
for (const auto& variant : dataEntry->stages[i]) {
bool usedEqual = true;
for (std::size_t w = 0; w < now.size() && usedEqual; ++w) {
if (now[w] != was[w] && (w >= 64u || ((variant->userDataMask >> w) & 1u) != 0)) usedEqual = false;
}
if (!usedEqual) continue;
if ((i == 0 && variant->pushOffset != 0) || std::any_of(candidates.begin(), candidates.end(), [&](const auto& candidate) { return candidate.first->pushOffset == variant->pushOffset; })) continue;
std::vector<ShaderRecompiler::MemoryRegion> regions;
appendEntryRegions(*variant, regions);
@@ -3,6 +3,7 @@
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
#include "prx/libSceAgcDriver/Execution/include/HandoffSpin.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTrace.hpp"
#include "prx/libSceAgcDriver/Execution/include/WorkerSampler.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
@@ -22,6 +23,10 @@ std::uint64_t elapsedNs(std::chrono::steady_clock::time_point since) {
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - since).count());
}
std::atomic<std::uint64_t> heldReadRanges{0};
std::atomic<std::uint64_t> heldReadQueries{0};
std::atomic<std::uint64_t> heldReadHits{0};
}
DrawPipeline& DrawPipeline::Queue0() {
@@ -73,7 +78,7 @@ void DrawPipeline::rethrowFailure() {
std::rethrow_exception(error);
}
void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress, std::vector<std::byte> labelBytes) {
void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64_t labelAddress, std::vector<std::byte> labelBytes, std::vector<Range> reads) {
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
std::unique_lock lock(mutex);
rethrowFailure();
@@ -93,7 +98,8 @@ void DrawPipeline::Enqueue(Commit commit, std::vector<Range> writes, std::uint64
}
depthSum += items.size();
++enqueued;
items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes)});
heldReadRanges.fetch_add(reads.size(), std::memory_order_relaxed);
items.push_back({std::move(commit), std::move(writes), labelAddress, std::move(labelBytes), std::move(reads)});
outstanding.fetch_add(1, std::memory_order_release);
if (!thread.joinable()) {
thread = std::thread([this] { run(); });
@@ -126,7 +132,10 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) {
const auto perCommit = commits == 0 ? 0.0 : static_cast<double>(commitNs) / 1e3 / static_cast<double>(commits);
const auto perDraw = enqueued == 0 ? 0.0 : static_cast<double>(fullWaitNs) / 1e3 / static_cast<double>(enqueued);
const auto queued = enqueued == 0 ? 0.0 : static_cast<double>(depthSum) / static_cast<double>(enqueued);
std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room; drains:%s\n", Depth(), static_cast<unsigned long long>(commits), perCommit, static_cast<unsigned long long>(commitErrors), static_cast<unsigned long long>(enqueued), queued, perDraw, drainText.c_str());
char reads[64];
std::snprintf(reads, sizeof(reads), " %llu read ranges, %llu stores held", static_cast<unsigned long long>(heldReadRanges.load(std::memory_order_relaxed)), static_cast<unsigned long long>(heldReadHits.exchange(0, std::memory_order_relaxed)));
drainText += reads;
std::fprintf(stderr, "[draw] pipelined (10 s, depth %zu): %llu commits at %.1f us each on the committer, %llu failed; %llu enqueued behind %.2f on average, waiting %.1f us per draw for room;%s; drains:%s\n", Depth(), static_cast<unsigned long long>(commits), perCommit, static_cast<unsigned long long>(commitErrors), static_cast<unsigned long long>(enqueued), queued, perDraw, reads, drainText.c_str());
commits = commitNs = commitErrors = enqueued = fullWaitNs = depthSum = 0;
drains = {};
drainWaitNs = {};
@@ -134,6 +143,7 @@ void DrawPipeline::report(std::chrono::steady_clock::time_point now) {
void DrawPipeline::run() {
PerformanceTrace::BindThread("draw committer");
StartCommitterSampler();
for (;;) {
Commit* commit = nullptr;
SpinUntil(HandoffSpinWindow(), [&] { return outstanding.load(std::memory_order_acquire) != 0; });
@@ -211,6 +221,27 @@ std::uint64_t DrawPipeline::DrainRequestsByOpcode(std::uint32_t opcode) {
return Queue0().drainRequests[std::min<std::uint32_t>(opcode, 0x100)].load(std::memory_order_relaxed);
}
bool DrawPipeline::HeldReads(std::uint64_t address, std::size_t bytes) {
if (bytes == 0 || std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") != nullptr) return false;
const auto end = address + bytes;
auto& pipeline = Queue0();
std::lock_guard lock(pipeline.mutex);
heldReadQueries.fetch_add(1, std::memory_order_relaxed);
for (const auto& item : pipeline.items) {
for (const auto& [begin, limit] : item.reads) {
if (address >= limit || begin >= end) continue;
heldReadHits.fetch_add(1, std::memory_order_relaxed);
return true;
}
}
return false;
}
void DrawPipeline::ReleaseRunningReads() {
std::lock_guard lock(mutex);
if (!items.empty()) items.front().reads.clear();
}
std::optional<std::uint64_t> DrawPipeline::PendingLabel(std::uint64_t address, std::size_t bytes) {
if (bytes == 0 || bytes > 8) return std::nullopt;
const auto end = address + bytes;
@@ -241,7 +241,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
const auto sameTess = [](const std::optional<ShaderRecompiler::TessellationConfiguration>& x, const std::optional<ShaderRecompiler::TessellationConfiguration>& y) {
if (x.has_value() != y.has_value()) return false;
if (!x) return true;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance;
return x->inputControlPoints == y->inputControlPoints && x->outputControlPoints == y->outputControlPoints && x->domain == y->domain && x->partitioning == y->partitioning && x->outputTopology == y->outputTopology && x->patchesPerGroup == y->patchesPerGroup && x->primitiveGroupSize == y->primitiveGroupSize && x->breakAtInstance == y->breakAtInstance && x->mergedHull == y->mergedHull;
};
if (s.stages.path != t.stages.path || s.stages.registerValue != t.stages.registerValue || s.stages.vertexWaveSize != t.stages.vertexWaveSize || s.stages.fragmentWaveSize != t.stages.fragmentWaveSize || !sameMesh(s.stages.mesh, t.stages.mesh) || !sameTess(s.stages.tessellation, t.stages.tessellation)) return false;
if (!sameColor(s.color, t.color) || s.colors.size() != t.colors.size() || s.blends.size() != t.blends.size()) return false;
@@ -251,18 +251,18 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) {
for (std::size_t i = 0; i < s.blends.size(); ++i) {
if (!sameBlend(s.blends[i], t.blends[i])) return false;
}
if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false;
if (s.hasColorTarget != t.hasColorTarget || s.rectList != t.rectList || s.renderExtent.width != t.renderExtent.width || s.renderExtent.height != t.renderExtent.height || s.topology != t.topology || s.negativeOneToOne != t.negativeOneToOne || s.depthClamp != t.depthClamp || s.conservativeRasterization != t.conservativeRasterization || s.perSampleShading != t.perSampleShading || s.cullMode != t.cullMode || s.polygonMode != t.polygonMode || s.frontFace != t.frontFace || !sameBlend(s.blend, t.blend) || s.blendConstants != t.blendConstants) return false;
if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false;
const auto& p = a.pixel;
const auto& q = b.pixel;
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.frontFaceAllBits != q.frontFaceAllBits || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.orderedPixelShader != q.orderedPixelShader || p.stencilRefExportEnable != q.stencilRefExportEnable || p.stencilOperationValueExport != q.stencilOperationValueExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false;
if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false;
for (std::size_t i = 0; i < a.programs.size(); ++i) {
const auto& x = a.programs[i];
const auto& y = b.programs[i];
if (x.binary.stage != y.binary.stage || x.binary.codeAddress != y.binary.codeAddress || x.userDataBase != y.userDataBase || x.firstUserSgpr != y.firstUserSgpr || x.userData != y.userData || x.snapshot != y.snapshot || x.codeOffset != y.codeOffset || x.tailContinuation != y.tailContinuation) return false;
}
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations) return false;
if (p.targetExportPacking != q.targetExportPacking || p.dualSourceBlend != q.dualSourceBlend || p.reversedBlend != q.reversedBlend || p.alphaToCoverage != q.alphaToCoverage || p.rasterPrimitive != q.rasterPrimitive || p.ditheredAlphaToCoverage != q.ditheredAlphaToCoverage || p.quadPixelMask != q.quadPixelMask || p.quadSampleMask != q.quadSampleMask || p.iteratesSamples != q.iteratesSamples || p.positionLocation != q.positionLocation || p.sampleLocations != q.sampleLocations || p.sampleOrder != q.sampleOrder) return false;
if (s.samples != t.samples || s.mergedCoverage != t.mergedCoverage || s.quadSampleMask != t.quadSampleMask || s.pixelEarlyZ != t.pixelEarlyZ) return false;
return true;
}
@@ -1,10 +1,13 @@
#include "prx/libSceAgcDriver/Execution/include/ThreadPriority.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Screenshot.hpp"
#include "prx/libkernel/Time/include/SleepWake.hpp"
#include <mutex>
namespace AgcDriver {
void Submit(const Packet* packet, std::uint32_t queue) try {
KernelNoteGpuSubmit_nid_no_patch();
DriverDetail::Driver::Get().Submit(packet, queue);
} catch (const ProcessShutdown&) {
LibcAwaitExit_nid_postfix();
@@ -83,6 +86,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;
@@ -166,6 +166,7 @@ void Driver::stop() {
CloseRegistrationPreparation();
Graphics::StopPipelineLibraryWorker();
std::lock_guard gpuLock(GuestMemory::GpuMutex());
Graphics::ReportHostImports();
device.Reset();
replacedDevices.clear();
Graphics::FlushPipelineJournal();
@@ -182,6 +183,10 @@ Driver::Driver() {
InstallAsyncLog_nid_no_patch();
static const bool driverMemory = (MapDriverMemory(), true);
static_cast<void>(driverMemory);
LibcSetExitDrain_nid_no_patch([] {
const auto current = Driver::Get().device.Load();
if (current) current->SavePipelineCache();
});
static_cast<void>(GuestMemory::TrackerGeneration());
ShaderMemory::SetWaitedMsProvider(&Graphics::Recorder::ThreadWaitedMs);
try {
@@ -151,6 +151,26 @@ std::optional<Graphics::DccKeys> KeysFillMemo(const std::array<std::uint32_t, 4>
}
}
std::optional<DwordPatternFill> MatchSgprPatternFill(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> code, const std::vector<std::uint32_t>& userData, const ShaderRecompiler::ShaderComputeStageInfo& compute) {
if (userData.size() != 9 || packet.size() < 5 || !compute.groupIdEnable[0] || compute.numThreads[0] != 64 || compute.numThreads[1] != 1 || compute.numThreads[2] != 1) return std::nullopt;
static constexpr std::array<std::uint32_t, 23> kernel{
0xbfa00001u, 0xd7460004u, 0x04010c09u, 0x7e000204u, 0x7e020205u, 0x7e040206u, 0x7e060207u, 0x7da80808u,
0xbf88000du, 0x360a0883u, 0xbe84047eu, 0xbe86047eu, 0x7e100505u, 0x7da40a08u, 0xbefc0308u, 0x7e0c8700u,
0x8a867e06u, 0xbefe0406u, 0xbf85fff9u, 0xbefe0404u, 0xe0102000u, 0x80000604u, 0xbf810000u};
if (code.size() < kernel.size() || !std::equal(kernel.begin(), kernel.end(), code.begin())) return std::nullopt;
const auto stride = (userData[1] >> 16u) & 0x3fffu;
const bool swizzled = ((userData[1] >> 31u) & 1u) != 0;
const bool addTid = ((userData[3] >> 23u) & 1u) != 0;
const auto outOfBounds = (userData[3] >> 28u) & 3u;
const auto format = (userData[3] >> 12u) & 0x7fu;
const auto type = userData[3] >> 30u;
if (type != 0 || stride != 4 || swizzled || addTid || outOfBounds > 1u || format < 20u || format > 22u || (packet[4] & 0x20u) != 0 || packet[2] != 1 || packet[3] != 1) return std::nullopt;
const auto dwords = std::min<std::uint64_t>({static_cast<std::uint64_t>(packet[1]) * 64u, userData[8], userData[2]});
if (dwords == 0) return std::nullopt;
const auto base = userData[0] | (static_cast<std::uint64_t>(userData[1] & 0xffffu) << 32u);
return DwordPatternFill{base, static_cast<std::size_t>(dwords * 4u), {userData[4], userData[5], userData[6], userData[7]}};
}
bool Driver::fillClearEnabled() {
static const bool enabled = std::getenv("APS5_NO_FILL_CLEAR") == nullptr;
return enabled;
@@ -4,10 +4,13 @@
#include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
#include "prx/libSceAgcDriver/Graphics/include/VertexInput.hpp"
#include "Optimization/ShaderStageInputInfo.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <stdexcept>
namespace AgcDriver::DriverDetail {
@@ -64,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));
@@ -156,6 +160,40 @@ std::vector<std::pair<std::uint64_t, std::uint64_t>> Driver::drawWriteRanges(con
return DrawWriteRanges(graphics, stages, ExactDrawWrites());
}
std::vector<std::pair<std::uint64_t, std::uint64_t>> QueuedDrawReadRanges(const Pm4::DrawParameters& draw, std::span<const Graphics::CompiledShader> stages) {
std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges;
const auto add = [&](std::uint64_t begin, std::uint64_t bytes) {
if (begin == 0 || bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - begin) return;
ranges.emplace_back(begin, begin + bytes);
};
if (draw.indexed && draw.indexSize != 0 && draw.indexCount != 0) add(draw.indexAddress, static_cast<std::uint64_t>(draw.indexCount) * draw.indexSize);
if (stages.empty()) return ranges;
for (const auto& stage : stages) {
if (stage.program == nullptr) continue;
for (const auto& binding : stage.program->bindings) {
if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers || binding.guestDescriptor.size() < static_cast<std::size_t>(binding.count) * 4u) continue;
for (std::uint32_t element = 0; element < binding.count; ++element) {
const auto* words = binding.guestDescriptor.data() + static_cast<std::size_t>(element) * 4u;
const ShaderRecompiler::ShaderBufferResource descriptor{{words[0], words[1], words[2], words[3]}};
add(descriptor.Base48(), descriptor.GetSize());
}
}
}
if (stages.empty() || stages.front().program == nullptr) return ranges;
for (const auto& attribute : stages.front().program->vertexAttributes) {
if (Graphics::NullVertexDescriptor(attribute) || Graphics::VertexFetchOutOfRange(attribute)) continue;
const auto address = attribute.resource.fields[0] | (static_cast<std::uint64_t>(attribute.resource.fields[1] & 0xffffu) << 32u);
std::uint64_t bytes = 0;
try {
bytes = Graphics::VertexBufferExtent(attribute);
} catch (const std::runtime_error&) {
continue;
}
add(address, bytes);
}
return ranges;
}
std::optional<WrittenBuffer> Driver::newestWriterLocked(std::uint64_t begin, std::uint64_t end) const {
for (auto it = writtenBuffers.rbegin(); it != writtenBuffers.rend(); ++it) {
if (begin < it->end && it->begin < end) return *it;
@@ -11,9 +11,12 @@
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
#include "prx/libSceAgcDriver/Eq/include/Event.hpp"
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <functional>
#include <shared_mutex>
#include <utility>
namespace AgcDriver::DriverDetail {
@@ -53,6 +56,14 @@ bool keepsDrawPipeline(std::span<const std::uint32_t> packet, std::uint32_t head
const auto address = packet[4] | (static_cast<std::uint64_t>(packet[5]) << 32u);
return !DrawPipeline::Queue0().Overlaps(address, bytes);
}
case 0x3c: case 0x93: {
if (packet.size() < 5) return false;
const std::uint64_t awaited = packet[2] | (static_cast<std::uint64_t>(packet[3]) << 32u);
const auto bytes = Pm4::WaitAwaitedBytes(packet);
if (DrawPipeline::Queue0().Overlaps(awaited, bytes)) return false;
GuestMemory::CheckRange(reinterpret_cast<const void*>(awaited), bytes, bytes);
return Pm4::WaitSatisfiedUnchecked(packet);
}
case 0x10: {
const auto operation = (header >> 2u) & 0x3fu;
return operation == 0 || operation == 0x09 || operation == 0x0b || operation == 0x0c || operation == 0x1a;
@@ -61,6 +72,20 @@ bool keepsDrawPipeline(std::span<const std::uint32_t> packet, std::uint32_t head
}
}
bool sealsWriteOnly(std::span<const std::uint32_t> packet, std::uint32_t opcode) {
if (opcode == 0x58) return true;
if (opcode != 0x46 || packet.size() < 2) return false;
switch (packet[1] & 0x3fu) {
case 0x07: case 0x0f: case 0x10: case 0x16: case 0x2a: case 0x2c: case 0x2e: case 0x31: return true;
default: return false;
}
}
bool& drawnSinceSeal() {
static thread_local bool drawn = false;
return drawn;
}
}
void Driver::deliverGuestEopInterrupt(std::uint32_t queue) {
@@ -350,14 +375,15 @@ void Driver::execute(const Submission& submission) {
timing.Mark("flip_flush");
timing.FinishPacket(submission.serial, cursor, 0xffffu, count);
auto frame = APS5_ENABLE_TIMING_LOG ? std::move(pendingFrameTiming) : std::make_shared<FrameTiming>(++frameSerial);
const bool queueTiming = APS5_ENABLE_TIMING_LOG && submission.queue == 0;
auto frame = queueTiming ? std::move(pendingFrameTiming) : std::make_shared<FrameTiming>(++frameSerial);
require(frame != nullptr, "flip has no frame timing");
const auto now = FrameTiming::Clock::now();
if (!APS5_ENABLE_TIMING_LOG) frame->IncludeSubmission(submission.serial, now, now, now, true);
frame->SetFlip(submission.serial, cursor, APS5_ENABLE_TIMING_LOG ? submission.receivedAt : now, now);
if (APS5_ENABLE_TIMING_LOG) frame->CollectBackground();
if (!queueTiming) frame->IncludeSubmission(submission.serial, now, now, now, true);
frame->SetFlip(submission.serial, cursor, queueTiming ? submission.receivedAt : now, now);
if (queueTiming) frame->CollectBackground();
frame->NoteFlipBatches(batchesAtFlip, unsignaledAtFlip);
CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast<unsigned long long>(frameSerial), static_cast<unsigned long long>(submission.serial), cursor, static_cast<unsigned long long>(batchesAtFlip), static_cast<unsigned long long>(unsignaledAtFlip));
CaptureTrace::Log("flip frame=%llu submission=%llu offset=%zu batch=%llu unsignaled=%llu", static_cast<unsigned long long>(frameSerial.load()), static_cast<unsigned long long>(submission.serial), cursor, static_cast<unsigned long long>(batchesAtFlip), static_cast<unsigned long long>(unsignaledAtFlip));
flipHold.Release();
submission.flips.at(cursor)->GpuReady(frame);
} else if (opcode == 0x15 || opcode == 0x16) {
@@ -396,6 +422,7 @@ void Driver::execute(const Submission& submission) {
} else if (drawPacket) {
landPendingWaits(submission.queue);
packetCounters.CountDraw();
drawnSinceSeal() = true;
bool drawn = false;
timed(&WorkerProfile::drawMs, [&] {
static const bool traceDraws = std::getenv("APS5_TRACE_DRAWS") != nullptr;
@@ -419,6 +446,10 @@ void Driver::execute(const Submission& submission) {
} else if (traceDraws) {
std::fprintf(stderr, "[draw] target 0x%llx mask 0x%x ok\n",static_cast<unsigned long long>(color), readRegister(queue.context, 0x8e));
}
} catch (const RefusedDraw& refused) {
CaptureTrace::Log("draw-refused submission=%llu offset=%zu reason=%.256s", static_cast<unsigned long long>(submission.serial), cursor, refused.what());
countSkip(Graphics::DrawSkip::Thrown);
ReportRefusedDraw(refused);
} catch (const std::exception& error) {
CaptureTrace::Log("draw-error submission=%llu offset=%zu reason=%.256s", static_cast<unsigned long long>(submission.serial), cursor, error.what());
countSkip(Graphics::DrawSkip::Thrown);
@@ -438,10 +469,43 @@ void Driver::execute(const Submission& submission) {
Pm4::Execute(packet, queue);
} else if (sampleDump && !wroteOnGpu) {
dumpSampleCounters(Pm4::SampleDumpAddress(packet));
} else if (submission.queue == 0 && sealsWriteOnly(packet, opcode)) {
if (std::exchange(drawnSinceSeal(), false)) {
const auto seal = [this] {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Flush);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
if (const auto localDevice = device.Load()) localDevice->SealPassWriteOnly();
};
if (pipelined && DrawPipeline::Queue0().Busy()) DrawPipeline::Queue0().Enqueue(seal, {});
else seal();
}
} else if (opcode != 0x42 && opcode != 0x46 && opcode != 0x58) {
if (!wroteOnGpu) {
if (Pm4::IndirectRegisterOpcode(opcode)) Pm4::ExecuteIndirectRegisters(packet, submission.registerLists.at(cursor), queue);
else Pm4::Execute(packet, queue);
if (Pm4::IndirectRegisterOpcode(opcode)) {
const auto& listed = submission.registerLists.at(cursor);
if (Pm4::IndirectRegistersValid(listed)) {
Pm4::ExecuteIndirectRegisters(packet, listed, queue);
} else {
landPendingWaits(submission.queue);
recordQueuedLabelsBeforeRead(submission.queue);
const auto current = Pm4::ReadIndirectRegisters(packet);
static const bool skipInvalid = std::getenv("APS5_SKIP_INVALID_INDIRECT_REGISTERS") != nullptr;
static std::atomic<bool> reported{false};
if (!reported.exchange(true)) {
std::uint32_t invalid = 0;
for (std::size_t i = 0; i < listed.size(); i += 2) {
if (!Pm4::IndirectRegistersValid(std::span(&listed[i], 1))) {
invalid = listed[i];
break;
}
}
std::fprintf(stderr, "[gpu] queue 0x%x %s at DWORD %zu: register list entry with offset dword 0x%08x read again where the packet executes%s\n", submission.queue, Pm4::Name(packet[0]).c_str(), cursor, invalid, skipInvalid ? "; invalid entries are skipped (APS5_SKIP_INVALID_INDIRECT_REGISTERS)" : "");
}
Pm4::ExecuteIndirectRegisters(packet, current, queue, skipInvalid);
}
} else {
Pm4::Execute(packet, queue);
}
if (opcode == 0x49 || opcode == 0x37) {
if (const auto label = Pm4::DecodeLabelWrite(packet)) noteLabelStore(label->address, label->Bytes(), ++eventSerial, submission.queue);
}
@@ -468,6 +532,7 @@ void Driver::execute(const Submission& submission) {
submitOpenWork();
wakeQueueZeroForCompletions();
}, {});
endTiming.Finish();
executeRewindTail(submission);
return;
}
@@ -11,6 +11,8 @@
#include "prx/libSceAgcDriver/Graphics/include/Recorder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
#include "prx/libSceAgcDriver/Graphics/include/UnitShadow.hpp"
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <limits>
@@ -114,7 +116,29 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
if (!drainAll && !cpuStores && (opcode == 0x40 || opcode == 0x50 || opcode == 0x83)) {
constexpr std::size_t gpuStoreLimit = 65536;
bool drained = true;
if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) {
const auto copy = opcode == 0x50 ? Pm4::DecodeMemoryCopy(packet) : std::nullopt;
const auto copyBehindPendingSource = [&] {
if (!copy.has_value() || copy->bytes > gpuStoreLimit || !GuestMemory::Accessible(reinterpret_cast<const void*>(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast<const void*>(copy->destination), copy->bytes, true)) return false;
const auto end = copy->source + copy->bytes;
const std::array<std::pair<std::uint64_t, std::uint64_t>, 1> range{{{copy->source, end}}};
const auto& queued = deferredLabels().labels;
const bool labelQueued = std::any_of(queued.begin(), queued.end(), [&](const DeferredLabel& label) { return label.address < end && copy->source < label.address + label.size; });
if (!labelQueued && !Graphics::Recorder::SnapshotWriteOverlaps(copy->source, copy->bytes) && !Graphics::StorageTexture::AnyPendingOverlaps(range) && !Graphics::AnyShadowedOverlaps(copy->source, copy->bytes)) return false;
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
const auto localDevice = device.Load();
if (localDevice == nullptr) return false;
recordDeferredLabels(localDevice.get(), submission.queue);
if (!localDevice->StoresPendingOver(copy->source, copy->bytes) || !localDevice->GpuCopyPossible(copy->destination, copy->source, copy->bytes)) return false;
const auto outcome = localDevice->CopyBuffer(copy->destination, copy->source, copy->bytes, 0, std::numeric_limits<std::size_t>::max(), 0, 0, submission.queue, [](std::span<const std::byte>, std::uint64_t) {});
if (outcome.path != 1 && outcome.path != 3) throw std::runtime_error("AGC driver: a DMA_DATA copy of up to 64 KiB that GpuCopyPossible accepted took copy path " + std::to_string(outcome.path));
return true;
};
if (copyBehindPendingSource()) {
++copiesBehindPendingSources;
wroteOnGpu = true;
drained = false;
} else if (const auto store = Pm4::ResolveStore(packet, queue, gpuStoreLimit)) {
const auto bytes = store->Bytes();
if (bytes.empty()) {
@@ -147,7 +171,6 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
}
}
if (drained && opcode == 0x50) {
const auto copy = Pm4::DecodeMemoryCopy(packet);
if (copy.has_value() && copy->bytes > gpuStoreLimit && GuestMemory::Accessible(reinterpret_cast<const void*>(copy->source), copy->bytes) && GuestMemory::Accessible(reinterpret_cast<const void*>(copy->destination), copy->bytes, true)) {
GuestMemory::TagGpuLockSite(GuestMemory::GpuLockSite::Copy);
std::lock_guard gpuLock(GuestMemory::GpuMutex());
@@ -311,6 +334,7 @@ bool Driver::enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32
const auto copy = Pm4::DecodeMemoryCopy(packet);
if (!copy.has_value() || copy->destination % 4 != 0 || copy->bytes % 4 != 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(copy->source), copy->bytes) || !GuestMemory::Accessible(reinterpret_cast<const void*>(copy->destination), copy->bytes, true)) return false;
DrawPipeline::Queue0().Enqueue([this, queue, copy = *copy] {
DrawPipeline::Queue0().ReleaseRunningReads();
GuestMemory::SetCurrentPacket(0x50, queue);
constexpr std::size_t gpuStoreLimit = 65536;
if (copy.bytes > gpuStoreLimit) {
@@ -326,7 +350,7 @@ bool Driver::enqueueDmaPacket(std::span<const std::uint32_t> packet, std::uint32
std::vector<std::byte> bytes(copy.bytes);
GuestMemory::Read(copy.source, bytes, 1);
commitDmaStore(queue, copy.destination, bytes);
}, {{copy->destination, copy->destination + copy->bytes}});
}, {{copy->destination, copy->destination + copy->bytes}}, 0, {}, {{copy->source, copy->source + copy->bytes}});
return true;
}
@@ -2,6 +2,7 @@
#include "ControlFlow/GraphBuilder.hpp"
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Shaders/ShaderRegistry.hpp"
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
@@ -32,6 +33,13 @@
#include <mutex>
#include <atomic>
#include <pthread.h>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <process.h>
#endif
#ifdef __GLIBC__
#include <malloc.h>
#endif
@@ -69,6 +77,11 @@ std::shared_ptr<const ShaderRecompiler::SourceHandle> PrepareShaderWithDiagnosti
}
std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address) {
const auto drain = [](std::uint64_t first, std::size_t bytes) {
if (!DrawPipeline::Active()) return;
auto& pipeline = DrawPipeline::Queue0();
if (pipeline.Busy() && pipeline.Overlaps(first, bytes)) pipeline.Drain(DrawPipeline::DrainReason::Capture);
};
GuestMemory::CheckRange(reinterpret_cast<const void*>(address), sizeof(std::uint32_t), 256);
static std::mutex cacheMutex;
static std::list<std::shared_ptr<const ShaderSnapshot>> cache;
@@ -81,6 +94,7 @@ std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address) {
}
if (cached) {
const auto code = std::as_bytes(std::span(cached->code));
drain(address, code.size());
GuestMemory::FlushGpuWrites(address, code.size());
if (GuestMemory::CompareMapped(address, code) == GuestMemory::Compare::Equal) {
std::lock_guard lock(cacheMutex);
@@ -101,6 +115,7 @@ std::shared_ptr<const ShaderSnapshot> ReadRawShader(std::uint64_t address) {
while (snapshot.code.size() < available) {
const auto previous = snapshot.code.size();
snapshot.code.resize(std::min(available, std::max<std::size_t>(64, previous * 2)));
drain(address + previous * sizeof(std::uint32_t), (snapshot.code.size() - previous) * sizeof(std::uint32_t));
GuestMemory::Read(address + previous * sizeof(std::uint32_t),
std::as_writable_bytes(std::span(snapshot.code).subspan(previous)), alignof(std::uint32_t));
try {
@@ -410,6 +425,36 @@ static bool RelocatedRegisters(const ShaderSnapshot& snapshot, Shader used, cons
return std::memcmp(static_cast<const void*>(bound), snapshot.header.data() + (address - snapshot.headerAddress), bytes) == 0;
}
// A title can relocate a registered header, and the arrays it points at, into memory of its own and resolve the ABI
// with that copy. Every array the copy refers to must still hold the registered bytes: the registered header then
// decodes to the values the copy describes.
static bool ReferredBytesMatch(const ShaderSnapshot& snapshot, const void* pointer, const void* registeredPointer, std::size_t bytes) {
if (bytes == 0 || pointer == registeredPointer) return true;
if (pointer == nullptr || registeredPointer == nullptr) return false;
const auto address = reinterpret_cast<std::uintptr_t>(registeredPointer);
if (address < snapshot.headerAddress) return false;
const auto offset = address - snapshot.headerAddress;
if (offset > snapshot.header.size() || snapshot.header.size() - offset < bytes) return false;
if (!GuestMemory::Accessible(pointer, bytes)) return false;
return std::memcmp(static_cast<const void*>(pointer), snapshot.header.data() + offset, bytes) == 0;
}
static bool RelocatedCopy(const ShaderSnapshot& snapshot, Shader used, const Shader& registered) {
Shader masked = used;
masked.user_data = registered.user_data;
masked.cx_registers = registered.cx_registers;
masked.sh_registers = registered.sh_registers;
masked.specials = registered.specials;
masked.input_semantics = registered.input_semantics;
masked.output_semantics = registered.output_semantics;
if (std::memcmp(&masked, &registered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) != 0) return false;
return ReferredBytesMatch(snapshot, used.sh_registers, registered.sh_registers, used.num_sh_registers * sizeof(ShaderRegister))
&& ReferredBytesMatch(snapshot, used.cx_registers, registered.cx_registers, used.num_cx_registers * sizeof(ShaderRegister))
&& ReferredBytesMatch(snapshot, used.specials, registered.specials, sizeof(ShaderSpecialRegs))
&& ReferredBytesMatch(snapshot, used.input_semantics, registered.input_semantics, used.num_input_semantics * sizeof(ShaderSemantic))
&& ReferredBytesMatch(snapshot, used.output_semantics, registered.output_semantics, used.num_output_semantics * sizeof(ShaderSemantic));
}
bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) {
if (snapshot.headerAddress == reinterpret_cast<std::uintptr_t>(shader)) return true;
if (snapshot.header.size() < sizeof(Shader)) return false;
@@ -419,7 +464,7 @@ bool SameHeader(const ShaderSnapshot& snapshot, const Shader* shader) {
std::memcpy(&used, static_cast<const void*>(shader), sizeof(Shader));
std::memcpy(&registered, snapshot.header.data(), sizeof(Shader));
used.user_data = registered.user_data;
return std::memcmp(&used, &registered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered);
return std::memcmp(&used, &registered, offsetof(Shader, num_sh_registers) + sizeof(Shader::num_sh_registers)) == 0 || RelocatedRegisters(snapshot, used, registered) || RelocatedCopy(snapshot, used, registered) || FusedHullHeader(snapshot, used, registered);
}
std::size_t ShaderRegistry::chunkFor(std::uint64_t address) const {
@@ -773,7 +818,7 @@ std::optional<ShaderRecompiler::ShaderFloatMode> DecodeFloatMode(const Registers
case 0: rsrc1 = 0x212; fp16OverflowBit = 26; break;
case 1: rsrc1 = 0x00a; fp16OverflowBit = 29; break;
case 2: case 4: case 6: rsrc1 = 0x08a; fp16OverflowBit = 31; break;
case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break;
case 3: case 5: case 7: rsrc1 = 0x10a; fp16OverflowBit = 30; break;
default: return std::nullopt;
}
const auto found = registers.find(rsrc1);
@@ -871,7 +916,7 @@ std::unique_ptr<RegisteredPreparation> PlanRegistered(const ShaderSnapshot& snap
case 1: stage = Stage::Fragment; programRegister = 0x008; resourceRegister = 0x00b; break;
case 2: stage = Stage::Vertex; programRegister = 0x0c8; resourceRegister = 0x08b; firstUser = 8; break;
case 4: stage = Stage::Mesh; programRegister = 0x0c8; resourceRegister = 0x08b; break;
case 5: return {};
case 3: case 5: return {};
case 6: stage = Stage::Mesh; programRegister = 0x088; resourceRegister = 0x08b; break;
case 7: stage = Stage::TessellationControl; programRegister = 0x108; resourceRegister = 0x10b; break;
default:
@@ -1160,7 +1205,7 @@ public:
}
void Close() {
std::vector<pthread_t> joined;
std::vector<Worker> joined;
{
std::unique_lock lock(mutex);
closed = true;
@@ -1168,16 +1213,45 @@ public:
joined.swap(workers);
}
ready.notify_all();
for (const auto worker : joined) {
if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr);
}
for (const auto& worker : joined) Join(worker);
}
private:
static constexpr std::size_t StackBytes = 64u << 20u;
#ifdef _WIN32
struct Worker {
HANDLE handle;
unsigned id;
};
static void Join(const Worker& worker) {
if (worker.id != GetCurrentThreadId()) WaitForSingleObject(worker.handle, INFINITE);
CloseHandle(worker.handle);
}
#else
using Worker = pthread_t;
static void Join(Worker worker) {
if (!pthread_equal(worker, pthread_self())) pthread_join(worker, nullptr);
}
#endif
explicit RegistrationPreparePool(unsigned count) {
#ifdef _WIN32
for (unsigned index = 0; index < count; ++index) {
unsigned id = 0;
const auto thread = _beginthreadex(nullptr, static_cast<unsigned>(StackBytes), &RegistrationPreparePool::WindowsMain, this, STACK_SIZE_PARAM_IS_A_RESERVATION, &id);
if (thread != 0) {
workers.push_back({reinterpret_cast<HANDLE>(thread), id});
++threads;
liveWorkers.fetch_add(1, std::memory_order_acq_rel);
}
}
#else
pthread_attr_t attributes;
pthread_attr_init(&attributes);
pthread_attr_setstacksize(&attributes, 64u << 20u);
pthread_attr_setstacksize(&attributes, StackBytes);
for (unsigned index = 0; index < count; ++index) {
pthread_t thread;
if (pthread_create(&thread, &attributes, &RegistrationPreparePool::Main, this) == 0) {
@@ -1187,8 +1261,16 @@ private:
}
}
pthread_attr_destroy(&attributes);
#endif
}
#ifdef _WIN32
static unsigned __stdcall WindowsMain(void* argument) {
Main(argument);
return 0;
}
#endif
static void* Main(void* argument) {
auto& pool = *static_cast<RegistrationPreparePool*>(argument);
if (PerformanceTrace::Configured()) PerformanceTrace::BindThread("shader prepare", TraceThread::DefaultCapacity / 8);
@@ -1233,7 +1315,7 @@ private:
std::condition_variable ready;
std::condition_variable idle;
std::deque<Task> tasks;
std::vector<pthread_t> workers;
std::vector<Worker> workers;
unsigned threads = 0;
inline static std::atomic<unsigned> liveWorkers{0};
unsigned running = 0;
@@ -1320,13 +1402,13 @@ std::vector<PreparedGraphicsStage> PrepareGraphicsStages(const DrawDecode& decod
require(expected == decoded.roles, "graphics ABI linked programs do not match the stage routing");
std::vector<ShaderRecompiler::LinkedProgram> linked;
std::vector<ShaderRecompiler::MemoryRegion> memory;
AppendProgramMemory(memory, decoded.programs);
for (std::size_t index = 0; index < decoded.programs.size(); ++index) {
const auto& program = decoded.programs[index];
require(program.snapshot != nullptr && program.codeOffset < program.snapshot->code.size(), "graphics ABI has an invalid registered entry point");
const auto code = std::span(program.snapshot->code).subspan(program.codeOffset);
require(program.binary.code.data() == code.data() && program.binary.code.size() == code.size() && program.binary.codeAddress == program.snapshot->codeAddress + program.codeOffset * sizeof(std::uint32_t), "graphics ABI entry point differs from its snapshot");
linked.push_back({decoded.roles[index], program.binary, program.userDataBase, program.firstUserSgpr, program.userData});
memory.insert(memory.end(), program.memory.begin(), program.memory.end());
}
std::vector<PreparedGraphicsStage> prepared;
std::uint32_t pushOffset = 0;
@@ -21,7 +21,7 @@ std::atomic<std::uint64_t> packetLockRecordUs{0}, packetLockSubmitUs{0};
std::atomic<std::uint64_t> triesFailed[TrySites] = {};
std::atomic<std::uint64_t> storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0};
std::atomic<std::uint64_t> storesOnGpu{0}, storesBehindCompletions{0}, storesOnCpu{0}, storesDrained{0}, dmaFills{0}, copiesToHostMemory{0}, copiesBehindPendingSources{0};
std::map<std::uint32_t, std::uint64_t> drainCounts;
@@ -39,7 +39,7 @@ SubmissionCosts& submissionCosts(std::uint32_t queue) {
void reportSync() {
std::string report;
for (const auto& [code, count] : drainCounts) report += " " + (code == 0xffffu ? std::string("flip") : Pm4::Name(code << 8u)) + "=" + std::to_string(count);
AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source\n", static_cast<unsigned long long>(drainTotal), report.c_str(), static_cast<unsigned long long>(unlockedDrains.load()), static_cast<unsigned long long>(gpuLabels.load()), static_cast<unsigned long long>(completionLabels.load()), static_cast<unsigned long long>(noOpLabels.load()), static_cast<unsigned long long>(labelFallbacks[1].load()), static_cast<unsigned long long>(labelFallbacks[2].load()), static_cast<unsigned long long>(labelFallbacks[3].load()), static_cast<unsigned long long>(labelFallbacks[4].load()), static_cast<unsigned long long>(queuedLabels.load()), static_cast<unsigned long long>(labelGroups.load()), static_cast<unsigned long long>(immediateLabels.load()), static_cast<unsigned long long>(storesOnGpu.load()), static_cast<unsigned long long>(storesBehindCompletions.load()), static_cast<unsigned long long>(storesOnCpu.load()), static_cast<unsigned long long>(storesDrained.load()), static_cast<unsigned long long>(dmaFills.load()), static_cast<unsigned long long>(copiesToHostMemory.load()));
AgcDriver::ProfilePrint_nid_no_patch("[sync] %llu device drains by packet:%s (%llu waited without the GPU mutex); %llu labels written on the GPU, %llu deferred behind completions, %llu no-op, fallbacks: idle %llu, completions %llu, not imported %llu, undecodable %llu; %llu labels queued per worker, recorded in %llu groups, %llu recorded at once; stores: %llu recorded on the GPU, %llu behind completions, %llu on the CPU (idle), %llu synced (drained), %llu DMA fills stored as buffer fills; %llu copies into host memory synced on their source; %llu copies of up to 64 KiB recorded on the GPU behind their source's pending stores\n", static_cast<unsigned long long>(drainTotal), report.c_str(), static_cast<unsigned long long>(unlockedDrains.load()), static_cast<unsigned long long>(gpuLabels.load()), static_cast<unsigned long long>(completionLabels.load()), static_cast<unsigned long long>(noOpLabels.load()), static_cast<unsigned long long>(labelFallbacks[1].load()), static_cast<unsigned long long>(labelFallbacks[2].load()), static_cast<unsigned long long>(labelFallbacks[3].load()), static_cast<unsigned long long>(labelFallbacks[4].load()), static_cast<unsigned long long>(queuedLabels.load()), static_cast<unsigned long long>(labelGroups.load()), static_cast<unsigned long long>(immediateLabels.load()), static_cast<unsigned long long>(storesOnGpu.load()), static_cast<unsigned long long>(storesBehindCompletions.load()), static_cast<unsigned long long>(storesOnCpu.load()), static_cast<unsigned long long>(storesDrained.load()), static_cast<unsigned long long>(dmaFills.load()), static_cast<unsigned long long>(copiesToHostMemory.load()), static_cast<unsigned long long>(copiesBehindPendingSources.load()));
}
void countLabelOutcome(int reason) {
@@ -329,6 +329,7 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
~BlockedWait() { driver.noteWaitBlocked(queue, 0, false); }
} blocked{*this, queue};
static const bool pauseSpin = std::getenv("APS5_NO_PAUSE_SPIN") == nullptr;
static const bool storeHeldResults = std::getenv("APS5_NO_QUEUED_DRAW_READ_ORDER") == nullptr;
const auto spinLimit = queue == 0 ? std::chrono::microseconds(1500) : std::chrono::microseconds(100);
const auto spinStart = std::chrono::steady_clock::now();
bool spinning = pauseSpin;
@@ -352,6 +353,7 @@ void Driver::waitMemory(std::span<const std::uint32_t> packet, std::uint32_t que
}
CheckFailure();
if (pollService(!spinning)) return;
if (storeHeldResults && !spinning && (polls & 31u) == 0) GuestMemory::FlushGpuWrites(awaited, awaitedBytes);
noteAwaitingTitle(queue, awaited);
if (requireMemory && (polls & 15u) == 0) {
std::unique_lock settleLock(GuestMemory::GpuMutex(), std::try_to_lock);
@@ -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);
@@ -810,27 +837,22 @@ struct WriteTracker {
bool initialized = false;
bool watched = false;
static constexpr std::size_t CoarseBlocks = 64;
#ifdef _WIN32
std::uintptr_t base = 0;
std::size_t size = 0;
// Generation of the last collected write per 64 KiB block of the arena.
std::vector<std::uint32_t> blocks;
// The same, stamped by collects only (dirty pages = CPU stores), never by MarkWritten: the
// late label rule asks whether the CPU touched a label's block, and the driver's own GPU label
// records in the same block (the title's per-job slots are 0x20 apart) must not count.
std::vector<std::uint32_t> cpuBlocks;
std::vector<std::uint32_t> coarseBlocks;
WriteWatchCoverage coverage;
std::vector<std::uint32_t> hintBlocks;
#else
static constexpr std::size_t LeafBlocks = std::size_t{1} << 16;
static constexpr std::size_t LeafCount = std::size_t{1} << 15;
struct Leaf {
std::array<std::uint32_t, LeafBlocks> blocks{};
std::array<std::uint32_t, LeafBlocks> cpuBlocks{};
std::array<std::uint32_t, LeafBlocks / CoarseBlocks> coarseBlocks{};
#ifdef _WIN32
std::array<std::uint32_t, LeafBlocks> hintBlocks{};
#endif
};
std::vector<std::unique_ptr<Leaf>> leaves;
#ifdef _WIN32
std::uintptr_t base = 0;
std::size_t size = 0;
WriteWatchCoverage coverage;
#else
static constexpr std::size_t LeafCount = std::size_t{1} << 15;
#endif
// Atomic only so a per-thread memo hit can read it without the mutex (every mutation and every
// stamp still happen under it): a hit must return the current value, including MarkWritten
@@ -862,11 +884,8 @@ struct WriteTracker {
GuestArena::GuestArenaRange_nid_postfix(&base, &size);
watched = size != 0 && GuestArena::GuestArenaWriteWatched_nid_postfix();
if (!watched) return;
blocks.assign(size / WriteBlockBytes + 1, 0);
cpuBlocks.assign(size / WriteBlockBytes + 1, 0);
coarseBlocks.assign(blocks.size() / CoarseBlocks + 1, 0);
leaves.resize((size / WriteBlockBytes + LeafBlocks) / LeafBlocks);
coverage.Initialize(base, size);
hintBlocks.assign(size / WriteBlockBytes + 1, 0);
pages.resize(1u << 16);
GuestArena::GuestArenaSetPrivateMappingObserver_nid_postfix(&watchPrivateMapping);
#else
@@ -906,24 +925,24 @@ struct WriteTracker {
if (first >= stop) return;
bool started = false;
for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) {
if (!coverage.Covers(blockBegin(block), 1)) continue;
if (!coverage.Covers(blockBegin(block), static_cast<std::size_t>(std::min<std::uint64_t>(WriteBlockBytes, base + size - blockBegin(block))))) continue;
if (!started) ++generation;
started = true;
hintBlocks[block] = generation;
stampHint(block);
}
}
bool hinted(std::uint64_t first, std::uint64_t stop) const {
if (first >= stop || first < base || stop > base + size) return false;
for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) {
if (hintBlocks[block] == 0 || coverage.Covers(blockBegin(block), 1)) return false;
if (hintOf(block) == 0 || coverage.Covers(blockBegin(block), static_cast<std::size_t>(std::min<std::uint64_t>(WriteBlockBytes, base + size - blockBegin(block))))) return false;
}
return !GuestArena::GuestArenaHostRegionOverlaps_nid_postfix(static_cast<std::uintptr_t>(first), static_cast<std::size_t>(stop - first));
}
std::uint32_t newestHint(std::uint64_t first, std::uint64_t stop) const {
std::uint32_t newest = 0;
for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) newest = std::max(newest, hintBlocks[block]);
for (auto block = blockOf(first); block <= blockOf(stop - 1); ++block) newest = std::max(newest, hintOf(block));
return newest;
}
#endif
@@ -1024,13 +1043,26 @@ struct WriteTracker {
slot = {stampGeneration, from, to};
}
std::uint32_t stampOf(std::uint64_t block) const {
Leaf& leafFor(std::uint64_t block) {
auto& leaf = leaves[block / LeafBlocks];
if (leaf == nullptr) leaf = std::make_unique<Leaf>();
return *leaf;
}
#ifdef _WIN32
return blocks[block];
#else
void stampHint(std::uint64_t block) {
leafFor(block).hintBlocks[block % LeafBlocks] = generation;
}
std::uint32_t hintOf(std::uint64_t block) const {
const auto& leaf = leaves[block / LeafBlocks];
return leaf != nullptr ? leaf->hintBlocks[block % LeafBlocks] : 0;
}
#endif
std::uint32_t stampOf(std::uint64_t block) const {
const auto& leaf = leaves[block / LeafBlocks];
return leaf != nullptr ? leaf->blocks[block % LeafBlocks] : 0;
#endif
}
static constexpr std::size_t UnstampedSlots = 1u << 16;
@@ -1068,28 +1100,23 @@ struct WriteTracker {
}
std::uint32_t coarseStampOf(std::uint64_t block) const {
#ifdef _WIN32
return coarseBlocks[block / CoarseBlocks];
#else
const auto& leaf = leaves[block / LeafBlocks];
return leaf != nullptr ? leaf->coarseBlocks[(block % LeafBlocks) / CoarseBlocks] : 0;
#endif
}
bool unchanged(std::uint64_t first, std::uint64_t last, std::uint64_t since) const {
if (first == last) return stampOf(first) <= since;
const auto newer = [since](std::uint32_t stamp) { return stamp > since; };
while (first <= last) {
const auto& leaf = leaves[first / LeafBlocks];
if (leaf == nullptr) {
first = (first / LeafBlocks + 1) * LeafBlocks;
continue;
}
const auto end = std::min<std::uint64_t>(last + 1, (first / CoarseBlocks + 1) * CoarseBlocks);
if (end - first < CoarseBlocks || coarseStampOf(first) > since) {
#ifdef _WIN32
if (std::any_of(blocks.begin() + first, blocks.begin() + end, newer)) return false;
#else
if (const auto& leaf = leaves[first / LeafBlocks]; leaf != nullptr) {
const auto begin = leaf->blocks.begin() + first % LeafBlocks;
if (std::any_of(begin, begin + (end - first), newer)) return false;
}
#endif
const auto begin = leaf->blocks.begin() + first % LeafBlocks;
if (std::any_of(begin, begin + (end - first), newer)) return false;
}
first = end;
}
@@ -1097,12 +1124,8 @@ struct WriteTracker {
}
std::uint32_t cpuStampOf(std::uint64_t block) const {
#ifdef _WIN32
return cpuBlocks[block];
#else
const auto& leaf = leaves[block / LeafBlocks];
return leaf != nullptr ? leaf->cpuBlocks[block % LeafBlocks] : 0;
#endif
}
bool storedOver(std::uint64_t address, std::uint64_t end, std::uint64_t generation, bool walked) const {
@@ -1143,23 +1166,13 @@ struct WriteTracker {
void stamp(std::uint64_t block, std::uint32_t stampGeneration, StampKind kind) {
const bool cpu = kind == StampKind::Cpu || kind == StampKind::ImportWindow;
const bool stored = kind != StampKind::ImportWindow;
#ifdef _WIN32
auto& leaf = leafFor(block);
if (stored) {
blocks[block] = stampGeneration;
auto& coarse = coarseBlocks[block / CoarseBlocks];
leaf.blocks[block % LeafBlocks] = stampGeneration;
auto& coarse = leaf.coarseBlocks[(block % LeafBlocks) / CoarseBlocks];
coarse = std::max(coarse, stampGeneration);
}
if (cpu) cpuBlocks[block] = stampGeneration;
#else
auto& leaf = leaves[block / LeafBlocks];
if (leaf == nullptr) leaf = std::make_unique<Leaf>();
if (stored) {
leaf->blocks[block % LeafBlocks] = stampGeneration;
auto& coarse = leaf->coarseBlocks[(block % LeafBlocks) / CoarseBlocks];
coarse = std::max(coarse, stampGeneration);
}
if (cpu) leaf->cpuBlocks[block % LeafBlocks] = stampGeneration;
#endif
if (cpu) leaf.cpuBlocks[block % LeafBlocks] = stampGeneration;
}
};
@@ -1191,6 +1204,7 @@ void watchPrivateMapping(std::uintptr_t address, std::size_t bytes, std::uint64_
++tracker.generation;
tracker.noteCpuStore(address, address + bytes, tracker.generation);
for (auto block = tracker.blockOf(address); block <= tracker.blockOf(address + bytes - 1); ++block) {
if (const auto& leaf = tracker.leaves[block / WriteTracker::LeafBlocks]; leaf != nullptr) leaf->hintBlocks[block % WriteTracker::LeafBlocks] = 0;
if (const auto found = tracker.driverPieces.find(block); found != tracker.driverPieces.end()) found->second.dropped = std::max<std::uint32_t>(found->second.dropped, tracker.generation);
tracker.stamp(block, tracker.generation, StampKind::Cpu);
}
@@ -1279,9 +1293,14 @@ void stampWrittenRun(void* context, std::uintptr_t begin, std::uintptr_t end) {
}
#ifdef _WIN32
std::atomic<FreshCollectHook> freshCollectHook{nullptr};
bool collectWindowsWrites(StampRuns& runs, std::uint64_t first, std::uint64_t stop, bool armedOnly) {
auto& tracker = runs.tracker;
const bool fresh = runs.kind == StampKind::Fresh;
if (fresh) {
if (const auto hook = freshCollectHook.load(std::memory_order_acquire)) return hook(static_cast<std::uintptr_t>(first), static_cast<std::size_t>(stop - first), &stampWrittenRun, &runs);
}
const auto collect = fresh ? &GuestArena::GuestArenaCollectFreshWrites_nid_postfix : armedOnly ? &GuestArena::GuestArenaCollectArmedWrites_nid_postfix : &GuestArena::GuestArenaCollectWrites_nid_postfix;
static const bool singlePass = std::getenv("APS5_NO_SINGLE_PASS_COLLECT") == nullptr;
const auto walk = [&](bool clear) {
@@ -1465,6 +1484,12 @@ void SetUnwatchHook(void (*hook)(std::uint64_t, std::size_t)) {
unwatchHook.store(hook, std::memory_order_release);
}
#ifdef _WIN32
void SetFreshCollectHook(FreshCollectHook hook) {
freshCollectHook.store(hook, std::memory_order_release);
}
#endif
void Unwatch(std::uint64_t address, std::size_t bytes) {
if (bytes == 0 || bytes > std::numeric_limits<std::uint64_t>::max() - address) return;
beforeUnwatch(address, bytes);
@@ -1517,39 +1542,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;
}
@@ -1660,7 +1701,7 @@ std::uint64_t StoreOwnBytes(std::uint64_t address, std::span<const std::byte> so
std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
auto& tracker = Tracker();
const auto lock = lockTracker(tracker);
auto lock = lockTracker(tracker);
tracker.initialize();
if (bytes == 0) return 0;
++tracker.generation;
@@ -1679,6 +1720,9 @@ std::uint64_t MarkWritten(std::uint64_t address, std::size_t bytes) {
const bool complete = GuestWriteWatch::GuestWriteWatchCollectFresh_nid_postfix(static_cast<std::uintptr_t>(blockFirst), static_cast<std::size_t>(blockStop - blockFirst), &stampWrittenRun, &runs);
#endif
if (!complete) {
lock.unlock();
beforeUnwatch(blockFirst, static_cast<std::size_t>(blockStop - blockFirst));
lock.lock();
unwatchLocked(tracker, blockFirst, static_cast<std::size_t>(blockStop - blockFirst));
tracker.noteUnstampedStore(address, address + bytes);
return 0;
@@ -1760,7 +1804,7 @@ bool walkHints(WriteTracker& tracker, std::uint64_t first, std::uint64_t stop) {
if (!GuestArena::GuestArenaCollectWrites_nid_postfix(static_cast<std::uintptr_t>(cursor), static_cast<std::size_t>(stop - cursor), tracker.pages.data(), &count, true)) return false;
if (count != 0 && !marked) ++tracker.generation;
marked = marked || count != 0;
for (std::size_t i = 0; i < count; ++i) tracker.hintBlocks[tracker.blockOf(reinterpret_cast<std::uintptr_t>(tracker.pages[i]))] = tracker.generation;
for (std::size_t i = 0; i < count; ++i) tracker.stampHint(tracker.blockOf(reinterpret_cast<std::uintptr_t>(tracker.pages[i])));
if (count < tracker.pages.size()) break;
cursor = reinterpret_cast<std::uintptr_t>(tracker.pages[count - 1]) + WritePageBytes;
}
@@ -1,6 +1,7 @@
#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp"
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <mutex>
namespace AgcDriver {
@@ -24,7 +25,7 @@ bool SameDisplay(const HostDisplayHdr& a, const HostDisplayHdr& b) {
void Publish(HostHdrStatus& status, const HostDisplayHdr& display, bool surfaceHdr10) {
const bool displayChanged = !SameDisplay(status.display, display);
const bool available = display.hdr && surfaceHdr10;
const bool available = display.hdr && surfaceHdr10 && std::getenv("APS5_FORCE_SDR") == nullptr;
const bool availabilityChanged = available != status.available;
status.display = display;
status.surfaceHdr10 = surfaceHdr10;
@@ -538,14 +538,8 @@ PerformanceOverlay::PerformanceOverlay(const Graphics::Context& context, std::si
layoutInfo.pushConstantRangeCount = 1;
layoutInfo.pPushConstantRanges = &push;
Graphics::Check(context.Function<PFN_vkCreatePipelineLayout>("vkCreatePipelineLayout")(context.device, &layoutInfo, nullptr, &pipelineLayout), "vkCreatePipelineLayout performance overlay");
const auto createModule = context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule");
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_VERT_SPV);
moduleInfo.pCode = PERFORMANCE_OVERLAY_VERT_SPV;
Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule performance overlay vertex");
moduleInfo.codeSize = sizeof(PERFORMANCE_OVERLAY_FRAG_SPV);
moduleInfo.pCode = PERFORMANCE_OVERLAY_FRAG_SPV;
Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule performance overlay fragment");
vertex = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_VERT_SPV, sizeof(PERFORMANCE_OVERLAY_VERT_SPV), "vkCreateShaderModule performance overlay vertex");
fragment = Graphics::CreateShaderModule(context, PERFORMANCE_OVERLAY_FRAG_SPV, sizeof(PERFORMANCE_OVERLAY_FRAG_SPV), "vkCreateShaderModule performance overlay fragment");
} catch (...) {
release();
throw;
@@ -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();
}
}
@@ -276,7 +276,7 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
break;
}
case 0x0c: break;
case 0x17: graphics(); size(6); break;
case 0x17: size(6); break;
case 0x1a:
graphics();
require(packet.size() == 3 || packet.size() == 5, "invalid context-state packet size");
@@ -945,15 +945,29 @@ void NoteDispatchRegisters(std::span<const std::uint32_t> packet, QueueState& qu
}
}
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue) {
bool IndirectRegistersValid(std::span<const std::uint32_t> pairs) {
for (std::size_t i = 0; i < pairs.size(); i += 2) {
if (pairs[i] == 0xffffffffu || (pairs[i] & ~0x70000000u) > 0xffffu) return false;
}
return true;
}
void ExecuteIndirectRegisters(std::span<const std::uint32_t> packet, std::span<const std::uint32_t> pairs, QueueState& queue, bool skipInvalid) {
const auto opcode = (packet[0] >> 8u) & 0xffu;
require(packet.size() == 5 && IndirectRegisterOpcode(opcode), "expected indirect register packet");
require(pairs.size() == static_cast<std::size_t>(packet[4]) * 2, "indirect register list does not match its packet");
for (std::size_t i = 0; i < pairs.size(); i += 2) registerOffset(pairs[i]);
for (std::size_t i = 0; i < pairs.size(); i += 2) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
const auto usable = [&](std::uint32_t value) { return !skipInvalid || IndirectRegistersValid(std::span(&value, 1)); };
for (std::size_t i = 0; i < pairs.size(); i += 2) {
if (usable(pairs[i])) registerOffset(pairs[i]);
}
for (std::size_t i = 0; i < pairs.size(); i += 2) {
if (usable(pairs[i])) writeRegister(queue, opcode, registerOffset(pairs[i]), pairs[i + 1]);
}
if (queue.savedContext.has_value() && TraceContextState()) {
std::fprintf(stderr, "[context] indirect 0x%x:", opcode);
for (std::size_t i = 0; i < pairs.size(); i += 2) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
for (std::size_t i = 0; i < pairs.size(); i += 2) {
if (usable(pairs[i])) std::fprintf(stderr, " %x", registerOffset(pairs[i]));
}
std::fprintf(stderr, "\n");
}
}
@@ -70,14 +70,8 @@ PresentationPass::PresentationPass(const Graphics::Context& context, std::size_t
tableWrites[i].pBufferInfo = &tableInfo;
}
context.Function<PFN_vkUpdateDescriptorSets>("vkUpdateDescriptorSets")(context.device, static_cast<std::uint32_t>(tableWrites.size()), tableWrites.data(), 0, nullptr);
const auto createModule = context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule");
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = sizeof(PRESENTATION_PASS_VERT_SPV);
moduleInfo.pCode = PRESENTATION_PASS_VERT_SPV;
Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &vertex), "vkCreateShaderModule presentation vertex");
moduleInfo.codeSize = sizeof(PRESENTATION_PASS_FRAG_SPV);
moduleInfo.pCode = PRESENTATION_PASS_FRAG_SPV;
Graphics::Check(createModule(context.device, &moduleInfo, nullptr, &fragment), "vkCreateShaderModule presentation fragment");
vertex = Graphics::CreateShaderModule(context, PRESENTATION_PASS_VERT_SPV, sizeof(PRESENTATION_PASS_VERT_SPV), "vkCreateShaderModule presentation vertex");
fragment = Graphics::CreateShaderModule(context, PRESENTATION_PASS_FRAG_SPV, sizeof(PRESENTATION_PASS_FRAG_SPV), "vkCreateShaderModule presentation fragment");
} catch (...) {
release();
throw;
@@ -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);
@@ -15,21 +15,26 @@
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
#include "prx/libSceAgcDriver/Execution/include/DeviceFault.hpp"
#include "prx/libSceAgcDriver/Execution/include/FragmentBarycentric.hpp"
#include "prx/libSceAgcDriver/Execution/include/Float32FlushControl.hpp"
#include "prx/libSceAgcDriver/Execution/include/PipelineLinkOptimization.hpp"
#include "prx/libSceAgcDriver/Execution/include/DrawInputsInPlace.hpp"
#include "prx/libSceAgcDriver/Execution/include/RobustBufferAccess.hpp"
#include "prx/libSceAgcDriver/Execution/include/SubgroupBarrier.hpp"
#include "prx/libSceAgcDriver/Execution/include/SubgroupClock.hpp"
#include "prx/libSceAgcDriver/Execution/include/ShaderDeviceProfile.hpp"
#include "prx/libSceAgcDriver/Execution/include/PresentMode.hpp"
#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"
#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp"
#include "prx/libSceAgcDriver/Execution/include/HostHdr.hpp"
#include "prx/libSceAgcDriver/Graphics/include/MultisampleTarget.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp"
#include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp"
#include "prx/libSceAgcDriver/Graphics/include/BufferPool.hpp"
@@ -62,6 +67,7 @@
#include <array>
#include <optional>
#include <algorithm>
#include <bit>
#include <cstring>
#include <deque>
#include <limits>
@@ -155,24 +161,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 +199,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();
}
@@ -311,10 +324,12 @@ struct VulkanDevice::State {
bool float16DenormPreserve = false;
bool float32FlushToZeroCandidate = false;
bool float32FlushToZero = false;
bool float32DenormPreserve = false;
bool probingFloatControls = false;
bool demoteToHelperInvocation = false;
bool earlyFragmentSampleMaskTestBeforeSampleCounting = false;
bool robustBufferAccess2 = false;
bool subgroupControlBarriers = true;
// VK_EXT_descriptor_indexing with non-uniform image array indexing (bindless image tables in
// graphics stages, and compute workgroups wider than a wave).
bool descriptorIndexing = false;
@@ -334,11 +349,15 @@ struct VulkanDevice::State {
bool imageViewMinLod = false;
bool pipelineExecutableInfo = false;
bool maintenance8 = false;
bool maintenance5 = false;
bool fillModeNonSolid = false;
std::uint32_t srgbDecodeFormats = 0;
std::uint32_t srgbCopyFormats = 0;
bool storageImageWithoutFormat = false;
VkSampleCountFlags sampleLocationSampleCounts = 0;
VkSampleCountFlags sampleLocationGridSampleCounts = 0;
std::array<VkExtent2D, 4> sampleLocationGridSizes{};
bool shaderStencilExport = false;
bool depthClamp = false;
bool depthBounds = false;
bool depthBiasClamp = false;
@@ -449,6 +468,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 +500,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 +699,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 +714,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
@@ -757,6 +783,7 @@ struct VulkanDevice::State {
Graphics::ClearCachedPipelines(device);
Graphics::ClearDepthSurfaces(device);
Graphics::ClearMultisampleTargets(device);
Graphics::ClearSampleOrders(device);
{
std::lock_guard pipelines(computePipelinesMutex);
computePipelines.clear();
@@ -898,10 +925,7 @@ ComputePipelineReplayResult VulkanDevice::ReplayComputePipeline(const ComputePip
check(state->DeviceFunction<PFN_vkCreatePipelineCache>("vkCreatePipelineCache")(state->device, &cacheInfo, nullptr, &replay.cache), "vkCreatePipelineCache replay");
result.cacheMilliseconds = elapsed(phase);
phase = std::chrono::steady_clock::now();
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = capture.spirv.size() * sizeof(std::uint32_t);
moduleInfo.pCode = capture.spirv.data();
check(state->DeviceFunction<PFN_vkCreateShaderModule>("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects.module), "vkCreateShaderModule replay");
objects.module = Graphics::CreateShaderModule(state->DeviceFunction<PFN_vkCreateShaderModule>("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, capture.spirv.data(), capture.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule replay");
result.moduleMilliseconds = elapsed(phase);
phase = std::chrono::steady_clock::now();
std::array<std::array<std::uint64_t, 4>, 2> descriptorCounts{};
@@ -1145,12 +1169,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
std::vector<VkExtensionProperties> availableExtensions(extensionCount);
check(enumerateDeviceExtensions(selected, nullptr, &extensionCount, availableExtensions.data()), "vkEnumerateDeviceExtensionProperties");
const auto hasExtension = [&](const char* name) { return std::any_of(availableExtensions.begin(), availableExtensions.end(), [&](const auto& item) { return std::strcmp(item.extensionName, name) == 0; }); };
VkDriverId driverId{};
if (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};
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &driverProperties);
driverId = driver.driverID;
state->driverInfo = std::string(driver.driverName) + " " + driver.driverInfo;
state->individuallyFreeDescriptorSets = driver.driverID == VK_DRIVER_ID_MESA_LLVMPIPE;
state->subgroupControlBarriers = SubgroupControlBarrierUsable(driver.driverID);
}
auto byteFeatures = QueryBdaByteFeatures(selected, state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), availableExtensions);
auto bdaFeatures = QueryBdaFeatures(selected, state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2"), availableExtensions);
@@ -1248,6 +1275,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
deviceExtensions.push_back(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
state->capabilities.push_back(spv::CapabilityStencilExportEXT);
state->spirvExtensions.push_back("SPV_EXT_shader_stencil_export");
state->shaderStencilExport = true;
}
if (state->shaderClock) {
deviceExtensions.push_back(VK_KHR_SHADER_CLOCK_EXTENSION_NAME);
@@ -1310,9 +1338,11 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
}
state->float16DenormPreserve = floatControls.shaderDenormPreserveFloat16 == VK_TRUE &&
floatControls.denormBehaviorIndependence == VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_ALL;
state->float32FlushToZeroCandidate = floatControls.shaderDenormFlushToZeroFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr;
if (state->float32FlushToZeroCandidate) state->capabilities.push_back(spv::CapabilityDenormFlushToZero);
if (state->float16DenormPreserve && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve);
const auto float32Flush = Float32FlushControlFor(driverId, floatControls.shaderDenormFlushToZeroFloat32, floatControls.denormBehaviorIndependence);
state->float32FlushToZeroCandidate = float32Flush.probe && std::getenv("APS5_NO_HW_F32_FLUSH") == nullptr;
if (state->float32FlushToZeroCandidate && float32Flush.declared) state->capabilities.push_back(spv::CapabilityDenormFlushToZero);
state->float32DenormPreserve = floatControls.shaderDenormPreserveFloat32 == VK_TRUE && floatControls.denormBehaviorIndependence != VK_SHADER_FLOAT_CONTROLS_INDEPENDENCE_NONE && std::getenv("APS5_NO_HW_F32_PRESERVE") == nullptr;
if ((state->float16DenormPreserve || state->float32DenormPreserve) && std::find(state->capabilities.begin(), state->capabilities.end(), static_cast<std::uint32_t>(spv::CapabilityDenormPreserve)) == state->capabilities.end()) state->capabilities.push_back(spv::CapabilityDenormPreserve);
deviceExtensions.push_back(VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME);
deviceExtensions.push_back(VK_KHR_8BIT_STORAGE_EXTENSION_NAME);
state->capabilities.push_back(4448);
@@ -1328,13 +1358,18 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
if (locations.sampleLocationSubPixelBits >= 4 && locations.sampleLocationCoordinateRange[0] <= 0.0f && locations.sampleLocationCoordinateRange[1] >= 15.0f / 16.0f) {
const auto multisampleProperties = state->InstanceFunction<PFN_vkGetPhysicalDeviceMultisamplePropertiesEXT>("vkGetPhysicalDeviceMultisamplePropertiesEXT");
std::string grids;
for (const auto samples : {VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT, VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT}) {
if ((locations.sampleLocationSampleCounts & samples) == 0) continue;
VkMultisamplePropertiesEXT multisample{VK_STRUCTURE_TYPE_MULTISAMPLE_PROPERTIES_EXT};
multisampleProperties(selected, samples, &multisample);
if (multisample.maxSampleLocationGridSize.width != 0 && multisample.maxSampleLocationGridSize.height != 0) state->sampleLocationSampleCounts |= samples;
if (multisample.maxSampleLocationGridSize.width % 2u == 0 && multisample.maxSampleLocationGridSize.height % 2u == 0) state->sampleLocationGridSampleCounts |= samples;
const auto grid = multisample.maxSampleLocationGridSize;
state->sampleLocationGridSizes[static_cast<std::size_t>(std::countr_zero(static_cast<std::uint32_t>(samples)))] = grid;
if (grid.width != 0 && grid.height != 0) state->sampleLocationSampleCounts |= samples;
if (AgcDriver::Graphics::HostsSampleLocationGrid(grid)) state->sampleLocationGridSampleCounts |= samples;
grids += (grids.empty() ? "" : ", ") + std::to_string(static_cast<std::uint32_t>(samples)) + " samples " + std::to_string(grid.width) + "x" + std::to_string(grid.height);
}
if (!grids.empty()) std::fprintf(stderr, "[gpu] largest sample location grids (vkGetPhysicalDeviceMultisamplePropertiesEXT): %s; maxSampleLocationGridSize for every count %ux%u\n", grids.c_str(), locations.maxSampleLocationGridSize.width, locations.maxSampleLocationGridSize.height);
}
if (state->sampleLocationSampleCounts != 0) deviceExtensions.push_back(VK_EXT_SAMPLE_LOCATIONS_EXTENSION_NAME);
}
@@ -1485,7 +1520,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 +1529,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};
@@ -1502,6 +1539,15 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
state->pipelineLinkTimeOptimization = PipelineLinkTimeOptimizationUsable(driver.driverID);
}
}
VkPhysicalDeviceMaintenance5FeaturesKHR maintenance5Features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR};
if (state->graphicsPipelineLibrary && hasExtension(VK_KHR_MAINTENANCE_5_EXTENSION_NAME)) {
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &maintenance5Features};
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
state->maintenance5 = maintenance5Features.maintenance5 == VK_TRUE;
if (state->maintenance5) deviceExtensions.push_back(VK_KHR_MAINTENANCE_5_EXTENSION_NAME);
}
maintenance5Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MAINTENANCE_5_FEATURES_KHR};
maintenance5Features.maintenance5 = VK_TRUE;
renderingFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_DYNAMIC_RENDERING_FEATURES_KHR, nullptr, VK_TRUE};
libraryFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_GRAPHICS_PIPELINE_LIBRARY_FEATURES_EXT, &dynamicStateFeatures, VK_TRUE};
dynamicStateFeatures = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_EXTENDED_DYNAMIC_STATE_FEATURES_EXT, &renderingFeatures, VK_TRUE};
@@ -1631,6 +1677,8 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
// PA_CL_CLIP_CNTL near/far clip disable maps to depth clamping.
enabled.depthClamp = available.depthClamp;
state->depthClamp = enabled.depthClamp == VK_TRUE;
enabled.fillModeNonSolid = available.fillModeNonSolid;
state->fillModeNonSolid = enabled.fillModeNonSolid == VK_TRUE;
enabled.depthBounds = available.depthBounds;
state->depthBounds = enabled.depthBounds == VK_TRUE;
enabled.depthBiasClamp = available.depthBiasClamp;
@@ -1720,7 +1768,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;
}
@@ -1742,6 +1790,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window, bool compileOnly) :
fifoLatestReadyFeatures.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &fifoLatestReadyFeatures;
}
if (state->maintenance5) {
maintenance5Features.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &maintenance5Features;
}
if (state->maintenance8) {
maintenance8Features.pNext = const_cast<void*>(deviceInfo.pNext);
deviceInfo.pNext = &maintenance8Features;
@@ -2100,6 +2152,10 @@ void VulkanDevice::WaitForPipelineJournalReplay() {
if (state->journalReplay != nullptr) state->journalReplay->Wait();
}
void VulkanDevice::SavePipelineCache() {
if (state->pipelineCache) state->pipelineCache->SaveNow();
}
std::uint64_t VulkanDevice::ComputePipelinesCreated() const {
return state->computePipelinesCreated.load(std::memory_order_relaxed);
}
@@ -2223,6 +2279,10 @@ bool VulkanDevice::OpenWriteOverlaps(std::uint64_t address, std::size_t bytes) c
return state->recorder && state->recorder->OpenWriteOverlaps(address, bytes);
}
void VulkanDevice::SealPassWriteOnly() {
if (state->recorder) state->recorder->SealPassWriteOnly();
}
bool VulkanDevice::RecordedWritesSettled(std::uint64_t address, std::size_t bytes) const {
if (!state->recorder) return true;
const auto& recorder = *state->recorder;
@@ -2518,44 +2578,63 @@ std::atomic<std::uint64_t> copiesAliased{0}, copyAliasCreated{0}, copyAliasNoSou
// source range must be exactly a live image's surface, and the destination range either exactly
// a live image of the same shape or none (one is made from the source's descriptor rebased). Under
// GuestMemory::GpuMutex. Debug aid: APS5_NO_COPY_ALIAS=1 keeps every such copy a transfer.
bool AliasCopy(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) {
bool CopyAliasDisabled() {
static const bool disabled = std::getenv("APS5_NO_COPY_ALIAS") != nullptr;
if (disabled) return false;
auto from = Graphics::StorageTexture::FindLive(source, bytes);
if (from == nullptr) {
return disabled;
}
struct AliasPlan {
std::shared_ptr<Graphics::StorageTexture> from;
std::shared_ptr<Graphics::StorageTexture> to;
const char* refusal = nullptr;
};
AliasPlan PlanAlias(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) {
AliasPlan plan;
plan.from = Graphics::StorageTexture::FindLive(source, bytes);
if (plan.from == nullptr) {
plan.refusal = "no source";
return plan;
}
plan.to = Graphics::StorageTexture::FindLive(destination, bytes);
plan.refusal = Graphics::StorageTexture::CopyRefusal(context, plan.to.get(), *plan.from, destination);
return plan;
}
bool AliasCopy(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) {
if (CopyAliasDisabled()) return false;
auto plan = PlanAlias(context, destination, source, bytes);
if (plan.from == nullptr) {
copyAliasNoSource.fetch_add(1, std::memory_order_relaxed);
return false;
}
auto to = Graphics::StorageTexture::FindLive(destination, bytes);
if (to != nullptr && !to->SameSurfaceShape(*from)) {
copyAliasShape.fetch_add(1, std::memory_order_relaxed);
if (plan.refusal != nullptr) {
if (plan.to != nullptr && !plan.to->SameSurfaceShape(*plan.from)) {
copyAliasShape.fetch_add(1, std::memory_order_relaxed);
} else {
static std::atomic<int> reported{0};
if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] image copy 0x%llx -> 0x%llx (0x%zx bytes) refused: %s\n", static_cast<unsigned long long>(source), static_cast<unsigned long long>(destination), bytes, plan.refusal);
copyAliasRefused.fetch_add(1, std::memory_order_relaxed);
}
return false;
}
if (to == nullptr) {
auto resource = from->Descriptor();
if (plan.to == nullptr) {
auto resource = plan.from->Descriptor();
resource.baseAddress = destination;
resource.dccAddress = 0;
try {
to = Graphics::CachedStorageSurface(context, resource);
} catch (const std::exception& error) {
static std::atomic<int> reported{0};
if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] no storage image for the copy destination 0x%llx: %s\n", static_cast<unsigned long long>(destination), error.what());
copyAliasRefused.fetch_add(1, std::memory_order_relaxed);
return false;
}
plan.to = Graphics::CachedStorageSurface(context, resource);
copyAliasCreated.fetch_add(1, std::memory_order_relaxed);
}
const char* refusal = nullptr;
if (!to->CopyFrom(*from, refusal)) {
static std::atomic<int> reported{0};
if (reported.fetch_add(1) < 8) std::fprintf(stderr, "[copy] image copy 0x%llx -> 0x%llx (0x%zx bytes) refused: %s\n", static_cast<unsigned long long>(source), static_cast<unsigned long long>(destination), bytes, refusal);
copyAliasRefused.fetch_add(1, std::memory_order_relaxed);
return false;
}
if (!plan.to->CopyFrom(*plan.from, refusal)) throw std::runtime_error(std::string("AGC graphics: an image copy its check allowed was refused: ") + refusal);
copiesAliased.fetch_add(1, std::memory_order_relaxed);
return true;
}
bool AliasCopyPossible(const Graphics::Context& context, std::uint64_t destination, std::uint64_t source, std::size_t bytes) {
return !CopyAliasDisabled() && PlanAlias(context, destination, source, bytes).refusal == nullptr;
}
}
VulkanDevice::CopyVerification VulkanDevice::CopyVerifyCounts() {
@@ -2575,21 +2654,34 @@ VulkanDevice::CopyAliasing VulkanDevice::CopyAliasCounts() {
return {copiesAliased.load(std::memory_order_relaxed), copyAliasCreated.load(std::memory_order_relaxed), copyAliasNoSource.load(std::memory_order_relaxed), copyAliasShape.load(std::memory_order_relaxed), copyAliasRefused.load(std::memory_order_relaxed)};
}
bool VulkanDevice::completionStoresOver(std::uint64_t address, std::size_t bytes) const {
static const bool alwaysSync = std::getenv("APS5_COPY_SYNC") != nullptr;
if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true;
return Graphics::Recorder::PendingCompletionLabels() != 0 && state->recorder->CompletionLabelIn(address, bytes);
}
bool VulkanDevice::copyWaitsOnCompletions(std::uint64_t address, std::size_t bytes) const {
return state->recorder->PendingWriteOverlaps(address, bytes) && completionStoresOver(address, bytes);
}
bool VulkanDevice::GpuCopyPossible(std::uint64_t destination, std::uint64_t source, std::size_t bytes) const {
if (!state->recorder || bytes == 0 || copyWaitsOnCompletions(source, bytes) || copyWaitsOnCompletions(destination, bytes)) return false;
const auto context = graphicsContext();
if (AliasCopyPossible(context, destination, source, bytes)) return true;
return Graphics::HostImportFor(context, destination, bytes) != nullptr && Graphics::HostImportFor(context, source, bytes) != nullptr;
}
VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, std::uint64_t source, std::size_t bytes, std::size_t cpuMax, std::size_t gpuMax, std::size_t knownMax, std::uint64_t programAddress, std::uint32_t queue, const CopyWriterNote& noteWriter) {
CopyOutcome outcome{2, false, 0, CopyOutcome::None, false, false, 0, 0, 0, 0, false};
if (!state->recorder || bytes == 0) return outcome;
auto& recorder = *state->recorder;
// Finished batches were retired by the caller (Driver.cpp copyBuffer, on the graphics worker
// only, as WriteLabelOnGpu reaps), so their writes neither refuse the CPU copy nor force a wait.
static const bool alwaysSync = std::getenv("APS5_COPY_SYNC") != nullptr;
static const bool waitForSource = std::getenv("APS5_COPY_WAIT_SOURCE") != nullptr;
static const bool verify = std::getenv("APS5_COPY_VERIFY") != nullptr;
// A store a batch's completion makes on the CPU over the range (a copied buffer's write-back,
// a completion-deferred label): it lands only when that batch is reaped, whatever its fence.
const auto completionStoreOverlaps = [&](std::uint64_t address) {
if (alwaysSync || state->CopiedWriterOverlaps(address, bytes)) return true;
return Graphics::Recorder::PendingCompletionLabels() != 0 && recorder.CompletionLabelIn(address, bytes);
};
const auto completionStoreOverlaps = [&](std::uint64_t address) { return completionStoresOver(address, bytes); };
// The CPU decision (see the header): pure queries, in the order that makes the first failing
// test the reason; a signaled writer of the source is fine (its in-place stores are final in
// host memory) as long as no completion store or label of it is still to land.
@@ -2667,8 +2759,8 @@ VulkanDevice::CopyOutcome VulkanDevice::CopyBuffer(std::uint64_t destination, st
// before the transfer (or the device copy below) by its barrier; a store a batch's completion
// makes on the CPU must land before the copy reads the source or writes the destination, so
// the copy waits for that batch (SyncThrough) first.
const bool waitSource = recorder.PendingWriteOverlaps(source, bytes) && completionStoreOverlaps(source);
const bool waitDestination = recorder.PendingWriteOverlaps(destination, bytes) && completionStoreOverlaps(destination);
const bool waitSource = copyWaitsOnCompletions(source, bytes);
const bool waitDestination = copyWaitsOnCompletions(destination, bytes);
if (waitSource || waitDestination) {
outcome.synced = true;
const auto syncStart = std::chrono::steady_clock::now();
@@ -3028,6 +3120,13 @@ std::optional<std::array<std::byte, 4>> CompressedClearPixel(const DisplayBuffer
return DisplayBufferClearPixel(buffer, keys);
}
std::vector<std::byte> DisplayPixels(const DisplayBuffer& buffer, const std::optional<std::array<std::byte, 4>>& cleared) {
if (!cleared) return ReadDisplayBuffer(buffer);
std::vector<std::byte> pixels(static_cast<std::size_t>(buffer.width) * buffer.height * cleared->size());
for (std::size_t offset = 0; offset < pixels.size(); offset += cleared->size()) std::memcpy(pixels.data() + offset, cleared->data(), cleared->size());
return pixels;
}
// Debug aid: APS5_NO_RESIDENT_PRESENT=1 always presents through guest memory.
bool NoResidentPresent() {
static const bool no = std::getenv("APS5_NO_RESIDENT_PRESENT") != nullptr;
@@ -3079,10 +3178,14 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
dumpFrame = true;
}
}
if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame, false, nullptr, 0, passthrough)) {
const bool screenshot = takeScreenshot(dumpFrame);
auto shot = screenshot && passthrough ? ReadDisplayBuffer(buffer) : std::vector<std::byte>{};
if (!present(buffer.width, buffer.height, true, pixels, nullptr, nullptr, VK_FILTER_LINEAR, dumpFrame || (screenshot && !passthrough), false, nullptr, 0, passthrough)) {
if (dumpFrame) --dumps.dumped;
if (screenshot) Screenshot::Request();
return false;
}
if (!shot.empty()) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot));
return true;
}
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
@@ -3128,9 +3231,7 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
bool dumpFrame = false;
if (frameDumpOutput.Enabled_nid_no_patch() && dumps.dumped < dumps.limit && ++dumps.presents % static_cast<std::uint64_t>(dumps.every) == 0) {
if (dumps.cpu || (passthrough && resident == nullptr)) {
auto full = cleared ? std::vector<std::byte>(static_cast<std::size_t>(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer);
for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size());
WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale());
WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, DisplayPixels(buffer, cleared), DumpScale());
} else {
state->nextDumpIndex = dumps.dumped++;
dumpFrame = true;
@@ -3141,6 +3242,9 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
lastReport = std::chrono::steady_clock::now();
AgcDriver::ProfilePrint_nid_no_patch("[flip] (cumulative): %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast<unsigned long long>(residentPresents), static_cast<unsigned long long>(refreshedPresents), static_cast<unsigned long long>(notPending), static_cast<unsigned long long>(unsuitable), static_cast<unsigned long long>(gpuDumps));
}
const bool screenshot = takeScreenshot(dumpFrame);
const bool cpuScreenshot = screenshot && passthrough && resident == nullptr;
auto shot = cpuScreenshot ? DisplayPixels(buffer, cleared) : std::vector<std::byte>{};
CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast<unsigned long long>(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast<unsigned long long>(resident ? resident->Generation() : 0));
if (passthrough && resident == nullptr) ensureHdrScaler(DisplayTexelFormat(buffer.pixelFormat));
VkClearColorValue uniform{};
@@ -3150,14 +3254,21 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) {
const auto channel = [&](std::size_t index) { return static_cast<float>(std::to_integer<unsigned>((*cleared)[index])) / 255.0f; };
uniform = {{channel(2), channel(1), channel(0), channel(3)}};
}
if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr, layer, passthrough)) {
if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame || (screenshot && !cpuScreenshot), convert, cleared ? &uniform : nullptr, layer, passthrough)) {
// A dropped frame (swapchain out of date) keeps the dump numbering contiguous.
if (dumpFrame) --dumps.dumped;
if (screenshot) Screenshot::Request();
return false;
}
if (cpuScreenshot) state->dumpWriter.Enqueue(0, buffer.width, buffer.height, 1, true, std::move(shot));
return true;
}
bool VulkanDevice::takeScreenshot(bool dumpFrame) {
state->nextDumpScreenshot = !dumpFrame && state->dumpWriter.Idle() && Screenshot::Take();
return state->nextDumpScreenshot;
}
bool VulkanDevice::AcquireImage() {
PerformanceTimer timing("Vulkan.Acquire");
require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain");
@@ -3331,7 +3442,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu
else scaler->RecordBlit(commands, barrier.image, state->extent.width, state->extent.height);
}
if (dumpFrame) {
const auto scale = DumpScale();
const auto scale = state->nextDumpScreenshot ? 1 : DumpScale();
if (drawn || direct) {
const auto extent = PresentationPass::DumpExtent(width, height, scale);
slot.dumpWidth = extent.width;
@@ -3399,6 +3510,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;
@@ -3563,9 +3675,15 @@ ShaderRecompiler::SpirvTarget VulkanDevice::Target() const {
require(state->shaderProfile != nullptr, "shader device profile is unavailable");
auto target = state->shaderProfile->Target();
target.float32FlushToZero = state->float32FlushToZero;
target.float32DenormPreserve = state->float32DenormPreserve;
return target;
}
Graphics::SampleOrder VulkanDevice::HostSampleOrder(const Graphics::State& graphics) const {
if (!graphics.sampleLocationGrid) return {};
return Graphics::HostSampleLocationsFor(graphicsContext(), graphics).order;
}
ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
const auto& limits = state->properties.limits;
ShaderRecompiler::SpirvTarget target{VK_API_VERSION_1_1, state->meshShader ? 0x00010400u : 0x00010300u, state->subgroup.subgroupSize, ShaderRecompiler::BdaAbi::Version, state->capabilities, state->spirvExtensions, false, {limits.maxComputeWorkGroupSize[0], limits.maxComputeWorkGroupSize[1], limits.maxComputeWorkGroupSize[2]}, limits.maxComputeWorkGroupInvocations, limits.maxComputeSharedMemorySize, {}, {}};
@@ -3579,6 +3697,7 @@ ShaderRecompiler::SpirvTarget VulkanDevice::buildTarget() const {
target.fixedPushSlots = state->graphicsPipelineLibrary;
target.registrationModules = state->graphicsPipelineLibrary;
target.robustBufferAccess2 = state->robustBufferAccess2;
target.waveLdsMemoryBarriers = !state->subgroupControlBarriers;
target.srgbDecodeFormats = state->srgbDecodeFormats & ~state->srgbCopyFormats;
target.vertexSubgroups = (state->subgroup.supportedStages & VK_SHADER_STAGE_VERTEX_BIT) != 0;
if (state->meshShader) {
@@ -3711,6 +3830,8 @@ Graphics::Context VulkanDevice::buildContext() const {
context.samplerTableUpdateAfterBind = SamplerTableUpdateAfterBind(context);
context.geometryShader = state->geometryShader;
context.sampleRateShading = state->sampleRateShading;
context.fillModeNonSolid = state->fillModeNonSolid;
context.maintenance5 = state->maintenance5;
context.storageImageMultisample = state->storageImageMultisample;
context.shaderClipDistance = state->shaderClipDistance;
context.shaderCullDistance = state->shaderCullDistance;
@@ -3728,7 +3849,9 @@ Graphics::Context VulkanDevice::buildContext() const {
context.srgbCopyFormats = state->srgbCopyFormats;
context.sampleLocationSampleCounts = state->sampleLocationSampleCounts;
context.sampleLocationGridSampleCounts = state->sampleLocationGridSampleCounts;
context.memoryProperties2 = state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
context.sampleLocationGridSizes = state->sampleLocationGridSizes;
context.shaderStencilExport = state->shaderStencilExport;
context.memoryProperties2 =state->memoryBudget ? state->InstanceFunction<PFN_vkGetPhysicalDeviceMemoryProperties2>("vkGetPhysicalDeviceMemoryProperties2") : nullptr;
context.singlePassStorage = state->storageImageWithoutFormat;
context.cacheReclaim = Graphics::ReleaseTextureCaches;
return context;
@@ -3799,6 +3922,10 @@ void VulkanDevice::Draw(const Graphics::State& graphics, const Pm4::DrawParamete
if (trace) APS5_LOG_CHARS_OUT("VulkanDevice::Draw complete");
}
bool VulkanDevice::DrawInputsInPlace() const {
return state->drawInputsInPlace;
}
namespace {
// The resource cache serves dispatches unless disabled; Revalidate proves a hit by texture identity,
@@ -4276,6 +4403,8 @@ struct RecordedDispatch {
std::shared_ptr<Graphics::ShaderResources::DrawBindings> bindings{};
std::uint64_t cpuArguments = 0;
int cpuReason = 0;
std::array<std::uint32_t, 3> threadGroup{};
std::array<std::uint32_t, 3> threadLimit{};
};
VkDevice VulkanDevice::Device() const {
@@ -4532,8 +4661,8 @@ void VulkanDevice::Dispatch(const ShaderRecompiler::RecompileResult& shader, std
static_cast<void>(dispatch(shader, x, y, z, 0, snapshots, programAddress, std::move(prepared), recipe));
}
VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe) {
return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe);
VulkanDevice::IndirectOutcome VulkanDevice::DispatchIndirect(const ShaderRecompiler::RecompileResult& shader, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipe, const std::array<std::uint32_t, 3>& threadGroup) {
return dispatch(shader, 0, 0, 0, arguments, snapshots, programAddress, std::move(prepared), recipe, threadGroup);
}
void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& outcome, char* groupsText) {
@@ -4541,6 +4670,7 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out
auto& timer = *record.timer;
const auto& context = *record.context;
const auto arguments = record.arguments;
const bool threadDimensions = record.threadGroup != std::array<std::uint32_t, 3>{};
// Decided here, after the resource build (which may retire imports and wait for recorded
// work) and before the dispatch itself is recorded, so the import cannot be dropped before
// the record. The build already recorded the copy-ins of its misaligned and staged regions
@@ -4567,7 +4697,8 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out
outcome.cpuReason = 8;
} else if (labelPending() || state->CopiedWriterOverlaps(arguments, 12)) {
outcome.cpuReason = 2;
} else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr) {
} else if ((record.argumentImport = Graphics::HostImportFor(context, arguments, 12)) == nullptr || (threadDimensions && record.argumentImport->address == 0)) {
record.argumentImport = nullptr;
// Valid until the next refreshImports, which only runs under GuestMemory::GpuMutex (held
// here, by every Dispatch caller) and Keeps a retired VkBuffer while the recorder is busy.
outcome.cpuReason = 3;
@@ -4584,6 +4715,10 @@ void VulkanDevice::decideIndirect(RecordedDispatch& record, IndirectOutcome& out
Graphics::FinishLeaseWork();
std::array<std::uint32_t, 3> groups{};
GuestMemory::Read(arguments, std::as_writable_bytes(std::span(groups)), 4);
if (threadDimensions) {
record.threadLimit = groups;
for (std::size_t axis = 0; axis < groups.size(); ++axis) groups[axis] = groups[axis] / record.threadGroup[axis] + (groups[axis] % record.threadGroup[axis] != 0 ? 1u : 0u);
}
outcome.argumentReadMs = std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - readStart).count();
record.x = groups[0];
record.y = groups[1];
@@ -4660,6 +4795,24 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
}
recordStep(PhaseRecordDataRefresh);
}
std::shared_ptr<Graphics::Buffer> threadArguments;
if (record.threadGroup != std::array<std::uint32_t, 3>{}) {
const auto data = resources.shaderDataBuffer(*record.shader);
const auto limitDword = record.shader->program->imageMetadataDword;
Graphics::Require(data != VK_NULL_HANDLE && limitDword >= 3, "Vulkan dispatch: a thread-dimension dispatch has no ShaderData thread limit");
const VkDeviceSize limitByte = static_cast<VkDeviceSize>(limitDword - 3) * sizeof(std::uint32_t);
Graphics::RecordMemoryBarrier(context, commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_TRANSFER_WRITE_BIT);
if (argumentImport != nullptr) {
threadArguments = std::make_shared<Graphics::Buffer>(context, 24, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT | VK_BUFFER_USAGE_TRANSFER_SRC_BIT);
Graphics::Require(recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), threadArguments->DeviceAddress(), &record.threadGroup), "Vulkan dispatch: thread-dimension indirect arguments need the argument kernel");
const VkBufferCopy limit{12, limitByte, 12};
context.Function<PFN_vkCmdCopyBuffer>("vkCmdCopyBuffer")(commands, threadArguments->Handle(), data, 1, &limit);
} else {
context.Resolved(&Graphics::DeviceFunctions::cmdUpdateBuffer, "vkCmdUpdateBuffer")(commands, data, limitByte, sizeof(record.threadLimit), record.threadLimit.data());
}
resources.forgetShaderDataWords(*record.shader);
covered = 0;
}
using CommandClass = Graphics::Recorder::CommandClass;
if (Graphics::Recorder::BarrierValidate()) {
if (argumentImport != nullptr) {
@@ -4693,7 +4846,11 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
}
VkBuffer indirectBuffer = argumentImport != nullptr ? argumentImport->buffer : VK_NULL_HANDLE;
VkDeviceSize indirectOffset = argumentImport != nullptr ? Graphics::HostImportOffset(*argumentImport, arguments) : 0;
if (argumentImport != nullptr && argumentImport->address != 0) {
if (threadArguments != nullptr) {
indirectBuffer = threadArguments->Handle();
indirectOffset = 0;
recorder.Keep(std::move(threadArguments));
} else if (argumentImport != nullptr && argumentImport->address != 0) {
auto checked = std::make_shared<Graphics::Buffer>(context, 12, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT);
if (recorder.RecordDispatchArguments(commands, argumentImport->address + (arguments - argumentImport->base), checked->DeviceAddress())) {
indirectBuffer = checked->Handle();
@@ -4786,6 +4943,7 @@ void VulkanDevice::recordDispatch(RecordedDispatch& record) {
// its bindings describe): what a dispatch or its preparation builds at the first use of a module,
// and what the journal replay builds on its own threads before that use.
std::shared_ptr<ComputePipelineObjects> VulkanDevice::createComputePipeline(const Graphics::Context& context, const ShaderRecompiler::RecompileResult& shader, VkDescriptorSetLayout setLayout, std::span<const std::uint32_t> layoutKey, VkShaderStageFlags pushStages, std::uint64_t programAddress) {
PerformanceTimer timing("Vulkan.ComputePipeline");
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
const auto start = std::chrono::steady_clock::now();
auto objects = std::make_shared<ComputePipelineObjects>();
@@ -4793,10 +4951,7 @@ std::shared_ptr<ComputePipelineObjects> VulkanDevice::createComputePipeline(cons
objects->destroyModule = state->DeviceFunction<PFN_vkDestroyShaderModule>("vkDestroyShaderModule");
objects->destroyLayout = state->DeviceFunction<PFN_vkDestroyPipelineLayout>("vkDestroyPipelineLayout");
objects->destroyPipeline = state->DeviceFunction<PFN_vkDestroyPipeline>("vkDestroyPipeline");
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t);
moduleInfo.pCode = shader.spirv.data();
check(state->DeviceFunction<PFN_vkCreateShaderModule>("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &objects->module), "vkCreateShaderModule");
objects->module = Graphics::CreateShaderModule(state->DeviceFunction<PFN_vkCreateShaderModule>("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule");
objects->resourcesLayout = setLayout;
const std::array<VkDescriptorSetLayout, 2> setLayouts{setLayout, shader.workgroupMemoryDwords != 0 ? state->workgroupMemoryLayout : VK_NULL_HANDLE};
const VkPushConstantRange push{VK_SHADER_STAGE_COMPUTE_BIT, 0, Graphics::ComputePushConstantBytes};
@@ -4856,11 +5011,7 @@ VkPipeline VulkanDevice::splitComputePipeline(const Graphics::Context& context,
std::array<ShaderRecompiler::PipelineSpecializationConstant, ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetWords> offsets{};
for (std::uint32_t component = 0; component < offsets.size(); ++component) offsets[component] = {ShaderRecompiler::PipelineSpecialization::WorkgroupOffsetBase + component, base[component]};
const Graphics::PipelineSpecialization specialization(shader, offsets);
VkShaderModuleCreateInfo moduleInfo{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
moduleInfo.codeSize = shader.spirv.size() * sizeof(std::uint32_t);
moduleInfo.pCode = shader.spirv.data();
VkShaderModule module = VK_NULL_HANDLE;
check(state->DeviceFunction<PFN_vkCreateShaderModule>("vkCreateShaderModule")(state->device, &moduleInfo, nullptr, &module), "vkCreateShaderModule");
VkShaderModule module = Graphics::CreateShaderModule(state->DeviceFunction<PFN_vkCreateShaderModule>("vkCreateShaderModule"), state->device, state->graphicsPipelineLibrary, shader.spirv.data(), shader.spirv.size() * sizeof(std::uint32_t), "vkCreateShaderModule");
VkPipeline pipeline = VK_NULL_HANDLE;
try {
CreateComputePipeline(state->device, state->DeviceFunction<PFN_vkCreateComputePipelines>("vkCreateComputePipelines"), context.pipelineCache, module, objects.layout, computePipelineFlags(), computeStageFlags(shader), specialization.Info(), computeSubgroupSize(shader), &pipeline, context.pipelineCacheSynchronization);
@@ -4911,8 +5062,19 @@ std::shared_ptr<ComputePipelineObjects> VulkanDevice::computePipeline(const Grap
return finish(std::move(objects), nullptr);
}
VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipeOut) {
VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::span<const Graphics::GuestMemorySnapshot> snapshots, std::uint64_t programAddress, std::shared_ptr<PreparedDispatch> prepared, std::shared_ptr<const Recipe>* recipeOut, const std::array<std::uint32_t, 3>& threadGroup) {
startJournalReplay();
for (const auto& binding : shader.bindings) {
if (binding.role != ShaderRecompiler::DescriptorRole::GuestBuffers) continue;
for (std::size_t at = 0; at + 4u <= binding.guestDescriptor.size(); at += 4u) {
const auto* words = binding.guestDescriptor.data() + at;
const auto base = static_cast<std::uint64_t>(words[0]) | (static_cast<std::uint64_t>(words[1] & 0xffffu) << 32u);
const auto stride = (words[1] >> 16u) & 0x3fffu;
const auto records = static_cast<std::uint64_t>(words[2]);
const auto bytes = stride != 0 ? records * stride : records;
if (base != 0 && bytes != 0) static_cast<void>(Graphics::FlushDepthPlanes(base, static_cast<std::size_t>(bytes)));
}
}
state->recorder->BoundKeptBytes();
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
PerformanceTimer timing("Vulkan.Dispatch");
@@ -5049,6 +5211,10 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
Graphics::ShaderResources::MoveProof proof;
auto moved = cached->MovedDispatchBuffers(shaders[0], state->recorder.get(), DispatchMoveBytes(), proof);
moves.checked.fetch_add(1, std::memory_order_relaxed);
if (moved.has_value() && threadGroup != std::array<std::uint32_t, 3>{}) {
const auto data = cached->shaderDataAllocation(shaders[0]);
if (data.has_value() && std::ranges::any_of(*moved, [&](const Graphics::ShaderResources::MovedBuffer& entry) { return entry.allocation == *data; })) moved.reset();
}
if (moved.has_value() && !moved->empty()) {
overlay = cached->PrepareDrawBindings(*state->recorder, *moved, true);
if (overlay != nullptr && overlay->allocation.set == VK_NULL_HANDLE) overlay.reset();
@@ -5151,6 +5317,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
RecordedDispatch record{&context, &shaders[0], resources, objects, pushStages, &pushBytes, x, y, z, arguments, nullptr, programAddress, fromCache && TemplateDataRefresh() ? RecordedDispatch::DataRefresh::Words : RecordedDispatch::DataRefresh::None, 0, revalidateMs, &timer};
record.resourceKeyMatched = fromCache;
record.bindings = overlay;
record.threadGroup = threadGroup;
if (arguments != 0) decideIndirect(record, outcome, groupsText);
// The indirect hold total below still covers the whole call of a CPU-resolved one.
const bool indirect = record.arguments != 0 || outcome.cpuReason != 0;
@@ -5225,7 +5392,7 @@ VulkanDevice::IndirectOutcome VulkanDevice::dispatch(const ShaderRecompiler::Rec
return outcome;
}
RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr<RecipeHit>& hit, IndirectOutcome& outcome, const std::shared_ptr<PreparedDispatch>& verify, bool refreshByWords) {
RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResult& shader, std::uint32_t x, std::uint32_t y, std::uint32_t z, std::uint64_t arguments, std::uint64_t programAddress, const std::shared_ptr<RecipeHit>& hit, IndirectOutcome& outcome, const std::shared_ptr<PreparedDispatch>& verify, bool refreshByWords, const std::array<std::uint32_t, 3>& threadGroup) {
state->recorder->BoundKeptBytes();
ShaderRecompiler::RuntimeAbi::RequireVersion(shader.runtimeAbiVersion);
PerformanceTimer timing("Vulkan.DispatchRecipe");
@@ -5321,6 +5488,7 @@ RecipeOutcome VulkanDevice::DispatchRecipe(const ShaderRecompiler::RecompileResu
if (dataRefresh == RecordedDispatch::DataRefresh::Words) counters.refreshByWords.fetch_add(1, std::memory_order_relaxed);
RecordedDispatch record{&context, &shaders[0], hit->resources, hit->objects, recipe.pushes ? VkShaderStageFlags{VK_SHADER_STAGE_COMPUTE_BIT} : VkShaderStageFlags{0}, &recipe.pushBytes, x, y, z, arguments, nullptr, programAddress, dataRefresh, recipe.dataWordsHash, 0, &timer};
record.resourceKeyMatched = verify != nullptr;
record.threadGroup = threadGroup;
if (arguments != 0) decideIndirect(record, outcome, groupsText);
const bool indirect = record.arguments != 0 || outcome.cpuReason != 0;
const auto recordStart = profile ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{};
@@ -27,6 +27,7 @@ namespace {
std::jthread processSamplerThread;
std::jthread workerSamplerThread;
std::jthread committerSamplerThread;
// Debug aid: APS5_SAMPLE_WORKER=<file> samples the calling thread every millisecond and, every 20 s,
// writes "module+offset self inclusive" lines to <file> for offline symbolization with nm.
@@ -285,11 +286,7 @@ void StartProcessSampler() {
});
}
}
void StartWorkerSampler() {
StartProcessSampler();
const char* path = std::getenv("APS5_SAMPLE_WORKER");
void StartCallingThreadSampler(const char* path, std::jthread& slot) {
if (path == nullptr) return;
auto sampler = std::make_unique<Sampler>();
sampler->path = path;
@@ -302,7 +299,7 @@ void StartWorkerSampler() {
sampler->stackHigh = reinterpret_cast<std::uint64_t>(block->StackBase);
}
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), GetCurrentProcess(), &sampler->target, THREAD_ALL_ACCESS, FALSE, 0)) throw std::system_error(GetLastError(), std::system_category(), "Duplicating sampler thread handle");
workerSamplerThread = std::jthread([sampler = std::move(sampler)](std::stop_token token) {
slot = std::jthread([sampler = std::move(sampler)](std::stop_token token) {
auto flushed = std::chrono::steady_clock::now();
while (!token.stop_requested()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
@@ -316,14 +313,28 @@ void StartWorkerSampler() {
});
}
}
void StartWorkerSampler() {
StartProcessSampler();
StartCallingThreadSampler(std::getenv("APS5_SAMPLE_WORKER"), workerSamplerThread);
}
void StartCommitterSampler() {
StartCallingThreadSampler(std::getenv("APS5_SAMPLE_COMMITTER"), committerSamplerThread);
}
void StopWorkerSampler() {
processSamplerThread.request_stop();
workerSamplerThread.request_stop();
committerSamplerThread.request_stop();
if (processSamplerThread.joinable()) processSamplerThread.join();
if (workerSamplerThread.joinable()) workerSamplerThread.join();
if (committerSamplerThread.joinable()) committerSamplerThread.join();
}
#else
void StartWorkerSampler() {}
void StartCommitterSampler() {}
void StopWorkerSampler() {}
#endif
+183
View File
@@ -0,0 +1,183 @@
#include <cstdint>
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
extern "C" {
int APS5_VABI sceAgcDriverAcquireRazorACQ(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverCreateQueue(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverCwsrResumeAcq(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverCwsrSuspendAcq(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverDebugHardwareStatus(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverDestroyQueue(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetPaDebugInterfaceVersion(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetRegShadowInfo(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetRegShadowInfoAgr(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetReservedDmemForAgc(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverGetWorkloadStreamInfo(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverIDHSSubmit(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverModuleRegistration(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverNotifyDefaultStates(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverPassInfoDownward(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverPatchClearState(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetFlip(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetHsOffchipParamDirect(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetWorkloadComplete(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetWorkloadsActive(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetupAsyncGraphics(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSetupRegisterShadow(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSubmitMultiCommandBuffersDirect(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSubmitToHDRScopesACQ(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSubmitToRazorACQ(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSuspendPointSubmit(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSuspendPointSubmitDirect(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSysEnableSubmitDone45Exception(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSysGetClientNumber(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverSysIsGameClosed(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverTmpInitIdhs(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverUserDataWritePacket(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverUserDataWritePopMarker(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverUserDataWritePushMarker(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
int APS5_VABI sceAgcDriverUserDataWriteSetMarker(void) {
NotImplemented_nid_no_patch(__func__);
return 0;
}
}
@@ -25,6 +25,11 @@ public:
std::vector<std::string> messages;
};
void SetImageTables(std::vector<ImageTableFaults> tables, std::vector<std::pair<std::uint64_t, std::uint64_t>> ranges);
struct SrtGuardFaults {
std::uint32_t stage = 0;
std::vector<std::uint32_t> flattenedSrt;
};
void SetSrtGuards(std::vector<SrtGuardFaults> guards);
// APS5_PROFILE_DRAW: page tables served from the per-device cache and built anew (cumulative),
// and how many recent tables the cache holds right now.
struct TableCacheStats {
@@ -52,11 +57,13 @@ private:
// same device addresses share one buffer while one of them is alive (see the table cache in
// BdaResources.cpp, which refers to it weakly).
void markWrittenPages() const;
void checkSrtGuard() const;
std::shared_ptr<Buffer> table;
std::unique_ptr<Buffer> fault;
std::size_t tableBytes = 0;
std::vector<ImageTableFaults> imageTables;
std::vector<std::pair<std::uint64_t, std::uint64_t>> imageTableRanges;
std::vector<SrtGuardFaults> srtGuards;
};
}
@@ -2,8 +2,10 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_BUFFERPOOL_HPP
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include <chrono>
#include <cstdint>
#include <deque>
#include <list>
#include <memory>
#include <mutex>
#include <optional>
@@ -24,6 +26,7 @@ struct BufferAllocation {
VkDeviceSize offset = 0;
bool slab = false;
std::size_t bufferBytes = 0;
std::uint32_t memoryType = 0;
};
struct SlabSlot {
@@ -63,6 +66,10 @@ public:
static std::size_t Capacity(std::size_t bytes, VkMemoryPropertyFlags properties);
static VkBufferUsageFlags Usage(VkBufferUsageFlags usage, VkMemoryPropertyFlags properties);
VkDeviceMemory AllocateMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType = std::nullopt, std::uint32_t* allocatedType = nullptr);
static VkDeviceSize ImageBudget(VkDeviceSize deviceBudget);
VkDeviceMemory TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType = std::nullopt, std::uint32_t* allocatedType = nullptr);
void PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept;
std::size_t RetainedImages();
std::optional<BufferAllocation> Take(std::size_t bytes, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
void Put(const BufferAllocation& allocation) noexcept;
static bool SlabEligible(std::size_t capacity, VkDeviceSize alignment, VkDeviceSize size, VkDeviceSize atom);
@@ -82,6 +89,7 @@ private:
};
struct Slab {
std::vector<SlabBlock*> available;
std::size_t blocks = 0;
std::size_t emptyBlocks = 0;
};
static std::uint64_t SlabKey(std::uint32_t memoryType, std::size_t capacity, bool addressable);
@@ -89,16 +97,18 @@ private:
struct Slot {
BufferAllocation allocation;
std::uint64_t lastUse;
std::list<Slot*>::iterator order;
};
struct SlotKey {
std::size_t bytes;
VkBufferUsageFlags usage;
VkMemoryPropertyFlags properties;
std::uint32_t memoryType = 0;
bool operator==(const SlotKey&) const = default;
};
struct SlotKeyHash {
std::size_t operator()(const SlotKey& key) const noexcept {
return std::hash<std::size_t>{}(key.bytes) ^ (static_cast<std::size_t>(key.usage) << 32u) ^ (static_cast<std::size_t>(key.properties) << 48u);
return std::hash<std::size_t>{}(key.bytes) ^ (static_cast<std::size_t>(key.usage) << 32u) ^ (static_cast<std::size_t>(key.properties) << 48u) ^ (static_cast<std::size_t>(key.memoryType) << 56u);
}
};
// One retention tier: its slots by key (each list oldest first), their count and bytes, the
@@ -126,6 +136,9 @@ private:
// caller destroys those after releasing the mutex, so builds taking buffers on other threads
// do not wait behind the Vulkan destroy calls. Nothing changes when the vector cannot grow.
void evictOldest(Tier& tier, std::vector<BufferAllocation>& evicted);
bool evictOldestImage(std::vector<BufferAllocation>& evicted);
bool imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const;
bool heapOverBudget();
// The retained-slot bound of each tier (APS5_BUFFER_POOL_SLOTS, default `defaultSlots`), read once.
static std::size_t MaxSlots();
std::size_t maxSlots;
@@ -136,11 +149,23 @@ private:
PFN_vkFreeMemory freeMemory;
PFN_vkAllocateMemory allocateMemory;
PFN_vkMapMemory mapMemory;
VkPhysicalDevice physical = VK_NULL_HANDLE;
VkPhysicalDeviceMemoryProperties memoryProperties{};
PFN_vkGetPhysicalDeviceMemoryProperties2 memoryProperties2 = nullptr;
std::mutex mutex;
// Not `small`/`large`: <rpcndr.h> (via <windows.h>) defines `small` as a macro.
Tier smallTier;
Tier largeTier;
Tier deviceTier;
std::chrono::steady_clock::time_point heapRead{};
bool heapOver = false;
VkDeviceSize imageBudget = 0;
VkDeviceSize imageBytes = 0;
std::size_t imageSlots = 0;
std::uint64_t imageHits = 0;
std::uint64_t imageMisses = 0;
std::uint64_t imageEvictions = 0;
std::list<Slot*> imageOrder;
std::uint64_t clock = 0;
std::mutex slabMutex;
std::unordered_map<std::uint64_t, Slab> slabs;
@@ -156,7 +181,8 @@ private:
};
std::shared_ptr<BufferPool> GetBufferPool(const Context& context);
VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation);
VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType = nullptr);
void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept;
}
@@ -6,6 +6,7 @@
#endif
#include <mutex>
#include <vulkan/vulkan.h>
#include <array>
#include <atomic>
#include <cstdint>
#include <cstdio>
@@ -20,6 +21,7 @@
namespace AgcDriver::Graphics {
class TextureDetiler;
class GpuColorTransfer;
class BufferPool;
@@ -71,6 +73,17 @@ inline void Check(VkResult result, const char* operation) {
if (result != VK_SUCCESS) throw std::runtime_error(std::string("AGC graphics: ") + operation + ": Vulkan result " + std::to_string(result));
}
inline VkShaderModule CreateShaderModule(PFN_vkCreateShaderModule create, VkDevice device, bool graphicsPipelineLibrary, const std::uint32_t* code, std::size_t bytes, const char* operation) {
std::vector<std::uint32_t> keyed(code, code + bytes / sizeof(std::uint32_t));
if (!graphicsPipelineLibrary && keyed.size() > 2) keyed[2] ^= 0x8000u;
VkShaderModuleCreateInfo info{VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO};
info.codeSize = keyed.size() * sizeof(std::uint32_t);
info.pCode = keyed.data();
VkShaderModule module = VK_NULL_HANDLE;
Check(create(device, &info, nullptr, &module), operation);
return module;
}
inline void Require(bool condition, const char* reason) {
if (!condition) throw std::runtime_error(std::string("AGC graphics: ") + reason);
}
@@ -79,6 +92,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).
@@ -220,6 +238,8 @@ struct Context {
bool samplerTableUpdateAfterBind = false;
bool geometryShader = false;
bool sampleRateShading = false;
bool fillModeNonSolid = false;
bool maintenance5 = false;
bool storageImageMultisample = false;
bool shaderClipDistance = false;
bool shaderCullDistance = false;
@@ -241,6 +261,8 @@ struct Context {
bool singlePassStorage = false;
VkSampleCountFlags sampleLocationSampleCounts = 0;
VkSampleCountFlags sampleLocationGridSampleCounts = 0;
std::array<VkExtent2D, 4> sampleLocationGridSizes{};
bool shaderStencilExport = false;
PipelineCacheSynchronization* pipelineCacheSynchronization = nullptr;
bool shaderTerminateInvocation = false;
@@ -332,6 +354,10 @@ struct Context {
}
};
inline VkShaderModule CreateShaderModule(const Context& context, const std::uint32_t* code, std::size_t bytes, const char* operation) {
return CreateShaderModule(context.Function<PFN_vkCreateShaderModule>("vkCreateShaderModule"), context.device, context.graphicsPipelineLibrary, code, bytes, operation);
}
inline void EndRenderPass(const Context& context, VkCommandBuffer commands) {
if (context.graphicsPipelineLibrary) context.Resolved(&DeviceFunctions::cmdEndRendering, "vkCmdEndRenderingKHR")(commands);
else context.Resolved(&DeviceFunctions::cmdEndRenderPass, "vkCmdEndRenderPass")(commands);
@@ -36,6 +36,7 @@ bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys fille
// The keys covering a surface of `surfaceBytes`, when they all agree.
DccKeys ReadDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
bool IsDccClear(DccKeys keys);
std::optional<std::uint8_t> LandedUniformMetadataByte(std::uint64_t address, std::size_t bytes);
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
DccKeys CurrentDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes, std::size_t keyCount);
std::optional<DccKeys> KnownPendingDccKeys(std::uint64_t metaAddress, std::uint64_t surfaceBytes);
@@ -2,6 +2,7 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_DEPTHSURFACE_HPP
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <array>
#include <cstdint>
@@ -9,13 +10,18 @@
#include <optional>
#include <span>
#include <utility>
#include <vector>
namespace AgcDriver::Graphics {
class Texture;
class StorageTexture;
VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target);
VkImageView DepthSurfaceView(const Context& context, const DepthTarget& target, bool depthWritten = true);
SampleOrderTarget DepthSurfaceOrderTarget(const Context& context, const DepthTarget& target);
DepthSampleLocations DepthSurfaceSampleLocations(const Context& context, const DepthTarget& target);
std::vector<std::pair<std::uint64_t, std::uint64_t>> FlushDepthPlanes(std::uint64_t address, std::size_t bytes);
bool DepthPlanesStale();
bool SampleLocationsCompatibleDepth(const Context& context, VkFormat format, std::uint32_t samples);
std::uint64_t DepthSurfacesSerial();
std::uint64_t DepthSurfacesReleased();
@@ -5,6 +5,7 @@
#include "BdaAbi.hpp"
#include "prx/libc/include/GuestAllocations.hpp"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdint>
#include <limits>
@@ -46,6 +47,7 @@ struct HostImport {
std::uint64_t pinnedBytes = 0;
std::uint32_t memoryType = std::numeric_limits<std::uint32_t>::max();
std::uint64_t backingSerial = 0;
mutable std::uint64_t spaceSerial = 0;
};
inline VkDeviceSize HostImportOffset(const HostImport& import, std::uint64_t address) {
@@ -72,15 +74,40 @@ ImportProbe ProbeDmaBufImportWriteProtection(const Context& context);
ImportWatch PrepareImportWatch(const Context& context);
void SetImportWatch(const Context& context, ImportWatch watch);
enum class ImportRequester : std::uint8_t { Map, Build, Access, Join, Segment };
inline constexpr std::size_t ImportRequesterCount = 5;
struct HostImportRequesterCounts {
std::uint64_t made = 0;
std::uint64_t madeBytes = 0;
std::uint64_t refused = 0;
std::uint64_t refusedBytes = 0;
std::uint64_t failed = 0;
std::uint64_t remembered = 0;
};
struct HostImportCounts {
std::uint64_t live = 0;
std::uint64_t liveBytes = 0;
std::array<HostImportRequesterCounts, ImportRequesterCount> requesters{};
};
const char* ImportRequesterName(ImportRequester requester);
HostImportCounts HostImportCounters();
std::string FormatHostImportCounts(const HostImportCounts& counts);
void ReportHostImports();
// The host import of the registered allocation containing [address, address + bytes), made on demand
// (alignment and budget permitting), or null. Bytes at `address` are at `HostImportOffset(*import,
// address)` in the import's buffer.
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes);
const HostImport* HostImportFor(const Context& context, std::uint64_t address, std::size_t bytes, ImportRequester requester = ImportRequester::Access);
const HostImport* ExistingHostImportFor(const Context& context, std::uint64_t address, std::size_t bytes);
void SetHostImportWindow(std::uint64_t bytes);
// Tests: the window of one large registered range (64 MiB) and the import budget (0: the configured one).
void SetHostImportRangeWindow(std::uint64_t bytes);
void SetHostImportBudget(std::uint64_t bytes);
std::uint64_t HostImportBudgetFor(const VkPhysicalDeviceMemoryProperties& memory, const VkPhysicalDeviceMemoryBudgetPropertiesEXT* reported, std::uint64_t configured, std::uint64_t liveBytes);
std::uint64_t HostImportBudgetWith(std::uint64_t configured, bool set, std::uint64_t reported, bool allocationFailed, std::uint64_t ceiling);
void SetAddressCopyLimit(std::uint64_t bytes);
void SetHostImportDeferredSettles(bool enabled);
// The larger of 6 GiB and half the physical memory, at most the largest host heap less the larger of 2 GiB and half
@@ -219,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);
@@ -2,6 +2,7 @@
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_GUESTTEXTURERESOURCE_HPP
#include "Recompiler.hpp"
#include <array>
#include <cstdint>
#include <span>
@@ -96,6 +97,7 @@ float EffectiveMinLod(const GuestTextureResource& resource);
bool IsNullTextureResource(std::span<const std::uint32_t> words);
GuestTextureResource DecodeTextureResource(std::span<const std::uint32_t> words);
bool DescriptorSingleLevel(std::span<const std::uint32_t> words);
bool FirstLayerDescriptor(std::span<const std::uint32_t> words, std::array<std::uint32_t, 8>& narrowed);
bool MatchesGuestDimension(ShaderRecompiler::DescriptorImageShape shape, TextureDimension dimension);
bool ArrayLayersServeView(const GuestTextureResource& surface, const GuestTextureResource& view);
@@ -3,7 +3,9 @@
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
#include "prx/libSceAgcDriver/Graphics/include/SampleOrder.hpp"
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <memory>
@@ -55,7 +57,15 @@ std::uint64_t MultisampledColorBytes(const ColorTarget& target);
std::size_t MultisampledDccBytes(const ColorTarget& target);
VkClearColorValue MultisampledDccClear(const ColorTarget& target, std::span<const std::byte> dcc, std::span<const std::byte> cmask);
bool MultisampledFillClearColor(const ColorTarget& target, std::uint32_t fill, VkClearColorValue& clear);
struct MultisampledSingleClear {
std::uint64_t descriptor = 0;
std::array<std::uint32_t, 4> color{};
};
std::optional<MultisampledSingleClear> MatchMultisampledSingleClear(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads);
void NoteMultisampledSingleClear(const GuestTextureResource& resource, const std::array<std::uint32_t, 4>& color);
bool TakeMultisampledSingleClear(std::span<const std::uint32_t> code, std::span<const std::uint32_t> userData, const std::array<std::uint32_t, 3>& numThreads);
bool TakeMultisampledClear(MultisampledCmask& cmask, MultisampledColorFill& colorFill, const ColorTarget& use, VkClearColorValue& clear);
SampleOrderTarget MultisampleTargetOrderTarget(const Context& context, const ColorTarget& target);
std::shared_ptr<Texture> MultisampleTargetTexture(const Context& context, const GuestTextureResource& resource, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D);
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, VkImage* image = nullptr);
VkImageView MultisampleTargetView(const Context& context, const ColorTarget& target, std::shared_ptr<void>& owner, VkImage* image = nullptr);
@@ -70,6 +80,7 @@ void ClearMultisampleTargets(VkDevice device);
void InstallResidentImageReadGuard_nid_no_patch();
void UpdateResidentImageUnmapObserver_nid_no_patch();
void RequireNoMultisampleTarget(std::uint64_t address, std::size_t bytes, const char* access);
void RequireNoMultisampleColorTarget(std::uint64_t address, std::size_t bytes, const char* access);
}
@@ -22,6 +22,7 @@ struct PassAccess {
std::span<const std::pair<VkImage, bool>> attachments;
bool anyReads = false;
bool anyWrites = false;
GuestRangeList writeOnly = {};
};
enum class PassHazard : std::uint8_t { None, ReadAfterWrite, WriteAfterWrite, WriteAfterRead, Image, UnknownRead, UnknownWrite };
@@ -32,6 +33,7 @@ public:
bool Overlaps(std::uint64_t begin, std::uint64_t end) const;
bool Empty() const { return ranges.empty(); }
std::size_t Size() const { return ranges.size(); }
GuestRangeList Ranges() const { return ranges; }
void Clear() { ranges.clear(); }
private:
@@ -43,6 +45,7 @@ public:
PassHazard Check(const PassAccess& access, bool samePass) const;
void Add(const PassAccess& access);
void BeginPass() { ++pass; }
void SealWriteOnly();
void Clear();
bool Empty() const;
@@ -55,6 +58,7 @@ private:
bool overlapsAny(const GuestRangeSet& set, GuestRangeList ranges) const;
GuestRangeSet reads;
GuestRangeSet writes;
GuestRangeSet writeOnly;
std::vector<std::pair<VkImage, bool>> images;
std::vector<Attachment> attachments;
std::uint64_t pass = 0;
@@ -58,6 +58,10 @@ public:
void ContinueBackFaces(VkCommandBuffer commands, const State& state) const;
bool SplitsSecondChannel() const { return secondChannel != VK_NULL_HANDLE; }
void ContinueSecondChannel(VkCommandBuffer commands) const;
const SampleOrder& HostSampleOrder() const { return sampleOrder; }
bool ReordersSamples() const { return Graphics::ReordersSamples(sampleOrder); }
bool SampleLocationsEnabled() const { return sampleLocationsEnabled; }
VkExtent2D HostSampleLocationGrid() const { return sampleLocationGrid; }
void PushConstants(VkCommandBuffer commands, VkShaderStageFlags stages, std::span<const std::byte, PipelinePushConstantBytes> bytes) const;
// Forgets the Vulkan objects without destroying them: for entries of a device that is already gone.
void Abandon() noexcept;
@@ -93,6 +97,7 @@ private:
std::vector<std::uint32_t> colorSlots;
bool sampleLocationsEnabled = false;
VkExtent2D sampleLocationGrid{1, 1};
SampleOrder sampleOrder{};
std::vector<CachedFramebuffer> framebuffers;
};
@@ -26,6 +26,7 @@ public:
VkPipelineCache Handle() const { return cache; }
PipelineCacheSynchronization& Synchronization() { return synchronization; }
void SaveNow();
void SaveUnlessBusy();
private:
struct FileHeader {
@@ -47,7 +48,7 @@ private:
File readFile() const;
std::optional<FileHeader> readHeader() const;
void load(std::vector<std::byte>& initialData);
void save(bool final);
void save(bool final, bool waitForPipelines);
void run();
Context context;
@@ -69,6 +69,7 @@ public:
};
DrawPassStart StartDrawPass(std::uint64_t key, bool forced, bool fenced, const PassAccess& access);
void LeaveRenderPassOpen(std::uint64_t key, std::uint32_t timing, bool fenced, const PassAccess& access, std::function<void(VkCommandBuffer)> afterPass = {});
void SealPassWriteOnly();
// DCC "uncompressed" key stores (DccMetadata.cpp StoreUncompressedOnGpu): queued on the open
// batch and recorded as one run (one barrier pair for every queued fill) at Submit, before a
// label store (RecordStore), or before a command that writes or reads a queued range (the
@@ -196,11 +197,12 @@ 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();
bool HasTimeline() const { return timeline != VK_NULL_HANDLE; }
bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments);
bool RecordDispatchArguments(VkCommandBuffer commands, VkDeviceAddress source, VkDeviceAddress arguments, const std::array<std::uint32_t, 3>* threadGroup = nullptr);
// Waits until every batch up to `serial` completed on the GPU. Called WITHOUT GuestMemory::GpuMutex:
// it touches only the immutable timeline semaphore (the caller keeps the device alive). Requires
// HasTimeline(); the debug switch APS5_NO_TIMELINE makes the device create the recorder without one.
@@ -231,6 +233,7 @@ public:
std::uint64_t end;
bool computed;
};
static std::vector<std::size_t> QueuedWriteOrder(std::span<const QueuedWrite> writes);
static std::vector<std::size_t> QueuedWriteGroups(std::span<const QueuedWrite> writes);
// Waits only for the batches up to the newest one that writes the range (submitting the open
// batch when it is that one); later batches stay in flight. Fences of one queue signal in
@@ -804,6 +807,10 @@ private:
Context context;
VkSemaphore timeline = VK_NULL_HANDLE;
VkSemaphore submitHold = VK_NULL_HANDLE;
std::uint64_t submitHoldValue = 0;
struct ProgressWatcher;
std::unique_ptr<ProgressWatcher> progressWatcher;
// 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;
@@ -0,0 +1,74 @@
#ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP
#define CORE_LIBS_PRX_LIBSCEAGCDRIVER_GRAPHICS_INCLUDE_SAMPLEORDER_HPP
#include "prx/libSceAgcDriver/Graphics/include/State.hpp"
#include <array>
#include <cstdint>
#include <memory>
#include <span>
#include <string>
#include <vector>
namespace AgcDriver::Graphics {
struct HostSampleLocations {
VkExtent2D grid{1, 1};
SampleOrder order{};
};
enum class SampleOrderMethod : std::uint8_t {
Storage,
Depth,
DepthStencilExport,
DepthStencilBits
};
struct SampleOrderStep {
std::uint32_t attachment = 0;
SampleOrderMethod method = SampleOrderMethod::Storage;
bool toHost = false;
bool toGuest = false;
};
struct SampleOrderPass {
SampleOrder order{};
VkExtent2D extent{};
std::array<VkSampleLocationEXT, 32> locations{};
bool located = false;
};
struct SampleOrderTarget {
VkImage image = VK_NULL_HANDLE;
VkImageView view = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
VkExtent2D extent{};
std::uint32_t samples = 1;
SampleOrderMethod method = SampleOrderMethod::Storage;
std::shared_ptr<void> keep;
};
struct DepthSampleLocations {
VkImage image = VK_NULL_HANDLE;
VkFormat format = VK_FORMAT_UNDEFINED;
std::uint32_t samples = 1;
VkExtent2D grid{1, 1};
std::array<VkSampleLocationEXT, 32> locations{};
std::shared_ptr<void> keep;
};
HostSampleLocations HostSampleLocationsFor(const Context& context, const State& state);
bool HostsSampleLocationGrid(VkExtent2D largestGrid);
bool MayReorderSamples(const Context& context, std::uint32_t samples);
SampleOrderMethod DepthSampleOrderMethod(const Context& context, const DepthTarget& depth);
VkFormat SampleOrderStorageFormat(const Context& context, VkFormat format, std::uint32_t elementBytes, std::uint32_t samples);
std::string SampleOrderRefusal(const Context& context, const State& state);
std::vector<SampleOrderStep> PlanSampleOrder(const Context& context, const State& state, const SampleOrder& order, VkAttachmentLoadOp load = VK_ATTACHMENT_LOAD_OP_LOAD, VkAttachmentStoreOp store = VK_ATTACHMENT_STORE_OP_STORE);
SampleOrderPass SampleOrderPassFor(const Context& context, const State& state, const SampleOrder& order);
void RecordSampleOrder(const Context& context, VkCommandBuffer commands, const SampleOrderPass& pass, std::span<const SampleOrderTarget> targets, bool toHost, std::vector<std::shared_ptr<void>>& kept);
void RecordDepthSampleLocations(const Context& context, VkCommandBuffer commands, const DepthSampleLocations& depth);
std::array<std::span<const std::uint32_t>, 9> SampleOrderShaderModules();
void ClearSampleOrders(VkDevice device);
}
#endif
@@ -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);
@@ -72,7 +73,7 @@ struct TableDescriptors {
std::uint32_t binding = 0;
std::vector<std::pair<std::uint32_t, std::uint32_t>> runs;
std::vector<VkDescriptorImageInfo> images;
std::shared_ptr<const std::vector<std::shared_ptr<Texture>>> textures;
std::shared_ptr<const std::vector<std::weak_ptr<Texture>>> textures;
};
class DescriptorCache {
@@ -233,6 +234,7 @@ public:
// The recorder's hazard tracker inputs (Recorder::NoteAccess): the written elements' guest
// ranges, the regions read in place, and the storage images with whether each is written.
const std::vector<std::pair<std::uint64_t, std::uint64_t>>& GpuWrites() const { return guestMemory.Writes(); }
const std::vector<std::pair<std::uint64_t, std::uint64_t>>& WriteOnlyRanges() const { return writeOnlyRanges; }
std::vector<std::pair<std::uint64_t, std::uint64_t>> InPlaceReads() const { return guestMemory.InPlaceReads(); }
void InPlaceReads(std::vector<std::pair<std::uint64_t, std::uint64_t>>& out) const { guestMemory.InPlaceReads(out); }
std::vector<std::pair<VkImage, bool>> StorageImages() const;
@@ -275,6 +277,7 @@ public:
static bool NeverReusable(std::span<const CompiledShader> shaders);
static std::uint64_t DepthGuardRefusals();
const std::vector<std::shared_ptr<Texture>>& SampledTextures() const { return textures; }
static std::size_t LookupLogCapacity();
bool ChecksFaults() const { return bda != nullptr; }
void CheckFaults() const;
// `shaders` are the stages the object was built from, in build order (a recorded draw's vertex
@@ -304,7 +307,7 @@ public:
bool DataWordsDiffer(const CompiledShader& shader) const;
// Why the fast proof of a Revalidate left the object to the full walk (the [rescache]
// revalidate line's reasons); Count: it did not.
enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Count };
enum class FastFail : std::size_t { NoRecord, Collect, Pending, Evicted, Changed, Keys, ClearedView, StorageKeys, Depth, Replaced, Audit, Count };
// Why a Pending failure was left to the full walk instead of the own-object refresh (T1, see
// refreshOwnObjects); Count: it was not.
enum class OwnRefreshFallback : std::size_t { Disabled, Snapshot, Keys, ForeignView, SurfaceKey, NotImported, Uncached, Rerun, Count };
@@ -333,6 +336,9 @@ public:
private:
friend class AgcDriver::VulkanDevice;
bool refreshData(VkCommandBuffer commands, const CompiledShader& shader, Recorder* recorder);
std::optional<std::size_t> shaderDataAllocation(const CompiledShader& shader) const;
VkBuffer shaderDataBuffer(const CompiledShader& shader) const;
void forgetShaderDataWords(const CompiledShader& shader);
struct DescribedRange {
const char* kind;
std::uint64_t address;
@@ -342,6 +348,7 @@ private:
std::uint32_t format = 0;
int tileMode = 0;
std::uint64_t dccAddress = 0;
bool imageTable = false;
};
std::vector<DescribedRange> describedRanges;
struct Allocation {
@@ -352,6 +359,7 @@ private:
ShaderRecompiler::DescriptorRole role = ShaderRecompiler::DescriptorRole::ShaderData;
// Guest buffers: whether the shader may store to the range (see addGuestBuffer).
bool written = true;
bool writeOnly = false;
// Data buffers: the words the buffer holds once the recorded work ran (see RefreshData).
std::vector<std::uint32_t> dataWords;
std::uint32_t adjustment = 0;
@@ -538,6 +546,7 @@ private:
GuestBufferMemory guestMemory;
std::size_t maxDrawSnapshotBytes = MaxDrawSnapshotBytes();
std::unique_ptr<BdaResources> bda;
std::vector<BdaResources::SrtGuardFaults> srtGuardFaults;
bool usesBda = false;
bool bdaReadOnly = true;
std::vector<std::uint64_t> multisampleSources;
@@ -569,7 +578,7 @@ private:
std::vector<std::pair<std::uint32_t, std::uint32_t>> runs;
};
std::array<TableWrites, 2> tableWrites;
std::shared_ptr<const TableDescriptors> keptTable(std::size_t set, std::shared_ptr<const std::vector<std::shared_ptr<Texture>>>& held) noexcept;
std::shared_ptr<const TableDescriptors> keptTable(std::size_t set, std::shared_ptr<const std::vector<std::weak_ptr<Texture>>>& held) noexcept;
std::uint32_t setOf(const Binding& binding) const;
std::vector<Allocation> allocations;
std::vector<VkDescriptorBufferInfo> descriptorBuffers;
@@ -587,6 +596,8 @@ private:
// Guest buffer elements bound read-only: each use of this object skips that many pending-write
// notes (counted in MarkGpuWrites for the [buffers] line).
std::size_t readOnlyBuffers = 0;
std::chrono::steady_clock::time_point lastFullWalk = std::chrono::steady_clock::now();
std::vector<std::pair<std::uint64_t, std::uint64_t>> writeOnlyRanges;
bool drawBuild = false;
std::uint64_t dispatchThreads = 0;
std::vector<std::shared_ptr<Texture>> textures;
@@ -143,6 +143,7 @@ struct State {
VkConservativeRasterizationModeEXT conservativeRasterization = VK_CONSERVATIVE_RASTERIZATION_MODE_DISABLED_EXT;
VkRect2D scissor;
VkCullModeFlags cullMode;
VkPolygonMode polygonMode = VK_POLYGON_MODE_FILL;
VkFrontFace frontFace;
VkProvokingVertexModeEXT provokingVertexMode = VK_PROVOKING_VERTEX_MODE_FIRST_VERTEX_EXT;
VkPipelineColorBlendAttachmentState blend;
@@ -167,6 +168,14 @@ void NormalizeUnusedViewportDepth(State& state, const ShaderRecompiler::ShaderPi
std::array<VkSampleLocationEXT, 32> SampleLocations(const State& state);
std::array<std::uint8_t, 32> PixelSampleLocations(const State& state, const ShaderRecompiler::ShaderPixelStageInfo& pixel);
VkExtent2D SampleLocationGrid(const State& state);
using SampleOrder = std::array<std::uint32_t, 4>;
inline constexpr std::uint32_t KeptSampleOrder = 0x76543210u;
std::optional<SampleOrder> SameSetSampleOrder(const State& state);
bool ReordersSamples(const SampleOrder& order);
std::uint32_t InvertSampleOrder(std::uint32_t order);
std::uint32_t HostSampleBits(std::uint32_t guestBits, std::uint32_t order);
bool SampleMaskFollowsOrder(const State& state, const SampleOrder& order);
std::array<std::uint16_t, 4> QuadSampleMasks(const State& state, const SampleOrder& order);
std::array<std::uint8_t, 8> ExportMappings(const State& state);
std::array<ShaderRecompiler::ColorExportPacking, 8> ExportPackings(const State& state);
ShaderRecompiler::RasterPrimitive RasterPrimitiveOf(const State& state);
@@ -270,6 +279,7 @@ inline constexpr std::array<DrawKeyRange, 50> DrawKeyRegisters{{
{RegisterBank::UserConfig, 0x242, 1}, {RegisterBank::UserConfig, 0x24b, 1}, {RegisterBank::UserConfig, 0x25b, 1},
}};
bool SplitsAlternateAlpha(const State& state);
bool StencilOperationValueReplaces(const Registers& context);
bool StencilReferenceUnused(const State& state);
// Whether DrawKeyRegisters holds the read.
bool DrawKeyCovers(RegisterRead read);
@@ -46,13 +46,13 @@ struct OwnedImage {
OwnedImage(const Context& context, VkImage image, VkDeviceMemory memory) : context(context), image(image), memory(memory) {}
OwnedImage(const OwnedImage&) = delete;
OwnedImage& operator=(const OwnedImage&) = delete;
~OwnedImage() {
if (image) context.Function<PFN_vkDestroyImage>("vkDestroyImage")(context.device, image, nullptr);
if (memory) context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
}
~OwnedImage();
void Allocate(const Context& device, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation);
Context context;
VkImage image;
VkDeviceMemory memory;
VkDeviceSize memoryBytes = 0;
std::uint32_t memoryType = 0;
};
class Texture {
@@ -63,7 +63,7 @@ public:
// compute pass writing it and the next pass sampling it share one image and copy nothing.
// CanCopyFrom says whether the two descriptors address the same surface compatibly.
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components);
Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr<OwnedImage> owner = {});
Texture(const Context& context, VkImage depthImage, VkFormat depthFormat, VkImageAspectFlags aspect, VkComponentMapping components, VkImageViewType viewType = VK_IMAGE_VIEW_TYPE_2D, std::uint32_t baseLayer = 0, std::shared_ptr<OwnedImage> owner = {}, VkImageUsageFlags usage = 0);
struct DepthCopyTag {};
Texture(const Context& context, const std::shared_ptr<StorageTexture>& source, const GuestTextureResource& descriptor, VkComponentMapping components, DepthCopyTag);
struct SrgbExpandTag {};
@@ -197,7 +197,8 @@ public:
void WriteBack();
// Deferred write-back (APS5_EAGER_WRITEBACK=1 stores at once instead).
void MarkDirty();
void MarkSliceDirty(std::uint32_t slice);
void MarkWholeWritten();
void MarkSliceDirty(std::uint32_t slice, std::uint32_t count = 1);
void Flush();
bool Dirty() const;
// Stores every pending image overlapping the range, except `except`; returns whether any was.
@@ -209,6 +210,7 @@ public:
// overlapping pending image, as before).
static bool FlushPending(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr, const char* reason = "memory access", PublishScope scope = PublishScope::Whole, bool* published = nullptr);
static void FlushAllPending(const char* reason);
static bool WaitForFlushing(std::uint64_t address, std::size_t bytes, const StorageTexture* except = nullptr);
static void SaveBeforeUnwatch(std::uint64_t address, std::size_t bytes);
static constexpr std::uint64_t SmallStoreBytes = 64 * 1024;
static std::size_t StoreSmallPending(const char* reason);
@@ -338,6 +340,7 @@ public:
// may carry DCC metadata, and this surface must be host-imported (the write-back is GPU-direct).
// False, naming why, when the copy must be a transfer.
bool CopyFrom(StorageTexture& source, const char*& refusal);
static const char* CopyRefusal(const Context& context, const StorageTexture* destination, const StorageTexture& source, std::uint64_t address);
VkImage Image() const { return image; }
const GuestTextureResource& Descriptor() const { return descriptor; }
std::uint32_t ImageLayers() const { return geometry.imageLayers; }
@@ -364,6 +367,9 @@ public:
static std::uint64_t SinglePassMoves();
static std::uint64_t SlabRetiles();
static std::uint64_t RefreshesProved();
static std::uint64_t ClearHandOvers();
static std::uint64_t HandedOverSuperseded();
static void SetClearHandOver(bool enabled);
static std::uint64_t UntrackedCompares();
std::uint64_t GuestBytes() const;
std::uint64_t BudgetBytes() const;
@@ -503,6 +509,8 @@ private:
// stays responsible for storing them until this image's own results supersede them
// (markLayersPending). APS5_NO_ALIAS_BORROW=1 stores and re-reads as before.
std::shared_ptr<StorageTexture> pendingAlias() const;
std::optional<DccKeys> clearHandOver();
void supersedeHandedOver(std::uint32_t first, std::uint32_t count);
// The pending images FlushPending(address, bytes) would store (not the one a Refresh validates,
// a pending unit inside the range), kept alive across the caller's use.
static std::vector<std::shared_ptr<StorageTexture>> overlappingPending(std::uint64_t address, std::size_t bytes);
@@ -583,6 +591,8 @@ private:
std::weak_ptr<StorageTexture> borrowedFrom;
std::uint64_t borrowedVersion = 0;
std::vector<bool> borrowedUnits;
std::vector<std::pair<std::weak_ptr<StorageTexture>, std::uint64_t>> handedOver;
std::vector<bool> handedUnits;
// Units of this image were borrowed since it was last written: the borrower's proofs
// (the resource cache's serial memo) hold only while this image's version does, so the next
// write here bumps the pending serial although the image was pending already.
@@ -590,6 +600,7 @@ private:
VkImage image = VK_NULL_HANDLE;
VkDeviceMemory memory = VK_NULL_HANDLE;
VkDeviceSize memoryBytes = 0;
std::uint32_t memoryType = 0;
VkImageView view = VK_NULL_HANDLE;
std::uint32_t defaultMip = 0;
std::map<std::uint32_t, VkImageView> extraViews;
@@ -608,6 +619,7 @@ private:
// Results are on the GPU only (guarded by the pending-write registry lock).
bool dirty = false;
std::uint64_t version = 0;
std::uint64_t writes = 0;
// `original` holds the guest bytes; false after a GPU-side clear, which never read them.
std::vector<bool> layerOriginal;
std::map<std::uint64_t, std::uint64_t> keptAtUnwatch;
@@ -19,6 +19,7 @@ VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat)
std::uint32_t SrgbCopyFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical, std::uint32_t decodeFormats);
bool SrgbExpandsToRgba8(const Context& context, std::uint32_t guestFormat);
bool IsSrgbTextureFormat(std::uint32_t guestFormat);
bool IgnoresForcedDegamma(std::uint32_t guestFormat);
std::optional<std::uint32_t> FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes);
std::optional<std::uint32_t> FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes);
std::uint32_t BytesPerElement(std::uint32_t guestFormat);
@@ -75,6 +75,8 @@ struct SurfaceGeometry {
};
SurfaceGeometry DescribeSurface(const GuestTextureResource& descriptor);
bool LevelsFitAllocation(const GuestTextureResource& surface, std::uint32_t levels);
GuestTextureResource ViewedArrayWindow(const GuestTextureResource& viewed);
GuestTextureResource ArrayLayerWindow(const GuestTextureResource& viewed, std::uint32_t maxArrayLayers);
}
@@ -363,23 +363,36 @@ inline std::optional<std::uint32_t> ClampDrawIndices(std::span<std::byte> indice
return highest;
}
template <typename TIndex>
std::optional<std::uint32_t> HighestDrawIndexOf(std::span<const std::byte> indices, bool skipRestart) {
constexpr auto restartIndex = std::numeric_limits<TIndex>::max();
const auto count = indices.size() / sizeof(TIndex);
const auto* data = indices.data();
TIndex highest = 0;
if (!skipRestart) {
for (std::size_t i = 0; i < count; ++i) {
TIndex index;
std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex));
highest = std::max(highest, index);
}
if (count == 0) return std::nullopt;
return highest;
}
TIndex kept = 0;
for (std::size_t i = 0; i < count; ++i) {
TIndex index;
std::memcpy(&index, data + i * sizeof(TIndex), sizeof(TIndex));
const TIndex valid = index == restartIndex ? TIndex{0} : TIndex(~TIndex{0});
highest = std::max(highest, static_cast<TIndex>(index & valid));
kept |= valid;
}
if (kept == 0) return std::nullopt;
return highest;
}
inline std::optional<std::uint32_t> HighestDrawIndex(std::span<const std::byte> indices, std::uint32_t indexSize, bool skipRestart) {
Require(indexSize == 2 || indexSize == 4, "unsupported index size");
const auto restartIndex = indexSize == 2 ? 0xffffu : 0xffffffffu;
std::optional<std::uint32_t> highest;
for (std::size_t offset = 0; offset + indexSize <= indices.size(); offset += indexSize) {
std::uint32_t index = 0;
if (indexSize == 2) {
std::uint16_t value = 0;
std::memcpy(&value, indices.data() + offset, sizeof(value));
index = value;
} else {
std::memcpy(&index, indices.data() + offset, sizeof(index));
}
if (skipRestart && index == restartIndex) continue;
highest = std::max(highest.value_or(0u), index);
}
return highest;
return indexSize == 2 ? HighestDrawIndexOf<std::uint16_t>(indices, skipRestart) : HighestDrawIndexOf<std::uint32_t>(indices, skipRestart);
}
inline std::vector<std::uint32_t> MeshRestartTable(std::span<const std::byte> indices, std::uint32_t indexSize) {
@@ -9,13 +9,29 @@ layout(push_constant) uniform Parameters {
uint maxX;
uint maxY;
uint maxZ;
uint threadDimensions;
uint groupX;
uint groupY;
uint groupZ;
uint unused;
} parameters;
uint groups(uint threads, uint group) {
return threads / group + (threads % group != 0u ? 1u : 0u);
}
void main() {
uint x = parameters.source.words[0];
uint y = parameters.source.words[1];
uint z = parameters.source.words[2];
if (parameters.threadDimensions != 0u) {
parameters.arguments.words[3] = x;
parameters.arguments.words[4] = y;
parameters.arguments.words[5] = z;
x = groups(x, parameters.groupX);
y = groups(y, parameters.groupY);
z = groups(z, parameters.groupZ);
}
bool valid = x <= parameters.maxX && y <= parameters.maxY && z <= parameters.maxZ;
parameters.arguments.words[0] = valid ? x : 0u;
parameters.arguments.words[1] = valid ? y : 0u;
@@ -0,0 +1,60 @@
#version 450
layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
layout(binding = 0, TEXEL_FORMAT) uniform restrict uimage2DMS samples;
layout(push_constant) uniform Order {
uvec4 map;
uvec2 extent;
uint count;
} order;
uvec4 pick(uint source, uvec4 s0, uvec4 s1, uvec4 s2, uvec4 s3, uvec4 s4, uvec4 s5, uvec4 s6, uvec4 s7) {
const bool odd = (source & 1u) != 0u;
const uvec4 low = (source & 2u) == 0u ? (odd ? s1 : s0) : (odd ? s3 : s2);
const uvec4 high = (source & 2u) == 0u ? (odd ? s5 : s4) : (odd ? s7 : s6);
return (source & 4u) == 0u ? low : high;
}
void reorder(ivec2 at, uint map) {
if (map == 0x76543210u || uint(at.x) >= order.extent.x || uint(at.y) >= order.extent.y) return;
const uvec4 s0 = imageLoad(samples, at, 0);
const uvec4 s1 = imageLoad(samples, at, 1);
uvec4 s2 = uvec4(0u);
uvec4 s3 = uvec4(0u);
uvec4 s4 = uvec4(0u);
uvec4 s5 = uvec4(0u);
uvec4 s6 = uvec4(0u);
uvec4 s7 = uvec4(0u);
if (order.count > 2u) {
s2 = imageLoad(samples, at, 2);
s3 = imageLoad(samples, at, 3);
}
if (order.count > 4u) {
s4 = imageLoad(samples, at, 4);
s5 = imageLoad(samples, at, 5);
s6 = imageLoad(samples, at, 6);
s7 = imageLoad(samples, at, 7);
}
imageStore(samples, at, 0, pick(map & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 1, pick((map >> 4u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
if (order.count > 2u) {
imageStore(samples, at, 2, pick((map >> 8u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 3, pick((map >> 12u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
}
if (order.count > 4u) {
imageStore(samples, at, 4, pick((map >> 16u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 5, pick((map >> 20u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 6, pick((map >> 24u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
imageStore(samples, at, 7, pick((map >> 28u) & 7u, s0, s1, s2, s3, s4, s5, s6, s7));
}
}
void main() {
const ivec2 quad = ivec2(gl_GlobalInvocationID.xy) * 2;
reorder(quad, order.map.x);
reorder(quad + ivec2(1, 0), order.map.y);
reorder(quad + ivec2(0, 1), order.map.z);
reorder(quad + ivec2(1, 1), order.map.w);
}
@@ -0,0 +1,31 @@
#version 450
#extension GL_EXT_samplerless_texture_functions : require
#ifdef STENCIL_EXPORT
#extension GL_ARB_shader_stencil_export : require
#endif
layout(binding = 0) uniform texture2DMS depthSource;
layout(binding = 1) uniform utexture2DMS stencilSource;
layout(push_constant) uniform Order {
uvec4 map;
uint bit;
} order;
void main() {
const ivec2 at = ivec2(gl_FragCoord.xy);
const bool right = (at.x & 1) != 0;
const uint top = right ? order.map.y : order.map.x;
const uint bottom = right ? order.map.w : order.map.z;
const uint map = (at.y & 1) != 0 ? bottom : top;
if (map == 0x76543210u) discard;
const int source = int((map >> (uint(gl_SampleID) * 4u)) & 7u);
#ifdef STENCIL_BITS
if (((texelFetch(stencilSource, at, source).r >> order.bit) & 1u) == 0u) discard;
#else
gl_FragDepth = texelFetch(depthSource, at, source).r;
#ifdef STENCIL_EXPORT
gl_FragStencilRefARB = int(texelFetch(stencilSource, at, source).r);
#endif
#endif
}
@@ -0,0 +1,6 @@
#version 450
void main() {
const vec2 corner = vec2(float((gl_VertexIndex << 1) & 2), float(gl_VertexIndex & 2));
gl_Position = vec4(corner * 2.0 - 1.0, 0.0, 1.0);
}
@@ -127,6 +127,7 @@ BdaResources::BdaResources(const Context& context) {
Require(ShaderRecompiler::BdaAbi::FaultBufferBytes <= context.limits.maxStorageBufferRange, "BDA fault buffer exceeds storage buffer range limit");
fault = std::make_unique<Buffer>(context, ShaderRecompiler::BdaAbi::FaultBufferBytes, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
std::memset(fault->Bytes().data(), 0, ShaderRecompiler::BdaAbi::WrittenListWord * sizeof(std::uint32_t));
std::memset(fault->Bytes().data() + ShaderRecompiler::BdaAbi::SrtGuardWord * sizeof(std::uint32_t), 0, sizeof(std::uint32_t));
}
BdaResources::BdaResources(const Context& context, const GuestBufferMemory& memory) : BdaResources(context) {
@@ -244,10 +245,11 @@ 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");
checkSrtGuard();
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 +295,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());
}
@@ -305,6 +311,27 @@ void BdaResources::SetImageTables(std::vector<ImageTableFaults> tables, std::vec
imageTableRanges = std::move(ranges);
}
void BdaResources::SetSrtGuards(std::vector<SrtGuardFaults> guards) {
srtGuards = std::move(guards);
}
void BdaResources::checkSrtGuard() const {
namespace Abi = ShaderRecompiler::BdaAbi;
std::uint32_t guard = 0;
std::memcpy(&guard, fault->Bytes().data() + Abi::SrtGuardWord * sizeof(std::uint32_t), sizeof(guard));
if (guard == 0) return;
const auto stage = guard >> Abi::SrtGuardStageShift;
const auto record = guard & ((1u << Abi::SrtGuardStageShift) - 1u);
const auto found = std::find_if(srtGuards.begin(), srtGuards.end(), [&](const SrtGuardFaults& candidate) { return candidate.stage == stage; });
const auto* guards = found != srtGuards.end() ? &*found : srtGuards.size() == 1 ? &srtGuards.front() : nullptr;
Require(guards != nullptr && record + 2u < guards->flattenedSrt.size(), "a guarded SRT read faulted without its poison record");
const auto& words = guards->flattenedSrt;
const auto address = static_cast<std::uint64_t>(words[record + 1u]) | (static_cast<std::uint64_t>(words[record + 2u]) << 32u);
std::ostringstream message;
message << "BDA access failed: address=0x" << std::hex << address << " instruction=0x" << words[record] << std::dec << " bytes=4 stage=" << stage << " reason=" << static_cast<std::uint32_t>(Abi::FaultReason::Unmapped);
throw std::runtime_error(message.str());
}
void BdaResources::markWrittenPages() const {
namespace Abi = ShaderRecompiler::BdaAbi;
auto* words = reinterpret_cast<std::uint32_t*>(fault->Bytes().data());
@@ -6,6 +6,7 @@
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <limits>
namespace AgcDriver::Graphics {
@@ -37,10 +38,14 @@ constexpr VkBufferUsageFlags deviceUsage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT |
}
BufferPool::BufferPool(const Context& context) : device(context.device), unmap(context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")), allocateMemory(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")), mapMemory(context.Function<PFN_vkMapMemory>("vkMapMemory")), maxSlots(std::max<std::size_t>(1, std::min<std::size_t>(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim) {
BufferPool::BufferPool(const Context& context) : maxSlots(std::max<std::size_t>(1, std::min<std::size_t>(MaxSlots(), context.limits.maxMemoryAllocationCount / 6u))), cacheReclaim(context.cacheReclaim), device(context.device), unmap(context.Function<PFN_vkUnmapMemory>("vkUnmapMemory")), destroyBuffer(context.Function<PFN_vkDestroyBuffer>("vkDestroyBuffer")), freeMemory(context.Function<PFN_vkFreeMemory>("vkFreeMemory")), allocateMemory(context.Function<PFN_vkAllocateMemory>("vkAllocateMemory")), mapMemory(context.Function<PFN_vkMapMemory>("vkMapMemory")), physical(context.physical), memoryProperties(context.memory), memoryProperties2(context.memoryProperties2) {
smallTier.budget = smallBudget;
largeTier.budget = budget;
deviceTier.budget = deviceBudget(context);
memoryProperties = context.memory;
physical = context.physical;
memoryProperties2 = context.memoryProperties2;
imageBudget = ImageBudget(deviceTier.budget);
}
BufferPool::~BufferPool() {
@@ -60,7 +65,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation
unsigned attempts = 1;
VkDeviceSize reclaimed = 0;
VkDeviceSize released = 0;
const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY; };
const auto exhausted = [](VkResult value) { return value == VK_ERROR_OUT_OF_DEVICE_MEMORY || value == VK_ERROR_OUT_OF_HOST_MEMORY || value == VK_ERROR_TOO_MANY_OBJECTS; };
if (exhausted(result)) {
reclaimed = trim();
if (reclaimed != 0) {
@@ -72,6 +77,7 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation
if (exhausted(result)) {
if (cacheReclaim != nullptr) released = cacheReclaim(ReclaimTarget(allocation.allocationSize));
if (released != 0) {
reclaimed += trim();
memory = VK_NULL_HANDLE;
result = allocateMemory(device, &allocation, nullptr, &memory);
++attempts;
@@ -96,12 +102,120 @@ VkDeviceMemory BufferPool::AllocateMemory(const VkMemoryAllocateInfo& allocation
if (attempts > 1) std::fprintf(stderr, "[bufferpool] %s recovered: allocation=%llu reclaimed=%llu released=%llu initialResult=%d attempts=%u\n", operation, static_cast<unsigned long long>(allocation.allocationSize), static_cast<unsigned long long>(reclaimed), static_cast<unsigned long long>(released), static_cast<int>(initialResult), attempts);
return memory;
}
char details[512];
char details[1536];
std::snprintf(details, sizeof(details), "%s: allocation=%llu memoryType=%u reclaimed=%llu released=%llu initialResult=%d attempts=%u", operation, static_cast<unsigned long long>(allocation.allocationSize), allocation.memoryTypeIndex, static_cast<unsigned long long>(reclaimed), static_cast<unsigned long long>(released), static_cast<int>(initialResult), attempts);
if (memoryProperties2 != nullptr && physical != VK_NULL_HANDLE) {
VkPhysicalDeviceMemoryBudgetPropertiesEXT budget{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2, &budget};
memoryProperties2(physical, &properties);
auto used = std::strlen(details);
for (std::uint32_t heap = 0; heap < memoryProperties.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) {
const auto remaining = sizeof(details) - used;
const auto appended = std::snprintf(details + used, remaining, "; heap %u %llu/%llu MiB", heap, static_cast<unsigned long long>(budget.heapUsage[heap] >> 20u), static_cast<unsigned long long>(budget.heapBudget[heap] >> 20u));
if (appended < 0 || static_cast<std::size_t>(appended) >= remaining) break;
used += static_cast<std::size_t>(appended);
}
}
Check(result, details);
return VK_NULL_HANDLE;
}
VkDeviceSize BufferPool::ImageBudget(VkDeviceSize deviceBudget) {
static const char* value = std::getenv("APS5_IMAGE_POOL_MIB");
if (value != nullptr) return static_cast<VkDeviceSize>(std::strtoull(value, nullptr, 10)) << 20u;
return deviceBudget / 4u;
}
bool BufferPool::imagePooled(VkDeviceSize bytes, std::uint32_t memoryType) const {
return imageBudget != 0 && bytes != 0 && bytes <= imageBudget && bytes <= deviceTier.budget && memoryType < memoryProperties.memoryTypeCount && DeviceTiered(memoryProperties.memoryTypes[memoryType].propertyFlags);
}
bool BufferPool::heapOverBudget() {
if (memoryProperties2 == nullptr || physical == VK_NULL_HANDLE) return false;
const auto now = std::chrono::steady_clock::now();
if (heapRead != std::chrono::steady_clock::time_point{} && now - heapRead < std::chrono::milliseconds(100)) return heapOver;
VkPhysicalDeviceMemoryBudgetPropertiesEXT reported{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_BUDGET_PROPERTIES_EXT};
VkPhysicalDeviceMemoryProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_MEMORY_PROPERTIES_2};
properties.pNext = &reported;
memoryProperties2(physical, &properties);
heapRead = now;
heapOver = false;
for (std::uint32_t heap = 0; heap < memoryProperties.memoryHeapCount && heap < VK_MAX_MEMORY_HEAPS; ++heap) {
if ((memoryProperties.memoryHeaps[heap].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) != 0 && reported.heapBudget[heap] != 0 && reported.heapUsage[heap] > reported.heapBudget[heap]) heapOver = true;
}
return heapOver;
}
VkDeviceMemory BufferPool::TakeImageMemory(const VkMemoryAllocateInfo& allocation, const char* operation, std::optional<std::uint32_t> hostType, std::uint32_t* allocatedType) {
const auto type = allocation.memoryTypeIndex;
if (imagePooled(allocation.allocationSize, type)) {
std::lock_guard lock(mutex);
++clock;
const auto found = deviceTier.free.find({static_cast<std::size_t>(allocation.allocationSize), 0, memoryProperties.memoryTypes[type].propertyFlags, type});
if (found != deviceTier.free.end() && !found->second.empty()) {
auto& slots = found->second;
const auto memory = slots.back().allocation.memory;
imageOrder.erase(slots.back().order);
slots.pop_back();
if (slots.empty()) deviceTier.free.erase(found);
--deviceTier.slots;
deviceTier.retainedBytes -= allocation.allocationSize;
--imageSlots;
imageBytes -= allocation.allocationSize;
++imageHits;
if (allocatedType != nullptr) *allocatedType = type;
return memory;
}
++imageMisses;
}
return AllocateMemory(allocation, operation, hostType, allocatedType);
}
void BufferPool::PutImageMemory(VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept {
if (memory == VK_NULL_HANDLE) return;
if (!imagePooled(bytes, memoryType)) {
freeMemory(device, memory, nullptr);
return;
}
BufferAllocation allocation{VK_NULL_HANDLE, memory, nullptr, 0, bytes, static_cast<std::size_t>(bytes), 0, memoryProperties.memoryTypes[memoryType].propertyFlags};
allocation.memoryType = memoryType;
std::vector<BufferAllocation> evicted;
try {
std::lock_guard lock(mutex);
if (heapOverBudget()) {
while (imageSlots != 0 && evictOldestImage(evicted)) {
}
evicted.push_back(allocation);
} else {
while (imageSlots != 0 && imageBytes + bytes > imageBudget && evictOldestImage(evicted)) {
}
while (deviceTier.slots != 0 && (deviceTier.retainedBytes + bytes > deviceTier.budget || deviceTier.slots >= maxSlots)) evictOldest(deviceTier, evicted);
const SlotKey imageKey{allocation.bytes, 0, allocation.properties, memoryType};
auto& slots = deviceTier.free[imageKey];
slots.push_back({allocation, ++clock, {}});
try {
slots.back().order = imageOrder.insert(imageOrder.end(), &slots.back());
} catch (...) {
slots.pop_back();
if (slots.empty()) deviceTier.free.erase(imageKey);
throw;
}
++deviceTier.slots;
deviceTier.retainedBytes += bytes;
++imageSlots;
imageBytes += bytes;
}
} catch (...) {
destroy(allocation);
}
for (const auto& gone : evicted) destroy(gone);
}
std::size_t BufferPool::RetainedImages() {
std::lock_guard lock(mutex);
return imageSlots;
}
VkDeviceSize BufferPool::ReclaimTarget(VkDeviceSize bytes) {
return bytes + std::max<VkDeviceSize>(bytes, VkDeviceSize{256} << 20u);
}
@@ -161,11 +275,12 @@ SlabSlot BufferPool::TakeSlot(const Context& context, std::uint32_t memoryType,
for (std::uint32_t index = slots; index-- != 0;) block->free.push_back(index);
std::lock_guard lock(slabMutex);
auto& slab = slabs[key];
if (slab.available.size() == slab.available.capacity()) slab.available.reserve(std::max<std::size_t>(1, slab.available.capacity() * 2));
if (slab.blocks == slab.available.capacity()) slab.available.reserve(std::max<std::size_t>(1, slab.available.capacity() * 2));
auto& inserted = *block;
const auto owner = slabBlocks.try_emplace(inserted.memory).first;
owner->second.reset(block.release());
slab.available.push_back(&inserted);
++slab.blocks;
++slab.emptyBlocks;
return takeFrom(*slab.available.back(), slab);
}
@@ -191,6 +306,7 @@ VkDeviceSize BufferPool::PutSlot(VkDeviceMemory memory, VkDeviceSize offset) noe
return 0;
}
slab.available.erase(std::find(slab.available.begin(), slab.available.end(), &block));
--slab.blocks;
released = std::move(found->second);
slabBlocks.erase(found);
}
@@ -241,7 +357,7 @@ VkDeviceSize BufferPool::destroy(const BufferAllocation& allocation) noexcept {
}
// Device-local allocations (see DeviceBuffer) are never mapped.
if (allocation.mapping != nullptr) unmap(device, allocation.memory);
destroyBuffer(device, allocation.buffer, nullptr);
if (allocation.buffer != VK_NULL_HANDLE) destroyBuffer(device, allocation.buffer, nullptr);
freeMemory(device, allocation.memory, nullptr);
return allocation.allocationBytes;
}
@@ -282,7 +398,7 @@ std::optional<BufferAllocation> BufferPool::Take(std::size_t bytes, VkBufferUsag
const auto now = std::chrono::steady_clock::now();
if (now - lastReport > std::chrono::seconds(10)) {
lastReport = now;
AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast<unsigned long long>(smallTier.hits), static_cast<unsigned long long>(smallTier.misses), static_cast<unsigned long long>(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast<unsigned long long>(largeTier.hits), static_cast<unsigned long long>(largeTier.misses), static_cast<unsigned long long>(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast<unsigned long long>(deviceTier.hits), static_cast<unsigned long long>(deviceTier.misses), static_cast<unsigned long long>(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast<unsigned long long>(deviceTier.budget >> 20u));
AgcDriver::ProfilePrint_nid_no_patch("[bufferpool] small: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB); large: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB)%s; device: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu); images: %llu hits, %llu misses, %llu evictions, %zu retained (%.0f MiB of %llu)\n", static_cast<unsigned long long>(smallTier.hits), static_cast<unsigned long long>(smallTier.misses), static_cast<unsigned long long>(smallTier.evictions), smallTier.slots, smallTier.retainedBytes / 1048576.0, static_cast<unsigned long long>(largeTier.hits), static_cast<unsigned long long>(largeTier.misses), static_cast<unsigned long long>(largeTier.evictions), largeTier.slots, largeTier.retainedBytes / 1048576.0, sharedTiers() ? " (shared)" : "", static_cast<unsigned long long>(deviceTier.hits), static_cast<unsigned long long>(deviceTier.misses), static_cast<unsigned long long>(deviceTier.evictions), deviceTier.slots, deviceTier.retainedBytes / 1048576.0, static_cast<unsigned long long>(deviceTier.budget >> 20u), static_cast<unsigned long long>(imageHits), static_cast<unsigned long long>(imageMisses), static_cast<unsigned long long>(imageEvictions), imageSlots, imageBytes / 1048576.0, static_cast<unsigned long long>(imageBudget >> 20u));
}
}
auto& tier = tierFor(capacity, properties);
@@ -307,12 +423,37 @@ void BufferPool::evictOldest(Tier& tier, std::vector<BufferAllocation>& evicted)
// First: a throw here leaves the slot retained and counted.
evicted.push_back(oldest->second.front().allocation);
tier.retainedBytes -= oldest->second.front().allocation.allocationBytes;
if (oldest->second.front().allocation.buffer == VK_NULL_HANDLE) {
imageOrder.erase(oldest->second.front().order);
imageBytes -= oldest->second.front().allocation.allocationBytes;
--imageSlots;
++imageEvictions;
}
oldest->second.pop_front();
if (oldest->second.empty()) tier.free.erase(oldest);
--tier.slots;
++tier.evictions;
}
bool BufferPool::evictOldestImage(std::vector<BufferAllocation>& evicted) {
if (imageOrder.empty()) return false;
const Slot* oldest = imageOrder.front();
const SlotKey key{oldest->allocation.bytes, 0, oldest->allocation.properties, oldest->allocation.memoryType};
const auto found = deviceTier.free.find(key);
evicted.push_back(oldest->allocation);
imageOrder.pop_front();
const auto bytes = oldest->allocation.allocationBytes;
deviceTier.retainedBytes -= bytes;
imageBytes -= bytes;
found->second.pop_front();
if (found->second.empty()) deviceTier.free.erase(found);
--deviceTier.slots;
--imageSlots;
++deviceTier.evictions;
++imageEvictions;
return true;
}
std::size_t BufferPool::MaxSlots() {
// APS5_BUFFER_POOL_SLOTS=<n> bounds the retained allocations; 64 is the capacity the pool had
// before least-recently-used retention, for A/B runs.
@@ -336,6 +477,10 @@ VkDeviceSize BufferPool::trim() {
tier->retainedBytes = 0;
tier->slots = 0;
}
imageEvictions += imageSlots;
imageBytes = 0;
imageSlots = 0;
imageOrder.clear();
}
VkDeviceSize bytes = 0;
for (const auto* tier : {&retiredSmall, &retiredLarge, &retiredDevice}) {
@@ -351,6 +496,7 @@ VkDeviceSize BufferPool::trim() {
if (found == slabBlocks.end()) break;
auto& slab = slabs.at(found->second->slab);
slab.available.erase(std::find(slab.available.begin(), slab.available.end(), found->second.get()));
--slab.blocks;
--slab.emptyBlocks;
released = std::move(found->second);
slabBlocks.erase(found);
@@ -372,7 +518,7 @@ void BufferPool::Put(const BufferAllocation& allocation) noexcept {
evicted.push_back(allocation);
} else {
while (tier.slots != 0 && (tier.retainedBytes + allocation.allocationBytes > tier.budget || tier.slots >= maxSlots)) evictOldest(tier, evicted);
tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock});
tier.free[{allocation.bytes, allocation.usage, allocation.properties}].push_back({allocation, ++clock, {}});
++tier.slots;
tier.retainedBytes += allocation.allocationBytes;
}
@@ -387,8 +533,17 @@ std::shared_ptr<BufferPool> GetBufferPool(const Context& context) {
return context.bufferPool;
}
VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation) {
return GetBufferPool(context)->AllocateMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex));
VkDeviceMemory AllocateImageMemory(const Context& context, const VkMemoryAllocateInfo& allocation, std::uint32_t mask, const char* operation, std::uint32_t* allocatedType) {
return GetBufferPool(context)->TakeImageMemory(allocation, operation, context.RefusedDeviceMemoryFallback(mask, allocation.memoryTypeIndex), allocatedType);
}
void ReleaseImageMemory(const Context& context, VkDeviceMemory memory, VkDeviceSize bytes, std::uint32_t memoryType) noexcept {
if (memory == VK_NULL_HANDLE) return;
if (context.bufferPool) {
context.bufferPool->PutImageMemory(memory, bytes, memoryType);
return;
}
context.Function<PFN_vkFreeMemory>("vkFreeMemory")(context.device, memory, nullptr);
}
}
@@ -611,6 +611,22 @@ bool IsDccClear(DccKeys keys) {
return keys == DccKeys::Clear0000 || keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110 || keys == DccKeys::Clear1111 || keys == DccKeys::ClearRegister;
}
std::optional<std::uint8_t> LandedUniformMetadataByte(std::uint64_t address, std::size_t bytes) {
if (address == 0 || bytes == 0 || !GuestMemory::Accessible(reinterpret_cast<const void*>(address), bytes)) return std::nullopt;
if (AnyShadowedOverlaps(address, bytes)) PublishShadow(address, bytes, PublishScope::Whole, PublishReason::Keys);
if (AnyShadowedOverlaps(address, bytes)) return std::nullopt;
GuestMemory::FlushGpuWrites(address, bytes);
auto* recorder = GuestMemory::GpuMutex().HeldByThisThread() ? Recorder::Active() : nullptr;
if (recorder != nullptr && recorder->PendingWriteOverlaps(address, bytes)) {
const auto landed = KeysAfterPendingWrites(*recorder, address, bytes);
if (landed.mixed || landed.unknown || landed.value < 0) return std::nullopt;
return static_cast<std::uint8_t>(landed.value);
}
const auto* data = reinterpret_cast<const std::uint8_t*>(address);
if (!AllKeysEqual(data + 1, bytes - 1, data[0])) return std::nullopt;
return data[0];
}
void MarkDccUncompressed(std::uint64_t metaAddress, std::uint64_t surfaceBytes) {
MarkDccUncompressed(metaAddress, surfaceBytes, DccKeyBytes(surfaceBytes));
}
Loaded 100 of 1186 files, more files were not shown because too many files have changed in this diff. Show more