Merge branch 'main' of https://github.com/boykopovar/AnyPS5 into fix/audioout2-read-grain-at-playback
This commit is contained in:
commit
e8f77dafa0
|
|
@ -0,0 +1,22 @@
|
|||
name: Conventions
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: conventions-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
conventions:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: python3 tools/check_conventions.py --base ${{ github.event.pull_request.base.sha }} --head ${{ github.event.pull_request.head.sha }}
|
||||
|
|
@ -28,3 +28,6 @@
|
|||
[submodule "3rdparty/stb"]
|
||||
path = 3rdparty/stb
|
||||
url = https://github.com/nothings/stb.git
|
||||
[submodule "3rdparty/libjpeg-turbo"]
|
||||
path = 3rdparty/libjpeg-turbo
|
||||
url = https://github.com/libjpeg-turbo/libjpeg-turbo.git
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
Subproject commit c85e6b905bf237038faa936dab160ebfc5da0344
|
||||
|
|
@ -4,9 +4,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).
|
||||
- A silent stub is allowed only when it unblocks a title and only affects the UI; add it to [silent stubs](docs/dev/TechnicalDebt.md#silent-stubs).
|
||||
- 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).
|
||||
- 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.
|
||||
|
|
@ -24,6 +26,12 @@ ctest --test-dir build --output-on-failure
|
|||
|
||||
Python 3 is optional; without it some relinker tests are not registered.
|
||||
|
||||
The Conventions check runs on every pull request and fails when a rule on this page is broken. It accepts code comments only when the pull request also changes [TechnicalDebt](docs/dev/TechnicalDebt.md), and only UTF-8 text files. Run it locally before pushing:
|
||||
|
||||
```
|
||||
python3 tools/check_conventions.py --base origin/main
|
||||
```
|
||||
|
||||
## Branches and pull requests
|
||||
|
||||
The [pull request template](.github/pull_request_template.md) is the checklist for these rules.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Tool for automatic executables porting to Linux and Windows.
|
|||
|
||||
Includes a [relinker](core/relinker) that converts executable to the target system's native format and implementations of [system prx libraries](core/libs/prx) suitable for dynamic linking. No emulation or separate runtime process.
|
||||
|
||||
[Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md)
|
||||
[Usage](docs/user/USAGE.md), [Build instructions](docs/dev/BUILD.md), [Architecture](docs/dev/ARCHITECTURE.md), [Technical debt of the project](docs/dev/TechnicalDebt.md), [code style conventions](docs/dev/CONVENTIONS.md), [contributing](CONTRIBUTING.md)
|
||||
|
||||
## Status
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,56 @@ function(add_jpeg_decoder target runtimeTarget)
|
|||
message(FATAL_ERROR "JPEG decoder requires an existing runtime target: ${runtimeTarget}")
|
||||
endif()
|
||||
|
||||
include(ExternalProject)
|
||||
if(NOT TARGET jpeg_turbo_external)
|
||||
set(jpeg_turbo_prefix "${CMAKE_BINARY_DIR}/libjpeg-turbo")
|
||||
if(WIN32)
|
||||
if(MSVC)
|
||||
set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/turbojpeg-static.lib")
|
||||
else()
|
||||
set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a")
|
||||
endif()
|
||||
else()
|
||||
set(jpeg_turbo_library "${jpeg_turbo_prefix}/lib/libturbojpeg.a")
|
||||
endif()
|
||||
file(MAKE_DIRECTORY "${jpeg_turbo_prefix}/include")
|
||||
ExternalProject_Add(jpeg_turbo_external
|
||||
SOURCE_DIR "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo"
|
||||
BINARY_DIR "${CMAKE_BINARY_DIR}/libjpeg-turbo-build"
|
||||
INSTALL_DIR "${jpeg_turbo_prefix}"
|
||||
CMAKE_ARGS
|
||||
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
|
||||
-DCMAKE_INSTALL_PREFIX=${jpeg_turbo_prefix}
|
||||
-DBUILD_SHARED_LIBS=OFF
|
||||
-DENABLE_STATIC=ON
|
||||
-DENABLE_SHARED=OFF
|
||||
-DWITH_SIMD=OFF
|
||||
-DWITH_TURBOJPEG=ON
|
||||
-DWITH_TOOLS=OFF
|
||||
-DWITH_TESTS=OFF
|
||||
-DCMAKE_INSTALL_LIBDIR=lib
|
||||
-DCMAKE_C_VISIBILITY_PRESET=hidden
|
||||
-DCMAKE_VISIBILITY_INLINES_HIDDEN=ON
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
|
||||
-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}
|
||||
-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=${CMAKE_C_COMPILER_LAUNCHER}
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=${CMAKE_CXX_COMPILER_LAUNCHER}
|
||||
BUILD_BYPRODUCTS
|
||||
"${jpeg_turbo_library}"
|
||||
)
|
||||
add_library(jpeg_turbo STATIC IMPORTED GLOBAL)
|
||||
set_target_properties(jpeg_turbo PROPERTIES
|
||||
IMPORTED_LOCATION "${jpeg_turbo_library}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${PROJECT_SOURCE_DIR}/3rdparty/libjpeg-turbo/src;${jpeg_turbo_prefix}/include"
|
||||
)
|
||||
add_dependencies(jpeg_turbo jpeg_turbo_external)
|
||||
endif()
|
||||
|
||||
add_library(${target} STATIC ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/src/Jpeg.cpp)
|
||||
target_include_directories(${target} PUBLIC ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/include)
|
||||
target_include_directories(${target} SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/3rdparty/stb)
|
||||
target_link_libraries(${target} PRIVATE ${runtimeTarget})
|
||||
target_link_libraries(${target} PRIVATE ${runtimeTarget} jpeg_turbo)
|
||||
set_target_properties(${target} PROPERTIES
|
||||
CXX_EXTENSIONS OFF
|
||||
CXX_VISIBILITY_PRESET hidden
|
||||
|
|
|
|||
|
|
@ -21,8 +21,15 @@ struct Header {
|
|||
std::uint32_t channels;
|
||||
};
|
||||
|
||||
enum class Sampling {
|
||||
Yuv444,
|
||||
Yuv422,
|
||||
Yuv420,
|
||||
};
|
||||
|
||||
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t channels, int quality);
|
||||
std::uint32_t channels, int quality, Sampling sampling = Sampling::Yuv444,
|
||||
std::uint32_t restartBlocks = 0, std::uint32_t restartRows = 0);
|
||||
|
||||
std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,16 +4,12 @@
|
|||
#include <cstddef>
|
||||
#include <stdexcept>
|
||||
|
||||
#define STB_IMAGE_WRITE_STATIC
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#define STBI_WRITE_NO_STDIO
|
||||
#include "stb_image_write.h"
|
||||
|
||||
#define STB_IMAGE_STATIC
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_ONLY_JPEG
|
||||
#define STBI_NO_STDIO
|
||||
#include "stb_image.h"
|
||||
#include "turbojpeg.h"
|
||||
|
||||
namespace Decoder::Jpeg {
|
||||
|
||||
|
|
@ -21,16 +17,40 @@ namespace {
|
|||
|
||||
constexpr std::uint32_t MAX_DIMENSION = 0xFFFF;
|
||||
|
||||
void appendBytes(void* context, void* data, int size) {
|
||||
auto* output = static_cast<std::vector<std::uint8_t>*>(context);
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(data);
|
||||
output->insert(output->end(), bytes, bytes + size);
|
||||
class Handle {
|
||||
public:
|
||||
Handle() : handle(tj3Init(TJINIT_COMPRESS)) {
|
||||
if (!handle) throw std::runtime_error("Jpeg::Encode: encoder initialization failed");
|
||||
}
|
||||
~Handle() { tj3Destroy(handle); }
|
||||
Handle(const Handle&) = delete;
|
||||
Handle& operator=(const Handle&) = delete;
|
||||
operator tjhandle() const { return handle; }
|
||||
|
||||
private:
|
||||
tjhandle handle;
|
||||
};
|
||||
|
||||
class Buffer {
|
||||
public:
|
||||
~Buffer() { tj3Free(data); }
|
||||
unsigned char* data = nullptr;
|
||||
};
|
||||
|
||||
int toSubsampling(Sampling sampling) {
|
||||
switch (sampling) {
|
||||
case Sampling::Yuv444: return TJSAMP_444;
|
||||
case Sampling::Yuv422: return TJSAMP_422;
|
||||
case Sampling::Yuv420: return TJSAMP_420;
|
||||
}
|
||||
throw std::invalid_argument("Jpeg::Encode: invalid sampling");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint32_t width, std::uint32_t height,
|
||||
std::uint32_t channels, int quality) {
|
||||
std::uint32_t channels, int quality, Sampling sampling, std::uint32_t restartBlocks,
|
||||
std::uint32_t restartRows) {
|
||||
if (width == 0 || height == 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
|
||||
throw std::invalid_argument("Jpeg::Encode: unsupported image size");
|
||||
}
|
||||
|
|
@ -39,12 +59,25 @@ std::vector<std::uint8_t> Encode(std::span<const std::uint8_t> pixels, std::uint
|
|||
if (pixels.size() < static_cast<std::size_t>(width) * height * channels) {
|
||||
throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small");
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> output;
|
||||
const int written = stbi_write_jpg_to_func(appendBytes, &output, static_cast<int>(width), static_cast<int>(height),
|
||||
static_cast<int>(channels), pixels.data(), quality);
|
||||
if (written == 0) throw std::runtime_error("Jpeg::Encode: encoding failed");
|
||||
return output;
|
||||
if (restartBlocks > MAX_DIMENSION || restartRows > MAX_DIMENSION || (restartBlocks != 0 && restartRows != 0)) {
|
||||
throw std::invalid_argument("Jpeg::Encode: invalid restart interval");
|
||||
}
|
||||
const int subsampling = toSubsampling(sampling);
|
||||
Handle handle;
|
||||
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");
|
||||
}
|
||||
if ((restartBlocks != 0 && tj3Set(handle, TJPARAM_RESTARTBLOCKS, static_cast<int>(restartBlocks)) < 0) ||
|
||||
(restartRows != 0 && tj3Set(handle, TJPARAM_RESTARTROWS, static_cast<int>(restartRows)) < 0)) {
|
||||
throw std::runtime_error("Jpeg::Encode: encoder configuration failed");
|
||||
}
|
||||
Buffer buffer;
|
||||
std::size_t size = 0;
|
||||
const int pixelFormat = channels == 1 ? TJPF_GRAY : TJPF_RGB;
|
||||
if (tj3Compress8(handle, pixels.data(), static_cast<int>(width), 0, static_cast<int>(height), pixelFormat, &buffer.data, &size) < 0) {
|
||||
throw std::runtime_error("Jpeg::Encode: encoding failed");
|
||||
}
|
||||
return {buffer.data, buffer.data + size};
|
||||
}
|
||||
|
||||
std::optional<Header> ParseHeader(std::span<const std::uint8_t> jpeg) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
|
@ -25,6 +26,25 @@ static int Difference(std::uint8_t left, std::uint8_t right) {
|
|||
return left > right ? left - right : right - left;
|
||||
}
|
||||
|
||||
struct FrameInfo { int components; std::uint8_t sampling; };
|
||||
|
||||
static FrameInfo ReadFrame(const std::vector<std::uint8_t>& jpeg) {
|
||||
std::size_t offset = 2;
|
||||
while (offset + 3 < jpeg.size()) {
|
||||
Require(jpeg[offset++] == 0xFF);
|
||||
while (offset < jpeg.size() && jpeg[offset] == 0xFF) ++offset;
|
||||
Require(offset < jpeg.size());
|
||||
const std::uint8_t marker = jpeg[offset++];
|
||||
if (marker == 0xDA || marker == 0xD9) break;
|
||||
Require(offset + 1 < jpeg.size());
|
||||
const std::size_t length = (static_cast<std::size_t>(jpeg[offset]) << 8) | jpeg[offset + 1];
|
||||
Require(length >= 2 && offset + length <= jpeg.size());
|
||||
if (marker >= 0xC0 && marker <= 0xC3) return {jpeg[offset + 7], jpeg[offset + 9]};
|
||||
offset += length;
|
||||
}
|
||||
std::abort();
|
||||
}
|
||||
|
||||
int main() {
|
||||
constexpr std::uint32_t width = 32;
|
||||
constexpr std::uint32_t height = 24;
|
||||
|
|
@ -41,6 +61,7 @@ int main() {
|
|||
|
||||
const std::vector<std::uint8_t> rgbJpeg = Decoder::Jpeg::Encode(rgb, width, height, 3, 90);
|
||||
Require(IsJpeg(rgbJpeg));
|
||||
Require(ReadFrame(rgbJpeg).components == 3 && ReadFrame(rgbJpeg).sampling == 0x11);
|
||||
const auto rgbImage = Decoder::Jpeg::Decode(rgbJpeg);
|
||||
Require(rgbImage.has_value());
|
||||
Require(rgbImage->width == width && rgbImage->height == height && rgbImage->channels == 3);
|
||||
|
|
@ -55,6 +76,7 @@ int main() {
|
|||
}
|
||||
const std::vector<std::uint8_t> grayJpeg = Decoder::Jpeg::Encode(gray, width, height, 1, 90);
|
||||
Require(IsJpeg(grayJpeg));
|
||||
Require(ReadFrame(grayJpeg).components == 1);
|
||||
const auto grayImage = Decoder::Jpeg::Decode(grayJpeg);
|
||||
Require(grayImage.has_value());
|
||||
Require(grayImage->width == width && grayImage->height == height);
|
||||
|
|
@ -69,6 +91,19 @@ int main() {
|
|||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, 0x10000, 1, 3, 90); }));
|
||||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height + 1, 3, 90); }));
|
||||
|
||||
for (const auto [sampling, factor] : {std::pair{Decoder::Jpeg::Sampling::Yuv444, std::uint8_t{0x11}},
|
||||
std::pair{Decoder::Jpeg::Sampling::Yuv422, std::uint8_t{0x21}},
|
||||
std::pair{Decoder::Jpeg::Sampling::Yuv420, std::uint8_t{0x22}}}) {
|
||||
const auto encoded = Decoder::Jpeg::Encode(rgb, 31, 23, 3, 1, sampling);
|
||||
Require(ReadFrame(encoded).components == 3 && ReadFrame(encoded).sampling == factor);
|
||||
const auto highQuality = Decoder::Jpeg::Encode(rgb, 31, 23, 3, 100, sampling);
|
||||
Require(ReadFrame(highQuality).components == 3 && ReadFrame(highQuality).sampling == factor);
|
||||
}
|
||||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, static_cast<Decoder::Jpeg::Sampling>(3)); }));
|
||||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 1, 1); }));
|
||||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 0x10000, 0); }));
|
||||
Require(ThrowsInvalidArgument([&] { Decoder::Jpeg::Encode(rgb, width, height, 3, 90, Decoder::Jpeg::Sampling::Yuv444, 0, 0x10000); }));
|
||||
|
||||
Require(!Decoder::Jpeg::Decode({}).has_value());
|
||||
const std::vector<std::uint8_t> garbage{1, 2, 3, 4, 5, 6, 7, 8};
|
||||
Require(!Decoder::Jpeg::Decode(garbage).has_value());
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ set_target_properties(libc PROPERTIES
|
|||
RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
|
||||
LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR}
|
||||
BUILD_WITH_INSTALL_RPATH ON
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
)
|
||||
|
||||
set(LIBC_UNPATCHED ${UNPATCHED_DIR}/libc.prx)
|
||||
|
|
@ -101,6 +103,8 @@ foreach(LIB_DIR ${LIBS_DIRS})
|
|||
RUNTIME_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
|
||||
LIBRARY_OUTPUT_DIRECTORY ${UNPATCHED_DIR}
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${IMPLIB_DIR}
|
||||
BUILD_WITH_INSTALL_RPATH ON
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
)
|
||||
|
||||
set(UNPATCHED_FILE ${UNPATCHED_DIR}/${LIB_NAME}.prx)
|
||||
|
|
@ -118,17 +122,6 @@ foreach(LIB_DIR ${LIBS_DIRS})
|
|||
endif()
|
||||
endforeach()
|
||||
|
||||
set(LIBCOHTML_PATCHED ${PATCHED_DIR}/libcohtml.prx)
|
||||
set(LIBCOHTML_PROSPERO_PATCHED ${PATCHED_DIR}/libcohtml.Prospero.prx)
|
||||
add_custom_command(
|
||||
OUTPUT ${LIBCOHTML_PROSPERO_PATCHED}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${LIBCOHTML_PATCHED}" "${LIBCOHTML_PROSPERO_PATCHED}"
|
||||
DEPENDS ${LIBCOHTML_PATCHED}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(libcohtml_prospero DEPENDS ${LIBCOHTML_PROSPERO_PATCHED})
|
||||
list(APPEND PATCHED_OUTPUTS ${LIBCOHTML_PROSPERO_PATCHED})
|
||||
|
||||
add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS})
|
||||
|
||||
if(BUILD_TESTING)
|
||||
|
|
@ -170,16 +163,6 @@ if(BUILD_TESTING)
|
|||
add_test(NAME host_thread_local COMMAND host_thread_local_tests)
|
||||
set_tests_properties(host_thread_local PROPERTIES TIMEOUT 15)
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -199,13 +199,39 @@ struct FileStat {
|
|||
KernelTimespec st_birthtim;
|
||||
};
|
||||
|
||||
struct ModuleInfo {
|
||||
std::uint64_t size;
|
||||
std::uint64_t info[32];
|
||||
KernelModule handle;
|
||||
std::uint8_t pad[156];
|
||||
struct ModuleSegmentInfo {
|
||||
std::uint64_t address;
|
||||
std::uint32_t size;
|
||||
std::int32_t prot;
|
||||
};
|
||||
|
||||
struct ModuleInfoEx {
|
||||
std::uint64_t st_size;
|
||||
char name[256];
|
||||
KernelModule id;
|
||||
std::uint32_t tls_index;
|
||||
std::uint64_t tls_init_addr;
|
||||
std::uint32_t tls_init_size;
|
||||
std::uint32_t tls_size;
|
||||
std::uint32_t tls_offset;
|
||||
std::uint32_t tls_align;
|
||||
std::uint64_t init_proc_addr;
|
||||
std::uint64_t fini_proc_addr;
|
||||
std::uint64_t reserved1;
|
||||
std::uint64_t reserved2;
|
||||
std::uint64_t eh_frame_hdr_addr;
|
||||
std::uint64_t eh_frame_addr;
|
||||
std::uint32_t eh_frame_hdr_size;
|
||||
std::uint32_t eh_frame_size;
|
||||
ModuleSegmentInfo segments[4];
|
||||
std::uint32_t segment_count;
|
||||
std::uint32_t ref_count;
|
||||
};
|
||||
static_assert(offsetof(ModuleInfoEx, id) == 0x108);
|
||||
static_assert(offsetof(ModuleInfoEx, eh_frame_hdr_addr) == 0x148);
|
||||
static_assert(offsetof(ModuleInfoEx, segments) == 0x160);
|
||||
static_assert(sizeof(ModuleInfoEx) == 0x1A8);
|
||||
|
||||
struct ModuleInfoForUnwind {
|
||||
std::uint64_t st_size;
|
||||
char name[256];
|
||||
|
|
@ -1703,6 +1729,8 @@ struct SystemGestureTouchEvent { std::uint8_t reserve[168]; };
|
|||
|
||||
struct UserServiceLoginUserIdList { int user_id[4]; };
|
||||
|
||||
struct UserServiceRegisteredUserIdList { int user_id[16]; };
|
||||
|
||||
struct SceUserServiceEvent {
|
||||
std::uint32_t event_type;
|
||||
int user_id;
|
||||
|
|
|
|||
|
|
@ -83,7 +83,13 @@ struct GnuHashLayout {
|
|||
|
||||
constexpr std::int64_t kDtNeeded = 1;
|
||||
constexpr std::int64_t kDtSoname = 14;
|
||||
constexpr std::int64_t kDtRpath = 15;
|
||||
constexpr std::int64_t kDtRunpath = 29;
|
||||
constexpr std::int64_t kDtNull = 0;
|
||||
|
||||
inline bool IsDynamicNameTag(std::int64_t tag) {
|
||||
return tag == kDtNeeded || tag == kDtSoname || tag == kDtRpath || tag == kDtRunpath;
|
||||
}
|
||||
constexpr std::uint32_t kShtDynamic = 6u;
|
||||
constexpr std::uint32_t kShtGnuVerneed = 0x6ffffffeu;
|
||||
constexpr std::uint32_t kShtDynsym = 11u;
|
||||
|
|
|
|||
|
|
@ -190,7 +190,7 @@ void RemapDynamicNeededOffsets(
|
|||
for (std::size_t d = 0u; d < dynCount; ++d) {
|
||||
const std::size_t entryOffset = dynOffset + d * sizeof(Elf64_Dyn);
|
||||
const auto dyn = Read<Elf64_Dyn>(elf, entryOffset);
|
||||
if (dyn.d_tag != kDtNeeded && dyn.d_tag != kDtSoname) continue;
|
||||
if (!IsDynamicNameTag(dyn.d_tag)) continue;
|
||||
|
||||
const auto oldNameOffset = static_cast<std::uint32_t>(dyn.d_val);
|
||||
const auto mapped = std::find_if(
|
||||
|
|
@ -266,7 +266,7 @@ std::vector<std::uint32_t> CollectDynamicNameOffsets(const std::vector<std::uint
|
|||
for (std::size_t d = 0u; d < dynCount; ++d) {
|
||||
const auto dyn = Read<Elf64_Dyn>(elf, dynOffset + d * sizeof(Elf64_Dyn));
|
||||
if (dyn.d_tag == kDtNull) break;
|
||||
if (dyn.d_tag != kDtNeeded && dyn.d_tag != kDtSoname) continue;
|
||||
if (!IsDynamicNameTag(dyn.d_tag)) continue;
|
||||
offsets.push_back(static_cast<std::uint32_t>(dyn.d_val));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
DT_NULL = 0
|
||||
DT_NEEDED = 1
|
||||
DT_SONAME = 14
|
||||
DT_RPATH = 15
|
||||
DT_RUNPATH = 29
|
||||
SHT_DYNAMIC = 6
|
||||
|
||||
|
||||
def dynamic_strings(image):
|
||||
shoff, = struct.unpack_from("<Q", image, 0x28)
|
||||
shentsize, shnum = struct.unpack_from("<HH", image, 0x3A)
|
||||
sections = [struct.unpack_from("<IIQQQQIIQQ", image, shoff + index * shentsize) for index in range(shnum)]
|
||||
dynamic = next(section for section in sections if section[1] == SHT_DYNAMIC)
|
||||
strings = sections[dynamic[6]]
|
||||
entries = []
|
||||
for position in range(dynamic[4], dynamic[4] + dynamic[5], 16):
|
||||
tag, value = struct.unpack_from("<qQ", image, position)
|
||||
if tag == DT_NULL:
|
||||
break
|
||||
if tag in (DT_NEEDED, DT_SONAME, DT_RPATH, DT_RUNPATH):
|
||||
assert value < strings[5], (tag, value)
|
||||
start = strings[4] + value
|
||||
entries.append((tag, image[start:image.index(0, start)].decode()))
|
||||
return entries
|
||||
|
||||
|
||||
def main():
|
||||
entries = dynamic_strings(Path(sys.argv[1]).read_bytes())
|
||||
runpaths = [value for tag, value in entries if tag in (DT_RPATH, DT_RUNPATH)]
|
||||
assert runpaths == ["$ORIGIN"], entries
|
||||
needed = [value for tag, value in entries if tag == DT_NEEDED]
|
||||
assert needed and all(re.fullmatch(r"[\w.+-]+\.so(\.\d+)*", name) for name in needed), entries
|
||||
print("NID patcher dynamic string tests passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
#include <cstdint>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
|
||||
// CD Projekt RED GOG Galaxy wrapper, bundled with the game as
|
||||
// modules/libREDGalaxy64.prx (seen in Cyberpunk 2077 PPSA04029).
|
||||
// Online Galaxy services cannot work under emulation: Init/Shutdown/
|
||||
// ProcessData are accepted and ignored, interface getters return null
|
||||
// (all observed engine call sites null-check the result).
|
||||
// Roles below were reversed from the module's own export bodies:
|
||||
// Init references clientID/clientSecret/configFilePath, Shutdown
|
||||
// releases the facade singletons, ProcessData throttles facade
|
||||
// ProcessData calls, GetErrorString formats Gog/Std exceptions.
|
||||
|
||||
APS5_EXPORT("1icN9EO2WSg", redGalaxy64Init);
|
||||
std::uint64_t APS5_VABI redGalaxy64Init(const void* options) {
|
||||
(void)options;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 1;
|
||||
}
|
||||
|
||||
APS5_EXPORT("SA7aCTRE5zg", redGalaxy64Shutdown);
|
||||
void APS5_VABI redGalaxy64Shutdown(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
}
|
||||
|
||||
APS5_EXPORT("ZVPOpEqpUvo", redGalaxy64ProcessData);
|
||||
void APS5_VABI redGalaxy64ProcessData(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
}
|
||||
|
||||
APS5_EXPORT("4trn+7BssaY", redGalaxy64GetErrorString);
|
||||
const char* APS5_VABI redGalaxy64GetErrorString(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return "";
|
||||
}
|
||||
|
||||
APS5_EXPORT("DQAUzFVaGl8", redGalaxy64GameServerInit);
|
||||
std::uint64_t APS5_VABI redGalaxy64GameServerInit(const void* options) {
|
||||
(void)options;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 1;
|
||||
}
|
||||
|
||||
APS5_EXPORT("GKomji4WVHw", redGalaxy64ProcessGameServerData);
|
||||
void APS5_VABI redGalaxy64ProcessGameServerData(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
}
|
||||
|
||||
APS5_EXPORT("36lort-kFp4", redgalaxy_stub_00);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_00(std::uint64_t a0, std::uint64_t a1, std::uint64_t a2) {
|
||||
(void)a0;
|
||||
(void)a1;
|
||||
(void)a2;
|
||||
NotImplemented_nid_no_patch("36lort-kFp4");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("NgDLhCc6F7Y", redgalaxy_stub_01);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_01(const void* a0, const char* a1, std::uint64_t a2) {
|
||||
(void)a0;
|
||||
(void)a1;
|
||||
(void)a2;
|
||||
NotImplemented_nid_no_patch("NgDLhCc6F7Y");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("huJ+beHcWBE", redgalaxy_stub_02);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_02(void) {
|
||||
NotImplemented_nid_no_patch("huJ+beHcWBE");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("sxiZFliOEDU", redgalaxy_stub_03);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_03(void) {
|
||||
NotImplemented_nid_no_patch("sxiZFliOEDU");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("49Bdn1OOJ90", redgalaxy_stub_04);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_04(void) {
|
||||
NotImplemented_nid_no_patch("49Bdn1OOJ90");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("8ahh9DUqZLY", redgalaxy_stub_05);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_05(void) {
|
||||
NotImplemented_nid_no_patch("8ahh9DUqZLY");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("Ard4nEfw5Vs", redgalaxy_stub_06);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_06(void) {
|
||||
NotImplemented_nid_no_patch("Ard4nEfw5Vs");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("AzO2fwK+MUs", redgalaxy_stub_07);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_07(void) {
|
||||
NotImplemented_nid_no_patch("AzO2fwK+MUs");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("HKIwQPq45o4", redgalaxy_stub_08);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_08(void) {
|
||||
NotImplemented_nid_no_patch("HKIwQPq45o4");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("IXRuom5imkM", redgalaxy_stub_09);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_09(void) {
|
||||
NotImplemented_nid_no_patch("IXRuom5imkM");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("LjDqfncevos", redgalaxy_stub_10);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_10(void) {
|
||||
NotImplemented_nid_no_patch("LjDqfncevos");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("Onwg0aGsFhM", redgalaxy_stub_11);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_11(void) {
|
||||
NotImplemented_nid_no_patch("Onwg0aGsFhM");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("oSyqfoYcK40", redgalaxy_stub_12);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_12(void) {
|
||||
NotImplemented_nid_no_patch("oSyqfoYcK40");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("qoS0gqW7RxI", redgalaxy_stub_13);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_13(void) {
|
||||
NotImplemented_nid_no_patch("qoS0gqW7RxI");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("SOtPNNwajSw", redgalaxy_stub_14);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_14(void) {
|
||||
NotImplemented_nid_no_patch("SOtPNNwajSw");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("udfpd9vbE+w", redgalaxy_stub_15);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_15(void) {
|
||||
NotImplemented_nid_no_patch("udfpd9vbE+w");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("vuGujcpcUEk", redgalaxy_stub_16);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_16(void) {
|
||||
NotImplemented_nid_no_patch("vuGujcpcUEk");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("VXUTdiaHbD4", redgalaxy_stub_17);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_17(void) {
|
||||
NotImplemented_nid_no_patch("VXUTdiaHbD4");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("w5-trCio4bQ", redgalaxy_stub_18);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_18(void) {
|
||||
NotImplemented_nid_no_patch("w5-trCio4bQ");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("YpZjREtZb20", redgalaxy_stub_19);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_19(void) {
|
||||
NotImplemented_nid_no_patch("YpZjREtZb20");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("gPJN5c9XCec", redgalaxy_stub_20);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_20(void) {
|
||||
NotImplemented_nid_no_patch("gPJN5c9XCec");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("z4SpWc89no8", redgalaxy_stub_21);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_21(void) {
|
||||
NotImplemented_nid_no_patch("z4SpWc89no8");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("zdzopKdBeU4", redgalaxy_stub_22);
|
||||
std::uint64_t APS5_VABI redgalaxy_stub_22(void) {
|
||||
NotImplemented_nid_no_patch("zdzopKdBeU4");
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
#include <cstdint>
|
||||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
|
||||
APS5_EXPORT("0DsDTr534gc", renoirStub00);
|
||||
uint64_t APS5_VABI renoirStub00(void) {
|
||||
NotImplemented_nid_no_patch("0DsDTr534gc");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("2T56HgbPW08", renoirStub01);
|
||||
uint64_t APS5_VABI renoirStub01(void) {
|
||||
NotImplemented_nid_no_patch("2T56HgbPW08");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("3HnCozh8Az8", renoirStub02);
|
||||
uint64_t APS5_VABI renoirStub02(void) {
|
||||
NotImplemented_nid_no_patch("3HnCozh8Az8");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("5Ysce+s-CWU", renoirStub03);
|
||||
uint64_t APS5_VABI renoirStub03(void) {
|
||||
NotImplemented_nid_no_patch("5Ysce+s-CWU");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("AdlSqA4lGS8", renoirStub04);
|
||||
uint64_t APS5_VABI renoirStub04(void) {
|
||||
NotImplemented_nid_no_patch("AdlSqA4lGS8");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("bC37hk7rHqs", renoirStub05);
|
||||
uint64_t APS5_VABI renoirStub05(void) {
|
||||
NotImplemented_nid_no_patch("bC37hk7rHqs");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("dfPK9R6M0qM", renoirStub06);
|
||||
uint64_t APS5_VABI renoirStub06(void) {
|
||||
NotImplemented_nid_no_patch("dfPK9R6M0qM");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("fZiBbVa2ZDU", renoirStub07);
|
||||
uint64_t APS5_VABI renoirStub07(void) {
|
||||
NotImplemented_nid_no_patch("fZiBbVa2ZDU");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("hkWBH8-WfW4", renoirStub08);
|
||||
uint64_t APS5_VABI renoirStub08(void) {
|
||||
NotImplemented_nid_no_patch("hkWBH8-WfW4");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("hUxvGdi64Io", renoirStub09);
|
||||
uint64_t APS5_VABI renoirStub09(void) {
|
||||
NotImplemented_nid_no_patch("hUxvGdi64Io");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("NxqsZxGisrA", renoirStub10);
|
||||
uint64_t APS5_VABI renoirStub10(void) {
|
||||
NotImplemented_nid_no_patch("NxqsZxGisrA");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("OgZVIkaNmb0", renoirStub11);
|
||||
uint64_t APS5_VABI renoirStub11(void) {
|
||||
NotImplemented_nid_no_patch("OgZVIkaNmb0");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("ojLQdRUnVgA", renoirStub12);
|
||||
uint64_t APS5_VABI renoirStub12(void) {
|
||||
NotImplemented_nid_no_patch("ojLQdRUnVgA");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("s0qDfgsAgVg", renoirStub13);
|
||||
uint64_t APS5_VABI renoirStub13(void) {
|
||||
NotImplemented_nid_no_patch("s0qDfgsAgVg");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("sgrd-r1Nw60", renoirStub14);
|
||||
uint64_t APS5_VABI renoirStub14(void) {
|
||||
NotImplemented_nid_no_patch("sgrd-r1Nw60");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("sjTRb8YD7dQ", renoirStub15);
|
||||
uint64_t APS5_VABI renoirStub15(void) {
|
||||
NotImplemented_nid_no_patch("sjTRb8YD7dQ");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("tG86XGc2VtE", renoirStub16);
|
||||
uint64_t APS5_VABI renoirStub16(void) {
|
||||
NotImplemented_nid_no_patch("tG86XGc2VtE");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("umlGxOcNzgc", renoirStub17);
|
||||
uint64_t APS5_VABI renoirStub17(void) {
|
||||
NotImplemented_nid_no_patch("umlGxOcNzgc");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("vPxA9OFW0os", renoirStub18);
|
||||
uint64_t APS5_VABI renoirStub18(void) {
|
||||
NotImplemented_nid_no_patch("vPxA9OFW0os");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("X2qEBlOLkNo", renoirStub19);
|
||||
uint64_t APS5_VABI renoirStub19(void) {
|
||||
NotImplemented_nid_no_patch("X2qEBlOLkNo");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("ynh4E+BIaqY", renoirStub20);
|
||||
uint64_t APS5_VABI renoirStub20(void) {
|
||||
NotImplemented_nid_no_patch("ynh4E+BIaqY");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("YXe6ABbHZPs", renoirStub21);
|
||||
uint64_t APS5_VABI renoirStub21(void) {
|
||||
NotImplemented_nid_no_patch("YXe6ABbHZPs");
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("z0jsXZVz75I", renoirStub22);
|
||||
uint64_t APS5_VABI renoirStub22(void) {
|
||||
NotImplemented_nid_no_patch("z0jsXZVz75I");
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -53,4 +53,14 @@ int APS5_VABI sceAcm_ConvReverb_SharedInput(void) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcm_FFT() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAcm_Panner() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,8 +72,9 @@ configure_windows_unwind(agc_command_tests)
|
|||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_command_tests PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME agc_initialization COMMAND agc_command_tests defaults)
|
||||
add_test(NAME agc_commands COMMAND agc_command_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_initialization PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
set_tests_properties(agc_initialization agc_commands PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
|
@ -124,6 +125,25 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_cb_cond_write_tests EXCLUDE_FROM_ALL
|
||||
tests/CbCondWrite.cpp
|
||||
Command/src/Packet.cpp
|
||||
Command/src/Memory.cpp
|
||||
Command/src/Control.cpp
|
||||
Cb/src/Branch.cpp
|
||||
)
|
||||
target_include_directories(agc_cb_cond_write_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_cb_cond_write_tests PRIVATE libc)
|
||||
configure_windows_unwind(agc_cb_cond_write_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_cb_cond_write_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_cb_cond_write_tests COMMAND agc_cb_cond_write_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_cb_cond_write_tests PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_payload_range_tests EXCLUDE_FROM_ALL tests/PayloadRange.cpp)
|
||||
target_include_directories(agc_payload_range_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_payload_range_tests PRIVATE libSceAgc libc)
|
||||
|
|
@ -137,6 +157,19 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_end_of_shader_action_size_tests EXCLUDE_FROM_ALL tests/EndOfShaderActionSize.cpp)
|
||||
target_include_directories(agc_end_of_shader_action_size_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_end_of_shader_action_size_tests PRIVATE libSceAgc libc)
|
||||
configure_windows_unwind(agc_end_of_shader_action_size_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_end_of_shader_action_size_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_end_of_shader_action_size_tests COMMAND agc_end_of_shader_action_size_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_end_of_shader_action_size_tests PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_branch_patch_tests EXCLUDE_FROM_ALL
|
||||
tests/BranchPatch.cpp
|
||||
Command/src/Packet.cpp
|
||||
|
|
@ -265,6 +298,26 @@ if(BUILD_TESTING)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_write_data_patch_tests EXCLUDE_FROM_ALL
|
||||
tests/WriteDataPatch.cpp
|
||||
Command/src/Packet.cpp
|
||||
Command/src/Memory.cpp
|
||||
Acb/src/Memory.cpp
|
||||
DcbFlow/src/Memory.cpp
|
||||
Patch/src/WaitRegMem.cpp
|
||||
)
|
||||
target_include_directories(agc_write_data_patch_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(agc_write_data_patch_tests PRIVATE libc)
|
||||
configure_windows_unwind(agc_write_data_patch_tests)
|
||||
if(BUILD_TESTING)
|
||||
set_target_properties(agc_write_data_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE)
|
||||
add_test(NAME agc_write_data_patch_tests COMMAND agc_write_data_patch_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(agc_write_data_patch_tests PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_test_executable(agc_prime_utcl2_tests EXCLUDE_FROM_ALL
|
||||
tests/PrimeUtcl2.cpp
|
||||
Command/src/Packet.cpp
|
||||
|
|
@ -291,6 +344,7 @@ add_test_executable(agc_cond_exec_patch_tests EXCLUDE_FROM_ALL
|
|||
Command/src/Memory.cpp
|
||||
Command/src/Control.cpp
|
||||
DcbFlow/src/Sync.cpp
|
||||
Acb/src/Sync.cpp
|
||||
Patch/src/CondExec.cpp
|
||||
)
|
||||
target_include_directories(agc_cond_exec_patch_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ std::uint32_t* APS5_VABI sceAgcCbBranch(CommandBuffer* buf, std::uint8_t mode, s
|
|||
const auto address = reinterpret_cast<std::uintptr_t>(compareAddress);
|
||||
const auto first = reinterpret_cast<std::uintptr_t>(buffer1);
|
||||
const auto second = reinterpret_cast<std::uintptr_t>(buffer2);
|
||||
Agc::Command::CheckAddress(address, 8, __func__);
|
||||
Agc::Command::CheckAddress(first, 4, __func__);
|
||||
Agc::Command::CheckAddress(second, 4, __func__);
|
||||
if (compareFunction != 0) Agc::Command::CheckAddress(address, 8, __func__);
|
||||
if (first != 0 || sizeInDwords1 != 0) Agc::Command::CheckAddress(first, 4, __func__);
|
||||
if (second != 0 || sizeInDwords2 != 0) Agc::Command::CheckAddress(second, 4, __func__);
|
||||
return Agc::Command::Emit(buf, 0x3fu, {mode | (static_cast<std::uint32_t>(compareFunction) << 8u), static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), static_cast<std::uint32_t>(mask), static_cast<std::uint32_t>(mask >> 32u), static_cast<std::uint32_t>(reference), static_cast<std::uint32_t>(reference >> 32u), static_cast<std::uint32_t>(first), static_cast<std::uint32_t>(first >> 32u), sizeInDwords1 | (static_cast<std::uint32_t>(cachePolicy1) << 28u), static_cast<std::uint32_t>(second), static_cast<std::uint32_t>(second >> 32u), sizeInDwords2 | (static_cast<std::uint32_t>(cachePolicy2) << 28u)}, __func__);
|
||||
}
|
||||
|
||||
|
|
@ -28,19 +28,20 @@ std::uint32_t APS5_VABI sceAgcCbBranchGetSize() {
|
|||
return 56;
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcCbCondWrite(CommandBuffer* buf, uint32_t cond, uint32_t a, uint32_t b, uint32_t c) {
|
||||
(void)buf;
|
||||
(void)cond;
|
||||
(void)a;
|
||||
(void)b;
|
||||
(void)c;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return nullptr;
|
||||
std::uint32_t* APS5_VABI sceAgcCbCondWrite(CommandBuffer* buf, std::uint32_t compareFunction, std::uint32_t writeSpace, const volatile void* writeAddress, std::uint32_t writeData, const volatile void* pollAddress, std::uint32_t reference, std::uint32_t mask) {
|
||||
Agc::Command::Require(compareFunction <= 6, __func__, "invalid write comparison");
|
||||
Agc::Command::Require(writeSpace == 1, __func__, "only memory writes are supported");
|
||||
const auto destination = reinterpret_cast<std::uintptr_t>(writeAddress);
|
||||
const auto source = reinterpret_cast<std::uintptr_t>(pollAddress);
|
||||
Agc::Command::CheckAddress(destination, 4, __func__);
|
||||
Agc::Command::CheckAddress(source, 4, __func__);
|
||||
Agc::Command::CheckBits(destination, 0xffffffffffffull, __func__);
|
||||
Agc::Command::CheckBits(source, 0xffffffffffffull, __func__);
|
||||
return Agc::Command::Emit(buf, 0x45u, {0x10u | compareFunction | (writeSpace << 8u), static_cast<std::uint32_t>(source), static_cast<std::uint32_t>(source >> 32u), reference, mask, static_cast<std::uint32_t>(destination), static_cast<std::uint32_t>(destination >> 32u), writeData}, __func__);
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcCbCondWriteGetSize() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return 36;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ std::uint32_t* WriteRegisters(CommandBuffer* buffer, std::uint32_t opcode, const
|
|||
|
||||
std::uint32_t* WriteIndirectRegisters(CommandBuffer* buffer, std::uint32_t opcode, const volatile ShaderRegister* registers, std::uint32_t count, const char* function) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(registers);
|
||||
CheckAddress(address, 4, function);
|
||||
if (address != 0 || count != 0) CheckAddress(address, 4, function);
|
||||
CheckBits(count, 0x3fffu, function);
|
||||
return Emit(buffer, opcode, {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>(address >> 32u), 0x80000000u, count}, function);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,11 @@ std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t o
|
|||
return Agc::Command::Emit(buf, 0x12u, {state}, __func__);
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcDcbClearState(CommandBuffer* buf, std::uint32_t command) {
|
||||
Agc::Command::CheckBits(command, 0xfu, __func__);
|
||||
return Agc::Command::Emit(buf, 0x12u, {command}, __func__);
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbRewind(CommandBuffer* buf, uint32_t initial_state) {
|
||||
return Agc::Command::WriteRewind(buf, initial_state, __func__);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,9 +69,8 @@ std::uint32_t APS5_VABI sceAgcDcbSetBaseDrawIndirectArgsGetSize() {
|
|||
return 16;
|
||||
}
|
||||
|
||||
uint32_t APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
std::uint32_t APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize() {
|
||||
return 32;
|
||||
}
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcDcbGetLodStatsGetSize() {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@
|
|||
|
||||
extern "C" {
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterDirect(CommandBuffer* buf, ShaderRegister reg) {
|
||||
return Agc::Command::WriteRegisterRange(buf, 0x68u, reg.offset, ®.value, 1u, __func__);
|
||||
}
|
||||
|
||||
std::uint32_t* APS5_VABI sceAgcDcbSetCfRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues) {
|
||||
return Agc::Command::WriteRegisterRange(buf, 0x68u, offset, values, numValues, __func__);
|
||||
}
|
||||
|
||||
uint32_t* APS5_VABI sceAgcDcbSetCxRegisterDirect(CommandBuffer* buf, ShaderRegister reg) {
|
||||
return Agc::Command::WriteRegisterRange(buf, 0x69u, reg.offset, ®.value, 1u, __func__);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ void* APS5_VABI sceAgcGetRegisterDefaults() {
|
|||
return Agc::Command::GetRegisterDefaults(0, false, __func__);
|
||||
}
|
||||
|
||||
void* APS5_VABI sceAgcGetRegisterDefaultsInternal() {
|
||||
return Agc::Command::GetRegisterDefaults(0, true, __func__);
|
||||
}
|
||||
|
||||
void* APS5_VABI sceAgcGetRegisterDefaults2(std::uint32_t version) {
|
||||
return Agc::Command::GetRegisterDefaults(version, false, __func__);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,40 +7,46 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
extern "C" {
|
||||
namespace {
|
||||
|
||||
int APS5_VABI sceAgcCondExecPatchSetCommandAddress(uint32_t* cmd, const volatile uint32_t* command) {
|
||||
Agc::Command::ValidatePacket(cmd, 0x22u, 5, __func__);
|
||||
int SetCommandAddress(std::uint32_t* cmd, const volatile std::uint32_t* command, const char* function) {
|
||||
Agc::Command::ValidatePacket(cmd, 0x22u, 5, function);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(command);
|
||||
Agc::Command::CheckAddress(address, 4, __func__);
|
||||
Agc::Command::CheckAddress(address, 4, function);
|
||||
cmd[1] = (cmd[1] & 3u) | (static_cast<std::uint32_t>(address) & ~3u);
|
||||
cmd[2] = static_cast<std::uint32_t>(address >> 32u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer) {
|
||||
Agc::Command::ValidatePacket(cmd, 0x22u, 5, __func__);
|
||||
int SetEnd(std::uint32_t* cmd, const volatile std::uint32_t* buffer, const char* function) {
|
||||
Agc::Command::ValidatePacket(cmd, 0x22u, 5, function);
|
||||
const auto end = reinterpret_cast<std::uintptr_t>(buffer);
|
||||
const auto packetEnd = reinterpret_cast<std::uintptr_t>(cmd + 5);
|
||||
Agc::Command::CheckAddress(end, 4, __func__);
|
||||
Agc::Command::CheckAddress(end, 4, function);
|
||||
const auto numDwords = (end - packetEnd) / sizeof(std::uint32_t);
|
||||
Agc::Command::Require(numDwords <= 0x3fffu, __func__, "conditional execution range is too large");
|
||||
Agc::Command::Require(numDwords <= 0x3fffu, function, "conditional execution range is too large");
|
||||
cmd[4] = (cmd[4] & ~0x3fffu) | static_cast<std::uint32_t>(numDwords);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAgcCondExecPatchSetCommandAddress(uint32_t* cmd, const volatile uint32_t* command) {
|
||||
return SetCommandAddress(cmd, command, __func__);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer) {
|
||||
return SetEnd(cmd, buffer, __func__);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcAsyncCondExecPatchSetCommandAddress(std::uint32_t* cmd, const volatile std::uint32_t* command) {
|
||||
(void)cmd;
|
||||
(void)command;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return SetCommandAddress(cmd, command, __func__);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcAsyncCondExecPatchSetEnd(std::uint32_t* cmd, const volatile std::uint32_t* buffer) {
|
||||
(void)cmd;
|
||||
(void)buffer;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return SetEnd(cmd, buffer, __func__);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,39 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
void ValidateWriteData(const std::uint32_t* cmd, const char* function) {
|
||||
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(cmd), 4, function);
|
||||
Agc::Command::Require(((cmd[0] >> 8u) & 0xffu) == 0x37u, function, "not a WRITE_DATA packet");
|
||||
}
|
||||
|
||||
int SetWriteDataAddress(std::uint32_t* cmd, std::uint64_t address, const char* function) {
|
||||
ValidateWriteData(cmd, function);
|
||||
cmd[2] = static_cast<std::uint32_t>(address);
|
||||
cmd[3] = static_cast<std::uint32_t>(address >> 32u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SetWriteDataDst(std::uint32_t* cmd, std::uint8_t dst, bool compute, const char* function) {
|
||||
ValidateWriteData(cmd, function);
|
||||
Agc::Command::CheckBits(dst, compute ? 0xfu : 0x1fu, function);
|
||||
Agc::Command::Require(dst != 0 || (cmd[1] & (1u << 20u)) == 0, function, "register writes do not support write confirmation");
|
||||
const auto mask = compute ? 0xfu << 8u : (1u << 30u) | (0xfu << 8u);
|
||||
const auto destination = compute ? static_cast<std::uint32_t>(dst) << 8u : ((dst & 1u) << 30u) | ((dst & 0x1eu) << 7u);
|
||||
cmd[1] = (cmd[1] & ~mask) | destination;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SetWriteDataCachePolicy(std::uint32_t* cmd, std::uint8_t cachePolicy, const char* function) {
|
||||
ValidateWriteData(cmd, function);
|
||||
Agc::Command::CheckBits(cachePolicy, 3, function);
|
||||
cmd[1] = (cmd[1] & ~(3u << 25u)) | (static_cast<std::uint32_t>(cachePolicy) << 25u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAgcWaitRegMemPatchAddress(std::uint32_t* cmd, const volatile void* address) {
|
||||
|
|
@ -29,6 +62,13 @@ int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t r
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask) {
|
||||
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
|
||||
Agc::Command::CheckBits(mask, 0xffffffffu, __func__);
|
||||
wait[((wait[0] >> 8u) & 0xffu) == 0x3cu ? 5 : 6] = static_cast<std::uint32_t>(mask);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint8_t compareFunction) {
|
||||
auto* wait = Agc::Command::ValidateWait(cmd, __func__);
|
||||
Agc::Command::Require(compareFunction <= 6, __func__, "invalid wait comparison");
|
||||
|
|
@ -37,11 +77,27 @@ int APS5_VABI sceAgcWaitRegMemPatchCompareFunction(std::uint32_t* cmd, std::uint
|
|||
}
|
||||
|
||||
int APS5_VABI sceAgcWriteDataPatchSetAddressOrOffset(std::uint32_t* cmd, std::uint64_t address) {
|
||||
Agc::Command::CheckAddress(reinterpret_cast<std::uintptr_t>(cmd), 4, __func__);
|
||||
Agc::Command::Require(((cmd[0] >> 8u) & 0xffu) == 0x37u, __func__, "not a WRITE_DATA packet");
|
||||
cmd[2] = static_cast<std::uint32_t>(address);
|
||||
cmd[3] = static_cast<std::uint32_t>(address >> 32u);
|
||||
return 0;
|
||||
return SetWriteDataAddress(cmd, address, __func__);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetAddressOrOffset(std::uint32_t* cmd, std::uint64_t address) {
|
||||
return SetWriteDataAddress(cmd, address, __func__);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcWriteDataPatchSetDst(std::uint32_t* cmd, std::uint8_t dst) {
|
||||
return SetWriteDataDst(cmd, dst, false, __func__);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetDst(std::uint32_t* cmd, std::uint8_t dst) {
|
||||
return SetWriteDataDst(cmd, dst, true, __func__);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcWriteDataPatchSetCachePolicy(std::uint32_t* cmd, std::uint8_t cachePolicy) {
|
||||
return SetWriteDataCachePolicy(cmd, cachePolicy, __func__);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetCachePolicy(std::uint32_t* cmd, std::uint8_t cachePolicy) {
|
||||
return SetWriteDataCachePolicy(cmd, cachePolicy, __func__);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,6 +70,11 @@ int APS5_VABI sceAgcSetAmmSemaphoreMemory() {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcSetSemaphoreMemory() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcCbMemsetExclusive() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
|
|
@ -81,11 +86,6 @@ int APS5_VABI sceAgcUnknown_7Wa3aeJgeVU() {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetDst() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcGetGsPrimPayload() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
|
|
@ -101,32 +101,12 @@ int APS5_VABI sceAgcGetShaderInstrumentation() {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetAddressOrOffset() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcWriteDataPatchSetCachePolicy() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("rP5xLdOf26k", sceAgcUnknown_rP5xLdOf26k);
|
||||
int APS5_VABI sceAgcUnknown_rP5xLdOf26k() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcWriteDataPatchSetDst() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetCachePolicy() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw);
|
||||
int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
|
|
|
|||
|
|
@ -58,6 +58,21 @@ void testWriter() {
|
|||
check(storage.packet[expected.size()] == 0xabcdef01u, "branch command overwrote following word");
|
||||
}
|
||||
|
||||
void testUnusedFields() {
|
||||
std::array<std::uint32_t, 64> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
auto* packet = sceAgcCbBranch(&buffer, 1, 0, compareAt(0x0000001234567894ull), 0, 0, 0, targetAt(0x0000002233445564ull), 0x123u, 0, nullptr, 0);
|
||||
const std::array expected{0xc00c3f00u, 0x1u, 0x34567894u, 0x12u, 0u, 0u, 0u, 0u, 0x33445564u, 0x22u, 0x123u, 0u, 0u, 0u};
|
||||
check(std::equal(expected.begin(), expected.end(), packet), "an always-taken branch without an else target was not written");
|
||||
packet = sceAgcCbBranch(&buffer, 1, 0, nullptr, 0, 0, 0, nullptr, 0, 0, nullptr, 0);
|
||||
check(packet[1] == 1u && packet[2] == 0u && packet[8] == 0u && packet[10] == 0u, "an empty always-taken branch was not written");
|
||||
const auto cursor = buffer.cursor_up;
|
||||
expectFailure([&] { sceAgcCbBranch(&buffer, 1, 3, compareAt(0x0000001234567894ull), 0, 0, 0, targetAt(0x2000u), 1, 0, nullptr, 0); });
|
||||
expectFailure([&] { sceAgcCbBranch(&buffer, 1, 0, nullptr, 0, 0, 0, nullptr, 1, 0, nullptr, 0); });
|
||||
expectFailure([&] { sceAgcCbBranch(&buffer, 2, 0, nullptr, 0, 0, 0, targetAt(0x2000u), 1, 0, nullptr, 1); });
|
||||
check(buffer.cursor_up == cursor, "a rejected branch advanced the command buffer");
|
||||
}
|
||||
|
||||
void testCompareAddress() {
|
||||
Storage storage;
|
||||
auto expected = storage.words;
|
||||
|
|
@ -109,6 +124,7 @@ void testMalformedPackets() {
|
|||
int main() {
|
||||
try {
|
||||
testWriter();
|
||||
testUnusedFields();
|
||||
testCompareAddress();
|
||||
testTarget(sceAgcBranchPatchSetThenTarget, 8, 0x10000000u, "then target setter wrote the wrong words");
|
||||
testTarget(sceAgcBranchPatchSetElseTarget, 11, 0x20000000u, "else target setter wrote the wrong words");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
extern "C" {
|
||||
std::uint32_t* APS5_VABI sceAgcCbCondWrite(CommandBuffer*, std::uint32_t, std::uint32_t, const volatile void*, std::uint32_t, const volatile void*, std::uint32_t, std::uint32_t);
|
||||
std::uint32_t APS5_VABI sceAgcCbCondWriteGetSize();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t Sentinel = 0xabcdef01u;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
template <typename TAction>
|
||||
void expectFailure(TAction action) {
|
||||
try {
|
||||
action();
|
||||
} catch (const std::runtime_error&) {
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("expected invalid input to fail");
|
||||
}
|
||||
|
||||
const volatile void* at(std::uintptr_t address) {
|
||||
return reinterpret_cast<const volatile void*>(address);
|
||||
}
|
||||
|
||||
struct Storage {
|
||||
std::array<std::uint32_t, 16> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
|
||||
Storage() { words.fill(Sentinel); }
|
||||
};
|
||||
|
||||
void testPacket() {
|
||||
for (std::uint32_t compareFunction = 0; compareFunction <= 6; ++compareFunction) {
|
||||
Storage storage;
|
||||
const auto* packet = sceAgcCbCondWrite(&storage.buffer, compareFunction, 1, at(0x0000123456789abcu), 0xdeadbeefu, at(0x0000fedcba987654u), 0x11223344u, 0xff00ff00u);
|
||||
check(packet == storage.words.data() && packet[0] == 0xc0074500u, "COND_WRITE header mismatch");
|
||||
check(packet[1] == (0x110u | compareFunction), "COND_WRITE control mismatch");
|
||||
check(packet[2] == 0xba987654u && packet[3] == 0x0000fedcu, "COND_WRITE poll address mismatch");
|
||||
check(packet[4] == 0x11223344u && packet[5] == 0xff00ff00u, "COND_WRITE reference or mask mismatch");
|
||||
check(packet[6] == 0x56789abcu && packet[7] == 0x00001234u, "COND_WRITE write address mismatch");
|
||||
check(packet[8] == 0xdeadbeefu, "COND_WRITE data mismatch");
|
||||
check(storage.buffer.cursor_up == storage.words.data() + 9 && storage.words[9] == Sentinel, "COND_WRITE cursor advance");
|
||||
}
|
||||
check(sceAgcCbCondWriteGetSize() == 9 * sizeof(std::uint32_t), "COND_WRITE size mismatch");
|
||||
}
|
||||
|
||||
void testInvalid() {
|
||||
Storage storage;
|
||||
const auto before = storage.words;
|
||||
const auto poll = at(0x1000u);
|
||||
const auto write = at(0x2000u);
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 7, 1, write, 0, poll, 0, 0); });
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 0, write, 0, poll, 0, 0); });
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 2, write, 0, poll, 0, 0); });
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, nullptr, 0, poll, 0, 0); });
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, at(0x2002u), 0, poll, 0, 0); });
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, write, 0, nullptr, 0, 0); });
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, write, 0, at(0x1001u), 0, 0); });
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, at(0x0001000000000000u), 0, poll, 0, 0); });
|
||||
expectFailure([&] { sceAgcCbCondWrite(&storage.buffer, 3, 1, write, 0, at(0x0001000000000000u), 0, 0); });
|
||||
check(storage.words == before && storage.buffer.cursor_up == storage.words.data(), "failed COND_WRITE modified the buffer");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
testPacket();
|
||||
testInvalid();
|
||||
std::puts("AGC COND_WRITE tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,14 +11,19 @@
|
|||
#include <string>
|
||||
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbResetQueue(CommandBuffer* buf, std::uint32_t op, std::uint32_t state);
|
||||
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" int APS5_VABI sceAgcSuspendPoint();
|
||||
extern "C" int APS5_VABI sceAgcInit(std::uint32_t version);
|
||||
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults();
|
||||
extern "C" void* APS5_VABI sceAgcGetRegisterDefaultsInternal();
|
||||
extern "C" void* APS5_VABI sceAgcGetRegisterDefaults2Internal(std::uint32_t version);
|
||||
extern "C" int APS5_VABI sceAgcUnknownInitState(std::uint32_t* state, std::uint32_t version);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexAuto(CommandBuffer* buf, std::uint32_t indexCount, std::uint64_t modifier);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirect(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint64_t modifier);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbDrawIndexIndirectMulti(CommandBuffer* buf, std::uint32_t dataOffsetInBytes, std::uint32_t countIndirect, std::uint32_t maxCountOrCount, const volatile void* countAddress, std::uint32_t strideInBytes, std::uint64_t modifier);
|
||||
extern "C" int APS5_VABI sceAgcWaitRegMemPatchReference(std::uint32_t* cmd, std::uint64_t reference);
|
||||
extern "C" int APS5_VABI sceAgcWaitRegMemPatchMask(std::uint32_t* cmd, std::uint64_t mask);
|
||||
extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcCbSetShRegisterRangeDirect(CommandBuffer* buf, std::uint32_t offset, const std::uint32_t* values, std::uint32_t numValues);
|
||||
extern "C" std::uint32_t* APS5_VABI sceAgcDcbContextStateAnotherOp(CommandBuffer* buf, std::uint32_t operation);
|
||||
|
|
@ -98,6 +103,21 @@ void testPackets() {
|
|||
check(exhausted.buffer.cursor_up == exhausted.words.data(), "failed allocation advanced cursor");
|
||||
}
|
||||
|
||||
void testClearState() {
|
||||
Storage storage;
|
||||
storage.words.fill(0xdeadbeefu);
|
||||
for (std::uint32_t command = 0; command <= 0xfu; ++command) {
|
||||
auto* packet = sceAgcDcbClearState(&storage.buffer, command);
|
||||
check(packet == storage.words.data() + command * 2 && packet[0] == 0xc0001200u && packet[1] == command, "incorrect CLEAR_STATE packet");
|
||||
}
|
||||
check(storage.buffer.cursor_up == storage.words.data() + 32 && storage.words[32] == 0xdeadbeefu, "incorrect CLEAR_STATE cursor advance");
|
||||
const auto before = storage.words;
|
||||
expectFailure([&] { sceAgcDcbClearState(&storage.buffer, 0x10u); });
|
||||
expectFailure([&] { sceAgcDcbClearState(&storage.buffer, 0xffffffffu); });
|
||||
expectFailure([] { sceAgcDcbClearState(nullptr, 0); });
|
||||
check(storage.words == before && storage.buffer.cursor_up == storage.words.data() + 32, "invalid CLEAR_STATE modified the buffer");
|
||||
}
|
||||
|
||||
struct ContextGrowth {
|
||||
Storage destination;
|
||||
std::uint32_t* expectedCursor;
|
||||
|
|
@ -331,8 +351,18 @@ void testMemory() {
|
|||
check(packet[8] == 7 && packet[9] == 0x11223344u, "reference patch changed the high word");
|
||||
const auto before = storage.words;
|
||||
expectFailure([&] { sceAgcWaitRegMemPatchReference(packet, 0x100000000ull); });
|
||||
expectFailure([&] { Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0, 32, __func__); });
|
||||
check(storage.words == before, "invalid memory operation modified packet memory");
|
||||
auto* truncated = Agc::Command::WriteWait(&storage.buffer, 0, 3, 0, 0, &value, 0x100000000ull, 0xffffffff00000001ull, 32, __func__);
|
||||
check(truncated[8] == 0u && truncated[9] == 1u, "32-bit wait did not keep the low halves of the reference and mask");
|
||||
sceAgcWaitRegMemPatchMask(packet, 0x0f0f0f0fu);
|
||||
check(packet[10] == 0x0f0f0f0fu && packet[11] == 0xffffffffu && packet[8] == 7, "64-bit mask patch changed the wrong word");
|
||||
sceAgcWaitRegMemPatchMask(truncated, 0xff00u);
|
||||
check(truncated[9] == 0xff00u && truncated[8] == 0u && truncated[10] == 2u, "32-bit mask patch changed the wrong word");
|
||||
const auto beforeMask = storage.words;
|
||||
expectFailure([&] { sceAgcWaitRegMemPatchMask(packet, 0x100000000ull); });
|
||||
expectFailure([&] { sceAgcWaitRegMemPatchMask(truncated, 0x100000000ull); });
|
||||
expectFailure([&] { sceAgcWaitRegMemPatchMask(packet + 4, 1); });
|
||||
check(storage.words == beforeMask, "invalid mask patch modified packet memory");
|
||||
}
|
||||
|
||||
void testDefaults() {
|
||||
|
|
@ -350,6 +380,9 @@ void testDefaults() {
|
|||
check(first != nullptr && first == Agc::Command::GetRegisterDefaults(version, internal, __func__), "unstable register defaults pointer");
|
||||
}
|
||||
}
|
||||
auto* internalDefaults = sceAgcGetRegisterDefaultsInternal();
|
||||
check(internalDefaults != nullptr && internalDefaults == Agc::Command::GetRegisterDefaults(0, true, __func__) && internalDefaults == sceAgcGetRegisterDefaults2Internal(0), "internal register defaults are not the baseline internal table");
|
||||
check(internalDefaults != sceAgcGetRegisterDefaults(), "internal register defaults returned the public table");
|
||||
expectFailure([] { Agc::Command::GetRegisterDefaults(14, false, __func__); });
|
||||
expectFailure([] { Agc::Command::GetRegisterDefaults(0xffffffffu, true, __func__); });
|
||||
}
|
||||
|
|
@ -365,6 +398,7 @@ int main(int argc, char** argv) {
|
|||
return 0;
|
||||
}
|
||||
testPackets();
|
||||
testClearState();
|
||||
testIndexedIndirectDraws();
|
||||
testMarkers();
|
||||
testIndexBuffer();
|
||||
|
|
|
|||
|
|
@ -9,14 +9,26 @@
|
|||
|
||||
extern "C" {
|
||||
std::uint32_t* APS5_VABI sceAgcDcbCondExec(CommandBuffer*, const volatile std::uint32_t*, std::uint32_t);
|
||||
std::uint32_t* APS5_VABI sceAgcAcbCondExec(CommandBuffer*, const volatile std::uint32_t*, std::uint32_t);
|
||||
int APS5_VABI sceAgcCondExecPatchSetCommandAddress(std::uint32_t*, const volatile std::uint32_t*);
|
||||
int APS5_VABI sceAgcCondExecPatchSetEnd(std::uint32_t*, const volatile std::uint32_t*);
|
||||
int APS5_VABI sceAgcAsyncCondExecPatchSetCommandAddress(std::uint32_t*, const volatile std::uint32_t*);
|
||||
int APS5_VABI sceAgcAsyncCondExecPatchSetEnd(std::uint32_t*, const volatile std::uint32_t*);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::uint32_t Sentinel = 0xabcdef01u;
|
||||
|
||||
using CondExecWriter = std::uint32_t* (APS5_VABI *)(CommandBuffer*, const volatile std::uint32_t*, std::uint32_t);
|
||||
using PatchFunction = int (APS5_VABI *)(std::uint32_t*, const volatile std::uint32_t*);
|
||||
|
||||
struct Variant {
|
||||
CondExecWriter write;
|
||||
PatchFunction setEnd;
|
||||
PatchFunction setCommandAddress;
|
||||
};
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
|
@ -41,9 +53,9 @@ struct Storage {
|
|||
std::uint32_t condition = 1;
|
||||
std::uint32_t* packet = nullptr;
|
||||
|
||||
Storage() {
|
||||
explicit Storage(CondExecWriter write) {
|
||||
words.fill(Sentinel);
|
||||
packet = sceAgcDcbCondExec(&buffer, &condition, 3);
|
||||
packet = write(&buffer, &condition, 3);
|
||||
check(packet == words.data() && packet[0] == 0xc0032200u && packet[4] == 3, "unexpected COND_EXEC packet");
|
||||
}
|
||||
};
|
||||
|
|
@ -53,51 +65,57 @@ std::uintptr_t endAfter(const std::uint32_t* packet, std::uintptr_t numDwords) {
|
|||
}
|
||||
|
||||
template <typename TAction>
|
||||
void expectUnchanged(TAction action) {
|
||||
Storage storage;
|
||||
void expectUnchanged(const Variant& variant, TAction action) {
|
||||
Storage storage(variant.write);
|
||||
const auto before = storage.words;
|
||||
expectFailure([&] { action(storage.packet); });
|
||||
check(storage.words == before, "failed patch modified the packet");
|
||||
}
|
||||
|
||||
void testSetEnd() {
|
||||
void testSetEnd(const Variant& variant) {
|
||||
const auto setEnd = variant.setEnd;
|
||||
for (const std::uintptr_t numDwords : {0u, 1u, 7u, 0x3fffu}) {
|
||||
Storage storage;
|
||||
Storage storage(variant.write);
|
||||
const auto before = storage.words;
|
||||
check(sceAgcCondExecPatchSetEnd(storage.packet, at(endAfter(storage.packet, numDwords))) == 0, "SetEnd failed");
|
||||
check(setEnd(storage.packet, at(endAfter(storage.packet, numDwords))) == 0, "SetEnd failed");
|
||||
check(storage.packet[4] == numDwords, "SetEnd wrote the wrong dword count");
|
||||
check(std::equal(before.begin(), before.begin() + 4, storage.words.begin()), "SetEnd modified other packet words");
|
||||
check(std::equal(before.begin() + 5, before.end(), storage.words.begin() + 5), "SetEnd modified following words");
|
||||
}
|
||||
|
||||
expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, at(endAfter(packet, 0x4000u))); });
|
||||
expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, packet + 4); });
|
||||
expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, nullptr); });
|
||||
expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet, at(endAfter(packet, 2) + 2)); });
|
||||
expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetEnd(packet + 1, at(endAfter(packet, 2))); });
|
||||
expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet, at(endAfter(packet, 0x4000u))); });
|
||||
expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet, packet + 4); });
|
||||
expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet, nullptr); });
|
||||
expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet, at(endAfter(packet, 2) + 2)); });
|
||||
expectUnchanged(variant, [=](std::uint32_t* packet) { setEnd(packet + 1, at(endAfter(packet, 2))); });
|
||||
}
|
||||
|
||||
void testSetCommandAddress() {
|
||||
void testSetCommandAddress(const Variant& variant) {
|
||||
const auto setCommandAddress = variant.setCommandAddress;
|
||||
for (const std::uintptr_t address : {std::uintptr_t{0x0000123456789abcu}, std::uintptr_t{4u}, std::uintptr_t{0x0000fffffffffffcu}}) {
|
||||
Storage storage;
|
||||
Storage storage(variant.write);
|
||||
storage.packet[1] |= 3u;
|
||||
const auto before = storage.words;
|
||||
check(sceAgcCondExecPatchSetCommandAddress(storage.packet, at(address)) == 0, "SetCommandAddress failed");
|
||||
check(storage.packet[1] == static_cast<std::uint32_t>(address) && storage.packet[2] == static_cast<std::uint32_t>(address >> 32u), "SetCommandAddress wrote the wrong address");
|
||||
check(setCommandAddress(storage.packet, at(address)) == 0, "SetCommandAddress failed");
|
||||
check(storage.packet[1] == (static_cast<std::uint32_t>(address) | 3u) && storage.packet[2] == static_cast<std::uint32_t>(address >> 32u), "SetCommandAddress wrote the wrong address");
|
||||
check(storage.packet[0] == before[0] && storage.packet[3] == before[3] && storage.packet[4] == before[4], "SetCommandAddress modified other packet words");
|
||||
check(std::equal(before.begin() + 5, before.end(), storage.words.begin() + 5), "SetCommandAddress modified following words");
|
||||
}
|
||||
|
||||
expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet, nullptr); });
|
||||
expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet, at(0x1002u)); });
|
||||
expectUnchanged([](std::uint32_t* packet) { sceAgcCondExecPatchSetCommandAddress(packet + 1, at(0x1000u)); });
|
||||
expectUnchanged(variant, [=](std::uint32_t* packet) { setCommandAddress(packet, nullptr); });
|
||||
expectUnchanged(variant, [=](std::uint32_t* packet) { setCommandAddress(packet, at(0x1002u)); });
|
||||
expectUnchanged(variant, [=](std::uint32_t* packet) { setCommandAddress(packet + 1, at(0x1000u)); });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
testSetEnd();
|
||||
testSetCommandAddress();
|
||||
for (const Variant& variant : {Variant{sceAgcDcbCondExec, sceAgcCondExecPatchSetEnd, sceAgcCondExecPatchSetCommandAddress},
|
||||
Variant{sceAgcAcbCondExec, sceAgcAsyncCondExecPatchSetEnd, sceAgcAsyncCondExecPatchSetCommandAddress}}) {
|
||||
testSetEnd(variant);
|
||||
testSetCommandAddress(variant);
|
||||
}
|
||||
std::puts("AGC COND_EXEC patch tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
#include "prx/libc/include/general/VabiMacros.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
extern "C" {
|
||||
std::uint32_t APS5_VABI sceAgcDcbQueueEndOfShaderActionGetSize();
|
||||
std::uint32_t APS5_VABI sceAgcAcbQueueEndOfShaderActionGetSize();
|
||||
std::uint32_t* APS5_VABI sceAgcCbReleaseMem(CommandBuffer*, std::uint8_t, std::uint16_t, std::uint8_t, std::uint8_t, const volatile Label*, std::uint8_t, std::uint64_t, std::uint16_t, std::uint16_t, std::uint8_t, std::uint32_t);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
std::array<std::uint32_t, 16> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
const auto* packet = sceAgcCbReleaseMem(&buffer, 0x2f, 0, 0, 0, nullptr, 0, 0, 0, 0, 0, 0);
|
||||
const auto releaseBytes = static_cast<std::uint32_t>((buffer.cursor_up - packet) * sizeof(std::uint32_t));
|
||||
check(((packet[0] >> 8u) & 0xffu) == 0x49u && releaseBytes == 32, "end-of-shader RELEASE_MEM size mismatch");
|
||||
check(sceAgcDcbQueueEndOfShaderActionGetSize() == releaseBytes, "DCB end-of-shader action size differs from its RELEASE_MEM");
|
||||
check(sceAgcDcbQueueEndOfShaderActionGetSize() == sceAgcAcbQueueEndOfShaderActionGetSize(), "DCB and ACB end-of-shader action sizes differ");
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("AGC end-of-shader action size tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,8 @@
|
|||
#include <stdexcept>
|
||||
|
||||
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 sceAgcDcbSetCxRegisterDirect(CommandBuffer*, ShaderRegister);
|
||||
std::uint32_t* APS5_VABI sceAgcDcbSetShRegisterDirect(CommandBuffer*, ShaderRegister);
|
||||
std::uint32_t* APS5_VABI sceAgcDcbSetUcRegisterDirect(CommandBuffer*, ShaderRegister);
|
||||
|
|
@ -21,6 +23,9 @@ std::uint32_t APS5_VABI sceAgcDcbSetUcRegistersIndirectGetSize(std::uint32_t);
|
|||
int APS5_VABI sceAgcSetCxRegIndirectPatchSetNumRegisters(std::uint32_t*, std::uint32_t);
|
||||
int APS5_VABI sceAgcSetShRegIndirectPatchSetNumRegisters(std::uint32_t*, std::uint32_t);
|
||||
int APS5_VABI sceAgcSetUcRegIndirectPatchSetNumRegisters(std::uint32_t*, std::uint32_t);
|
||||
int APS5_VABI sceAgcSetCxRegIndirectPatchSetAddress(std::uint32_t*, const volatile ShaderRegister*);
|
||||
int APS5_VABI sceAgcSetShRegIndirectPatchSetAddress(std::uint32_t*, const volatile ShaderRegister*);
|
||||
int APS5_VABI sceAgcSetUcRegIndirectPatchSetAddress(std::uint32_t*, const volatile ShaderRegister*);
|
||||
std::uint32_t* APS5_VABI sceAgcCbSetShRegistersDirect(CommandBuffer*, const volatile ShaderRegister*, std::uint32_t);
|
||||
std::uint32_t* APS5_VABI sceAgcCbSetUcRegistersDirect(CommandBuffer*, const volatile ShaderRegister*, std::uint32_t);
|
||||
std::uint32_t APS5_VABI sceAgcCbSetShRegistersDirectGetSize(std::uint32_t);
|
||||
|
|
@ -69,6 +74,29 @@ void testDirect() {
|
|||
}
|
||||
}
|
||||
|
||||
void testConfig() {
|
||||
Storage storage;
|
||||
storage.words.fill(0xabcdef01u);
|
||||
auto* direct = sceAgcDcbSetCfRegisterDirect(&storage.buffer, {0x2468u, 0x12345678u});
|
||||
const std::array expectedDirect{0xc0016800u, 0x2468u, 0x12345678u};
|
||||
check(direct == storage.words.data() && std::equal(expectedDirect.begin(), expectedDirect.end(), direct), "incorrect config register packet");
|
||||
const std::array<std::uint32_t, 3> values{1, 2, 3};
|
||||
auto* range = sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x100u, values.data(), values.size());
|
||||
const std::array expectedRange{0xc0036800u, 0x100u, 1u, 2u, 3u};
|
||||
check(range == direct + 3 && std::equal(expectedRange.begin(), expectedRange.end(), range), "incorrect config register range packet");
|
||||
auto* reserved = sceAgcDcbSetCfRegisterRangeDirect(&storage.buffer, 0x200u, nullptr, 2);
|
||||
check(reserved == range + 5 && reserved[0] == 0xc0026800u && reserved[1] == 0x200u && reserved[2] == 0xabcdef01u && reserved[3] == 0xabcdef01u, "null config values wrote the payload");
|
||||
check(storage.buffer.cursor_up == reserved + 4 && reserved[4] == 0xabcdef01u, "config register cursor mismatch");
|
||||
const auto before = storage.words;
|
||||
expectFailure([&] { sceAgcDcbSetCfRegisterDirect(&storage.buffer, {0x10000u, 1}); });
|
||||
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 testIndirect() {
|
||||
const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect};
|
||||
const std::array sizes{sceAgcDcbSetCxRegistersIndirectGetSize, sceAgcDcbSetShRegistersIndirectGetSize, sceAgcDcbSetUcRegistersIndirectGetSize};
|
||||
|
|
@ -105,6 +133,26 @@ void testIndirect() {
|
|||
}
|
||||
}
|
||||
|
||||
void testIndirectPlaceholder() {
|
||||
const std::array writers{sceAgcDcbSetCxRegistersIndirect, sceAgcDcbSetShRegistersIndirect, sceAgcDcbSetUcRegistersIndirect};
|
||||
const std::array addressSetters{sceAgcSetCxRegIndirectPatchSetAddress, sceAgcSetShRegIndirectPatchSetAddress, sceAgcSetUcRegIndirectPatchSetAddress};
|
||||
const std::array countSetters{sceAgcSetCxRegIndirectPatchSetNumRegisters, sceAgcSetShRegIndirectPatchSetNumRegisters, sceAgcSetUcRegIndirectPatchSetNumRegisters};
|
||||
const std::array headers{0xc0039f00u, 0xc0036300u, 0xc0036400u};
|
||||
const std::array<ShaderRegister, 2> registers{{{0x10u, 7}, {0x11u, 8}}};
|
||||
for (std::size_t i = 0; i < writers.size(); ++i) {
|
||||
Storage storage;
|
||||
auto* packet = writers[i](&storage.buffer, nullptr, 0);
|
||||
check(packet[0] == headers[i] && packet[1] == 0 && packet[2] == 0 && packet[3] == 0x80000000u && packet[4] == 0, "placeholder packet mismatch");
|
||||
check(storage.buffer.cursor_up == packet + 5, "placeholder cursor mismatch");
|
||||
const auto before = storage.words;
|
||||
expectFailure([&] { writers[i](&storage.buffer, nullptr, 1); });
|
||||
check(storage.words == before && storage.buffer.cursor_up == packet + 5, "null register list with a count modified the buffer");
|
||||
check(addressSetters[i](packet, registers.data()) == 0 && countSetters[i](packet, 2) == 0, "placeholder was not patched");
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(registers.data());
|
||||
check(packet[1] == static_cast<std::uint32_t>(address) && packet[2] == static_cast<std::uint32_t>(address >> 32u) && packet[4] == 2, "patched placeholder mismatch");
|
||||
}
|
||||
}
|
||||
|
||||
void testDirectList() {
|
||||
constexpr std::uint32_t sentinel = 0xabcdef01u;
|
||||
constexpr std::uint32_t wordSize = sizeof(std::uint32_t);
|
||||
|
|
@ -157,7 +205,9 @@ void testDirectList() {
|
|||
int main() {
|
||||
try {
|
||||
testDirect();
|
||||
testConfig();
|
||||
testIndirect();
|
||||
testIndirectPlaceholder();
|
||||
testDirectList();
|
||||
std::puts("AGC register command tests passed");
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
#include "prx/libSceAgc/Command/include/Packet.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
|
||||
extern "C" {
|
||||
std::uint32_t* APS5_VABI sceAgcDcbWriteData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t);
|
||||
std::uint32_t* APS5_VABI sceAgcAcbWriteData(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t);
|
||||
int APS5_VABI sceAgcWriteDataPatchSetAddressOrOffset(std::uint32_t*, std::uint64_t);
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetAddressOrOffset(std::uint32_t*, std::uint64_t);
|
||||
int APS5_VABI sceAgcWriteDataPatchSetDst(std::uint32_t*, std::uint8_t);
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetDst(std::uint32_t*, std::uint8_t);
|
||||
int APS5_VABI sceAgcWriteDataPatchSetCachePolicy(std::uint32_t*, std::uint8_t);
|
||||
int APS5_VABI sceAgcAsyncWriteDataPatchSetCachePolicy(std::uint32_t*, std::uint8_t);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::uint32_t, 3> Payload{0x11111111u, 0x22222222u, 0x33333333u};
|
||||
constexpr std::uint64_t Address = 0x0000123456789ab0ull;
|
||||
constexpr std::uint64_t OtherAddress = 0x0000fedcba987650ull;
|
||||
|
||||
void check(bool condition, const char* message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
template <typename TAction>
|
||||
void expectFailure(TAction action) {
|
||||
try {
|
||||
action();
|
||||
} catch (const std::runtime_error&) {
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error("expected invalid input to fail");
|
||||
}
|
||||
|
||||
struct Packet {
|
||||
std::array<std::uint32_t, 16> words{};
|
||||
CommandBuffer buffer{words.data(), words.data() + words.size(), words.data(), words.data() + words.size(), nullptr, nullptr, 0};
|
||||
};
|
||||
|
||||
struct Fields {
|
||||
std::uint8_t dst;
|
||||
std::uint8_t cachePolicy;
|
||||
std::uint64_t address;
|
||||
std::uint8_t writeConfirm;
|
||||
};
|
||||
|
||||
using Writer = std::uint32_t* (APS5_VABI *)(CommandBuffer*, std::uint8_t, std::uint8_t, std::uint64_t, const void*, std::uint32_t, std::uint8_t, std::uint8_t);
|
||||
using AddressPatch = int (APS5_VABI *)(std::uint32_t*, std::uint64_t);
|
||||
using FieldPatch = int (APS5_VABI *)(std::uint32_t*, std::uint8_t);
|
||||
|
||||
struct Variant {
|
||||
Writer writer;
|
||||
AddressPatch setAddress;
|
||||
FieldPatch setDst;
|
||||
FieldPatch setCachePolicy;
|
||||
std::uint8_t maxDst;
|
||||
};
|
||||
|
||||
std::uint32_t* build(Packet& packet, const Variant& variant, const Fields& fields) {
|
||||
return variant.writer(&packet.buffer, fields.dst, fields.cachePolicy, fields.address, Payload.data(), static_cast<std::uint32_t>(Payload.size()), 1, fields.writeConfirm);
|
||||
}
|
||||
|
||||
void expectPatched(const Variant& variant, const Fields& from, const Fields& to, const char* message) {
|
||||
Packet patched;
|
||||
auto* packet = build(patched, variant, from);
|
||||
if (from.address != to.address) check(variant.setAddress(packet, to.address) == 0, "address patch failed");
|
||||
if (from.dst != to.dst) check(variant.setDst(packet, to.dst) == 0, "destination patch failed");
|
||||
if (from.cachePolicy != to.cachePolicy) check(variant.setCachePolicy(packet, to.cachePolicy) == 0, "cache policy patch failed");
|
||||
Packet expected;
|
||||
build(expected, variant, to);
|
||||
check(patched.words == expected.words, message);
|
||||
}
|
||||
|
||||
void testVariant(const Variant& variant) {
|
||||
for (std::uint8_t from = 1; from <= variant.maxDst; ++from) {
|
||||
for (std::uint8_t to = 1; to <= variant.maxDst; ++to) {
|
||||
expectPatched(variant, {from, 1, Address, 1}, {to, 1, Address, 1}, "destination patch does not match a packet built with that destination");
|
||||
}
|
||||
}
|
||||
expectPatched(variant, {5, 1, Address, 0}, {0, 1, Address, 0}, "register destination patch does not match a packet built with it");
|
||||
for (std::uint8_t from = 0; from < 4; ++from) {
|
||||
for (std::uint8_t to = 0; to < 4; ++to) {
|
||||
expectPatched(variant, {5, from, Address, 1}, {5, to, Address, 1}, "cache policy patch does not match a packet built with that policy");
|
||||
}
|
||||
}
|
||||
expectPatched(variant, {5, 2, Address, 1}, {5, 2, OtherAddress, 1}, "address patch does not match a packet built with that address");
|
||||
expectPatched(variant, {2, 0, Address, 1}, {variant.maxDst, 3, OtherAddress, 1}, "combined patches do not match a packet built with those fields");
|
||||
|
||||
Packet packet;
|
||||
auto* written = build(packet, variant, {5, 1, Address, 1});
|
||||
const auto before = packet.words;
|
||||
expectFailure([&] { variant.setDst(written, static_cast<std::uint8_t>(variant.maxDst + 1u)); });
|
||||
expectFailure([&] { variant.setDst(written, 0); });
|
||||
expectFailure([&] { variant.setCachePolicy(written, 4); });
|
||||
check(packet.words == before, "rejected patch modified the packet");
|
||||
|
||||
std::array<std::uint32_t, 8> nop{0xc0021000u, 0u, 0u, 0u, 0u, 0u, 0u, 0u};
|
||||
const auto nopBefore = nop;
|
||||
expectFailure([&] { variant.setAddress(nop.data(), Address); });
|
||||
expectFailure([&] { variant.setDst(nop.data(), 5); });
|
||||
expectFailure([&] { variant.setCachePolicy(nop.data(), 1); });
|
||||
check(nop == nopBefore, "patch of another packet modified it");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
testVariant({sceAgcDcbWriteData, sceAgcWriteDataPatchSetAddressOrOffset, sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy, 0x1f});
|
||||
testVariant({sceAgcAcbWriteData, sceAgcAsyncWriteDataPatchSetAddressOrOffset, sceAgcAsyncWriteDataPatchSetDst, sceAgcAsyncWriteDataPatchSetCachePolicy, 0xf});
|
||||
std::puts("AGC write data patch tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -240,22 +240,6 @@ target_link_libraries(agc_driver_tests PRIVATE
|
|||
set_target_properties(agc_driver_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_tests)
|
||||
|
||||
add_test_executable(agc_driver_recompiler_tests EXCLUDE_FROM_ALL
|
||||
tests/Recompiler.cpp
|
||||
)
|
||||
|
||||
target_include_directories(agc_driver_recompiler_tests PRIVATE
|
||||
${LIBS_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
target_link_libraries(agc_driver_recompiler_tests PRIVATE
|
||||
libSceAgcDriver
|
||||
libc
|
||||
)
|
||||
|
||||
set_target_properties(agc_driver_recompiler_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_driver_recompiler_tests)
|
||||
|
||||
add_test_executable(agc_shader_memory_tests EXCLUDE_FROM_ALL
|
||||
tests/ShaderMemory.cpp
|
||||
Execution/src/ShaderMemory.cpp
|
||||
|
|
@ -289,6 +273,11 @@ target_link_libraries(agc_structurizer_tests PRIVATE shader_recompiler_agc_drive
|
|||
set_target_properties(agc_structurizer_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_structurizer_tests)
|
||||
|
||||
add_test_executable(agc_spirv_module_tests EXCLUDE_FROM_ALL tests/SpirvModule.cpp)
|
||||
target_link_libraries(agc_spirv_module_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_spirv_module_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
configure_windows_unwind(agc_spirv_module_tests)
|
||||
|
||||
add_test_executable(agc_dpp_write_tests EXCLUDE_FROM_ALL tests/DppWrite.cpp)
|
||||
target_link_libraries(agc_dpp_write_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
set_target_properties(agc_dpp_write_tests PROPERTIES CXX_EXTENSIONS OFF)
|
||||
|
|
@ -532,7 +521,7 @@ foreach(agcExecutionTestSource IN LISTS agcExecutionTestSources)
|
|||
endforeach()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests ${agcExecutionTests})
|
||||
foreach(agcTest IN ITEMS agc_driver_tests agc_shader_memory_tests agc_structurizer_tests agc_spirv_module_tests agc_dpp_write_tests agc_masked_select_tests agc_vop1_control_decode_tests agc_validate_program_tests agc_shader_disk_cache_tests agc_shader_control_flow_tests agc_driver_flip_tests agc_driver_pm4_tests agc_driver_graphics_tests ${agcExecutionTests})
|
||||
string(REGEX REPLACE "_tests$" "" agcTestName "${agcTest}")
|
||||
set_target_properties(${agcTest} PROPERTIES EXCLUDE_FROM_ALL OFF)
|
||||
add_test(NAME ${agcTestName} COMMAND ${agcTest})
|
||||
|
|
@ -804,6 +793,14 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(image_sample_gather PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test_executable(pixel_ancillary_tests ${LIBS_INCLUDE_DIR}/tests/PixelAncillary.cpp)
|
||||
target_link_libraries(pixel_ancillary_tests PRIVATE shader_recompiler_agc_driver libc)
|
||||
configure_windows_unwind(pixel_ancillary_tests)
|
||||
add_test(NAME pixel_ancillary COMMAND pixel_ancillary_tests)
|
||||
if(WIN32)
|
||||
set_tests_properties(pixel_ancillary PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
|
||||
endif()
|
||||
add_test_executable(guest_indexed_draw_tests ${LIBS_INCLUDE_DIR}/tests/GuestIndexedDraw.cpp)
|
||||
target_include_directories(guest_indexed_draw_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_indexed_draw_tests PRIVATE libSceAgc libSceAgcDriver libc)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ void Submit(const Packet* packet, std::uint32_t queue);
|
|||
}
|
||||
|
||||
extern "C" void AgcDriverWaitIdle_nid_postfix();
|
||||
extern "C" void AgcDriverLockVulkanLoader_nid_postfix();
|
||||
extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix();
|
||||
extern "C" void AgcDriverShutdown_nid_postfix();
|
||||
extern "C" void AgcDriverSuspendPoint_nid_postfix();
|
||||
extern "C" void AgcDriverRegisterShader_nid_postfix(const Shader* shader);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ void require(bool condition, const char* reason);
|
|||
|
||||
std::uint32_t readRegister(const Registers& registers, std::uint32_t offset);
|
||||
|
||||
std::uint32_t readUserData(const Registers& shader, std::uint32_t offset);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ inline constexpr Opcode Opcodes[] = {
|
|||
{0x41, "CP_DMA"},
|
||||
{0x42, "PFP_SYNC_ME"},
|
||||
{0x43, "SURFACE_SYNC"},
|
||||
{0x45, "COND_WRITE"},
|
||||
{0x46, "EVENT_WRITE"},
|
||||
{0x47, "EVENT_WRITE_EOP"},
|
||||
{0x48, "EVENT_WRITE_EOS"},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ public:
|
|||
~VulkanDevice();
|
||||
VulkanDevice(const VulkanDevice&) = delete;
|
||||
VulkanDevice& operator=(const VulkanDevice&) = delete;
|
||||
std::string DeviceName() const;
|
||||
ShaderRecompiler::SpirvTarget Target() const;
|
||||
ShaderRecompiler::SpirvTarget ComputeTarget(std::uint32_t waveSize) const;
|
||||
// Distinguishes this device from every earlier one in the process (a memo keyed by the device
|
||||
|
|
@ -158,6 +160,7 @@ public:
|
|||
void Resize(std::uint32_t width, std::uint32_t height);
|
||||
bool Presentable() const;
|
||||
bool PrimitiveListRestart() const;
|
||||
bool SamplerFilterMinmax() const;
|
||||
// A presentation is a few steps so the presenter holds GuestMemory::GpuMutex only while it
|
||||
// touches the queue. Presentations are slots (FlipInFlight() + 1, each with its own command
|
||||
// buffer, fence, kept resident image and dump buffer): RetirePresents(keep) (no mutex) retires
|
||||
|
|
|
|||
|
|
@ -21,4 +21,9 @@ std::uint32_t readRegister(const Registers& registers, std::uint32_t offset) {
|
|||
return it->second;
|
||||
}
|
||||
|
||||
std::uint32_t readUserData(const Registers& shader, std::uint32_t offset) {
|
||||
const auto it = shader.find(offset);
|
||||
return it == shader.end() ? 0u : it->second;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ void Driver::dispatch(QueueState& queue, std::span<const std::uint32_t> packet,
|
|||
const auto userCount = (readRegister(queue.shader, 0x213) >> 1u) & 0x1fu;
|
||||
std::vector<std::uint32_t> userData;
|
||||
for (std::uint32_t i = 0; i < userCount; ++i) {
|
||||
userData.push_back(readRegister(queue.shader, 0x240 + i));
|
||||
userData.push_back(readUserData(queue.shader, 0x240 + i));
|
||||
}
|
||||
auto compute = Graphics::DecodeComputeStageInfo(queue.shader);
|
||||
std::vector<ShaderRecompiler::MemoryRegion> memory{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}};
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ std::shared_ptr<DrawDecode> Driver::decodeDraw(const QueueState& queue, const Su
|
|||
};
|
||||
for (std::uint32_t i = 0; i < userCount; ++i) {
|
||||
Graphics::NoteRegisterRead(Graphics::RegisterBank::Shader, userDataBase + i);
|
||||
result.userData.push_back(readRegister(queue.shader, userDataBase + i));
|
||||
result.userData.push_back(readUserData(queue.shader, userDataBase + i));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp"
|
||||
#include <mutex>
|
||||
|
||||
namespace AgcDriver {
|
||||
|
||||
|
|
@ -56,6 +57,19 @@ extern "C" void AgcDriverWaitIdle_nid_postfix() try {
|
|||
LibcAwaitExit_nid_postfix();
|
||||
}
|
||||
|
||||
static std::mutex& VulkanLoaderMutex() {
|
||||
static std::mutex mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverLockVulkanLoader_nid_postfix() {
|
||||
VulkanLoaderMutex().lock();
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverUnlockVulkanLoader_nid_postfix() {
|
||||
VulkanLoaderMutex().unlock();
|
||||
}
|
||||
|
||||
extern "C" void AgcDriverShutdown_nid_postfix() {
|
||||
AgcDriver::Shutdown();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@ bool Driver::preparePacketMemory(const Submission& submission, QueueState& queue
|
|||
|
||||
static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr;
|
||||
const bool drains = drainAll ? ((Pm4::AccessesMemory(header) && opcode != 0x16) || opcode == 0x42 || opcode == 0x46 || opcode == 0x58 || header == FlipPacketHeader)
|
||||
: (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip)));
|
||||
: (!wroteOnGpu && !orderedAlready && (opcode == 0x49 || opcode == 0x37 || opcode == 0x40 || opcode == 0x45 || opcode == 0x50 || opcode == 0x83 || sampleDump || (drawPacket && drawDrain) || (header == FlipPacketHeader && syncFlip)));
|
||||
if (drains) {
|
||||
|
||||
static const bool unlockedDrain = std::getenv("APS5_NO_UNLOCKED_DRAIN") == nullptr && !drainAll;
|
||||
|
|
|
|||
|
|
@ -43,6 +43,23 @@ bool Driver::copySegment(Submission& submission, const std::uint32_t* guest, std
|
|||
}
|
||||
reach(cursor, cursor + count);
|
||||
const auto opcode = (header >> 8u) & 0xffu;
|
||||
if (opcode == 0x3fu && count == 14) {
|
||||
const auto mode = guest[cursor + 1] & 3u;
|
||||
require(mode == 1u || mode == 2u, "invalid COND_INDIRECT_BUFFER mode");
|
||||
require(((guest[cursor + 1] >> 8u) & 7u) == 0u, "a COND_INDIRECT_BUFFER with a comparison is not implemented");
|
||||
require(!Pm4::Predicated(header), "a predicated COND_INDIRECT_BUFFER is not implemented");
|
||||
const auto* target = reinterpret_cast<const std::uint32_t*>(static_cast<std::uintptr_t>(guest[cursor + 8] & ~3u) | (static_cast<std::uintptr_t>(guest[cursor + 9] & 0xffffu) << 32u));
|
||||
const std::size_t targetWords = guest[cursor + 10] & 0xfffffu;
|
||||
if (targetWords != 0) {
|
||||
GuestMemory::CheckRange(target, targetWords * sizeof(std::uint32_t), alignof(std::uint32_t));
|
||||
if (copySegment(submission, target, targetWords, budget)) {
|
||||
require(guarded.empty(), "a REWIND inside a conditional execution range is not implemented");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
cursor += count;
|
||||
continue;
|
||||
}
|
||||
if (opcode == 0x3fu) {
|
||||
require(count == 4, "invalid INDIRECT_BUFFER size");
|
||||
const auto* target = reinterpret_cast<const std::uint32_t*>(static_cast<std::uintptr_t>(guest[cursor + 1] & ~3u) | (static_cast<std::uintptr_t>(guest[cursor + 2] & 0xffffu) << 32u));
|
||||
|
|
|
|||
|
|
@ -163,25 +163,34 @@ std::uint64_t NullPixelProgramAddress() {
|
|||
|
||||
void Driver::RegisterShader(const Shader* shader) {
|
||||
CheckFailure();
|
||||
GuestMemory::CheckRange(shader, sizeof(Shader), alignof(Shader));
|
||||
require(shader->file_header == 0x34333231u && shader->version == 0x18u, "invalid shader header");
|
||||
require(shader->header_size >= sizeof(Shader), "shader header is smaller than its fixed fields");
|
||||
require(shader->shader_size != 0 && (shader->shader_size & 3u) == 0, "invalid shader size");
|
||||
GuestMemory::CheckRange(shader, shader->header_size, alignof(Shader));
|
||||
const auto* code = const_cast<const void*>(shader->code);
|
||||
GuestMemory::CheckRange(code, shader->shader_size, 256);
|
||||
ShaderSnapshot snapshot{reinterpret_cast<std::uintptr_t>(code), reinterpret_cast<std::uintptr_t>(shader), shader->type, {}, {}};
|
||||
snapshot.code.resize(shader->shader_size / sizeof(std::uint32_t));
|
||||
std::memcpy(snapshot.code.data(), code, shader->shader_size);
|
||||
snapshot.header.resize(shader->header_size);
|
||||
std::memcpy(snapshot.header.data(), shader, shader->header_size);
|
||||
GuestMemory::CheckRange(shader, sizeof(Shader), 1);
|
||||
Shader fields;
|
||||
std::memcpy(&fields, static_cast<const void*>(shader), sizeof(Shader));
|
||||
require(fields.file_header == 0x34333231u && fields.version == 0x18u, "invalid shader header");
|
||||
require(fields.header_size >= sizeof(Shader), "shader header is smaller than its fixed fields");
|
||||
require(fields.shader_size != 0 && (fields.shader_size & 3u) == 0, "invalid shader size");
|
||||
GuestMemory::CheckRange(shader, fields.header_size, 1);
|
||||
const auto* code = const_cast<const void*>(fields.code);
|
||||
GuestMemory::CheckRange(code, fields.shader_size, 256);
|
||||
ShaderSnapshot snapshot{reinterpret_cast<std::uintptr_t>(code), reinterpret_cast<std::uintptr_t>(shader), fields.type, {}, {}};
|
||||
snapshot.code.resize(fields.shader_size / sizeof(std::uint32_t));
|
||||
std::memcpy(snapshot.code.data(), code, fields.shader_size);
|
||||
snapshot.header.resize(fields.header_size);
|
||||
std::memcpy(snapshot.header.data(), static_cast<const void*>(shader), fields.header_size);
|
||||
|
||||
static const char* traceRegs = std::getenv("APS5_TRACE_SHADER_REGS");
|
||||
if (traceRegs != nullptr && (std::string(traceRegs) == "all" || std::strtoull(traceRegs, nullptr, 16) == snapshot.codeAddress)) {
|
||||
std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast<unsigned long long>(snapshot.codeAddress), shader->type);
|
||||
for (std::uint32_t i = 0; i < shader->num_cx_registers && shader->cx_registers != nullptr; ++i) std::fprintf(stderr, " %x=%08x", shader->cx_registers[i].offset, shader->cx_registers[i].value);
|
||||
const auto print = [](const ShaderRegister* registers, std::uint32_t count) {
|
||||
for (std::uint32_t i = 0; i < count && registers != nullptr; ++i) {
|
||||
ShaderRegister value;
|
||||
std::memcpy(&value, static_cast<const void*>(registers + i), sizeof(value));
|
||||
std::fprintf(stderr, " %x=%08x", value.offset, value.value);
|
||||
}
|
||||
};
|
||||
std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast<unsigned long long>(snapshot.codeAddress), fields.type);
|
||||
print(fields.cx_registers, fields.num_cx_registers);
|
||||
std::fprintf(stderr, " sh");
|
||||
for (std::uint32_t i = 0; i < shader->num_sh_registers && shader->sh_registers != nullptr; ++i) std::fprintf(stderr, " %x=%08x", shader->sh_registers[i].offset, shader->sh_registers[i].value);
|
||||
print(fields.sh_registers, fields.num_sh_registers);
|
||||
std::fprintf(stderr, "\n");
|
||||
}
|
||||
std::lock_guard lock(mutex);
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ std::string_view UnsupportedReason(std::uint32_t header) {
|
|||
}
|
||||
switch (opcode) {
|
||||
case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x20: case 0x22: case 0x26: case 0x27:
|
||||
case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x46: case 0x50:
|
||||
case 0x2a: case 0x2d: case 0x2f: case 0x35: case 0x37: case 0x40: case 0x42: case 0x45: case 0x46: case 0x50:
|
||||
case 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a:
|
||||
case 0x81: case 0x83: case 0x9f: return {};
|
||||
case 0x24: case 0x25: case 0x2c: case 0x38:
|
||||
|
|
@ -382,6 +382,15 @@ void Validate(std::span<const std::uint32_t> packet, std::uint32_t queue) {
|
|||
require((packet[2] & (opcode == 0x3c ? 3u : 7u)) == 0, "misaligned WAIT_REG_MEM address");
|
||||
break;
|
||||
}
|
||||
case 0x45: {
|
||||
size(9);
|
||||
require((packet[1] & ~0x317u) == 0, "COND_WRITE reserved fields are not implemented");
|
||||
require((packet[1] & 0x10u) != 0, "register-space COND_WRITE poll is not implemented");
|
||||
require((packet[1] & 7u) <= 6u, "invalid COND_WRITE compare function");
|
||||
require(((packet[1] >> 8u) & 3u) == 1u, "register or scratch COND_WRITE destination is not implemented");
|
||||
require((packet[2] & 3u) == 0 && (packet[6] & 3u) == 0, "misaligned COND_WRITE address");
|
||||
break;
|
||||
}
|
||||
case 0x49: {
|
||||
size(8);
|
||||
const auto dataSelect = packet[2] >> 29u;
|
||||
|
|
@ -579,7 +588,7 @@ bool UsesGpuCacheBarrier(std::span<const std::uint32_t> packet) {
|
|||
|
||||
bool AccessesMemory(std::uint32_t header) {
|
||||
switch ((header >> 8u) & 0xffu) {
|
||||
case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true;
|
||||
case 0x22: case 0x27: case 0x3c: case 0x93: case 0x49: case 0x16: case 0x2d: case 0x35: case 0x24: case 0x25: case 0x2c: case 0x38: case 0x37: case 0x40: case 0x45: case 0x50: case 0x63: case 0x64: case 0x83: case 0x9f: return true;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -808,6 +817,15 @@ void Execute(std::span<const std::uint32_t> packet, QueueState& queue) {
|
|||
case 0x83:
|
||||
GuestMemory::Write(address(packet[3], packet[4]), std::as_bytes(std::span(queue.constantRam).subspan(packet[1] / 4, packet[2])), 4);
|
||||
return;
|
||||
case 0x45: {
|
||||
std::uint32_t value = 0;
|
||||
{
|
||||
const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait);
|
||||
GuestMemory::Read(address(packet[2], packet[3]), std::as_writable_bytes(std::span(&value, 1)), 4);
|
||||
}
|
||||
if (waitCompares(packet, false, value)) GuestMemory::Write(address(packet[6], packet[7]), std::as_bytes(packet.subspan(8, 1)), 4);
|
||||
return;
|
||||
}
|
||||
case 0x37: {
|
||||
const auto destination = address(packet[2], packet[3]);
|
||||
if ((packet[1] & 0x10000u) != 0) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/CaptureTrace.hpp"
|
||||
#include "BdaAbi.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp"
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
#ifdef _WIN32
|
||||
|
|
@ -28,6 +30,7 @@
|
|||
#include <charconv>
|
||||
#include <condition_variable>
|
||||
#include <fstream>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
#include <SDL_loadso.h>
|
||||
|
|
@ -42,6 +45,7 @@
|
|||
#include <list>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
|
@ -190,11 +194,13 @@ struct VulkanDevice::State {
|
|||
// 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;
|
||||
bool imageInt64Atomics = false;
|
||||
bool primitiveListRestart = false;
|
||||
bool depthClipControl = false;
|
||||
bool imageViewMinLod = false;
|
||||
bool pipelineExecutableInfo = false;
|
||||
bool maintenance8 = false;
|
||||
std::uint32_t srgbDecodeFormats = 0;
|
||||
bool depthClamp = false;
|
||||
bool depthBounds = false;
|
||||
bool depthBiasClamp = false;
|
||||
|
|
@ -203,6 +209,7 @@ struct VulkanDevice::State {
|
|||
bool depthRangeUnrestricted = false;
|
||||
bool samplerAnisotropy = false;
|
||||
bool textureCompressionBC = false;
|
||||
bool samplerFilterMinmax = false;
|
||||
// VK_KHR_timeline_semaphore enabled: the recorder's unlocked waits are available.
|
||||
bool timelineSemaphores = false;
|
||||
bool computeWave32 = false;
|
||||
|
|
@ -510,8 +517,11 @@ struct VulkanDevice::State {
|
|||
}
|
||||
// Every ShaderResources (kept by the recorder or the resource cache) is gone now, so the
|
||||
// sets and samplers they borrowed can go.
|
||||
copiedWriters->clear();
|
||||
resourceCache.Clear();
|
||||
Graphics::ClearCachedTextures(device);
|
||||
Graphics::ClearImageMirrors(device);
|
||||
patternBuffers.clear();
|
||||
descriptorCache.reset();
|
||||
emptyBuffer.reset();
|
||||
samplerCache.reset();
|
||||
|
|
@ -570,6 +580,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
if (state->instanceProc == nullptr) {
|
||||
throw std::runtime_error("Vulkan loader: vkGetInstanceProcAddr missing");
|
||||
}
|
||||
AgcDriverLockVulkanLoader_nid_postfix();
|
||||
struct LoaderUnlock {
|
||||
~LoaderUnlock() { AgcDriverUnlockVulkanLoader_nid_postfix(); }
|
||||
} loaderUnlock;
|
||||
VkApplicationInfo application{VK_STRUCTURE_TYPE_APPLICATION_INFO};
|
||||
application.pApplicationName = "AnyPS5 libSceAgcDriver";
|
||||
application.apiVersion = VK_API_VERSION_1_1;
|
||||
|
|
@ -613,6 +627,20 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
VkPhysicalDevice selected = VK_NULL_HANDLE;
|
||||
std::uint32_t family = 0;
|
||||
int selectedRank = -1;
|
||||
const char* requestedName = std::getenv("ANYPS5_GPU");
|
||||
if (requestedName != nullptr && *requestedName == '\0') requestedName = nullptr;
|
||||
const auto matchesRequest = [&](const char* deviceName) {
|
||||
const std::string_view name(deviceName);
|
||||
const std::string_view request(requestedName);
|
||||
if (request.size() > name.size()) return false;
|
||||
for (std::size_t start = 0; start + request.size() <= name.size(); ++start) {
|
||||
std::size_t index = 0;
|
||||
while (index < request.size() && std::tolower(static_cast<unsigned char>(name[start + index])) == std::tolower(static_cast<unsigned char>(request[index]))) ++index;
|
||||
if (index == request.size()) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
std::string candidateNames;
|
||||
const auto rankDeviceType = [](VkPhysicalDeviceType type) {
|
||||
switch (type) {
|
||||
case VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU: return 3;
|
||||
|
|
@ -626,10 +654,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
VkPhysicalDeviceProperties properties{};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties>("vkGetPhysicalDeviceProperties")(physical, &properties);
|
||||
APS5_LOG_OUT("Physical device candidate=%p name=%s api=0x%x type=%d", reinterpret_cast<void*>(physical), properties.deviceName, properties.apiVersion, static_cast<int>(properties.deviceType));
|
||||
if (!candidateNames.empty()) candidateNames += ", ";
|
||||
candidateNames += properties.deviceName;
|
||||
if (properties.apiVersion < VK_API_VERSION_1_1) {
|
||||
continue;
|
||||
}
|
||||
const int rank = rankDeviceType(properties.deviceType);
|
||||
if (requestedName != nullptr && !matchesRequest(properties.deviceName)) continue;
|
||||
const int rank = requestedName != nullptr ? 4 : rankDeviceType(properties.deviceType);
|
||||
if (rank <= selectedRank) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -663,6 +694,9 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
}
|
||||
}
|
||||
}
|
||||
if (selected == VK_NULL_HANDLE && requestedName != nullptr) {
|
||||
throw std::runtime_error(std::string("Vulkan: no usable device whose name contains ANYPS5_GPU=\"") + requestedName + "\"; devices: " + candidateNames);
|
||||
}
|
||||
if (selected == VK_NULL_HANDLE) {
|
||||
throw std::runtime_error(window ? "Vulkan: no Vulkan 1.1 device with graphics, compute and swapchain presentation" : "Vulkan: no Vulkan 1.1 graphics and compute queue");
|
||||
}
|
||||
|
|
@ -672,6 +706,7 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
|
||||
state->properties = properties.properties;
|
||||
state->physical = selected;
|
||||
state->srgbDecodeFormats = Graphics::SrgbDecodeFormats(state->InstanceFunction<PFN_vkGetPhysicalDeviceFormatProperties>("vkGetPhysicalDeviceFormatProperties"), selected);
|
||||
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BASIC_BIT) != 0) {
|
||||
state->capabilities.push_back(spv::CapabilityGroupNonUniform);
|
||||
if ((state->subgroup.supportedOperations & VK_SUBGROUP_FEATURE_BALLOT_BIT) != 0) state->capabilities.push_back(spv::CapabilityGroupNonUniformBallot);
|
||||
|
|
@ -739,6 +774,20 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
atomicInt64Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_ATOMIC_INT64_FEATURES_KHR};
|
||||
atomicInt64Features.shaderBufferInt64Atomics = VK_TRUE;
|
||||
if (bufferInt64Atomics) deviceExtensions.push_back(VK_KHR_SHADER_ATOMIC_INT64_EXTENSION_NAME);
|
||||
VkPhysicalDeviceShaderImageAtomicInt64FeaturesEXT imageAtomicInt64Features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_IMAGE_ATOMIC_INT64_FEATURES_EXT};
|
||||
if (hasExtension(VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceFeatures2 features{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, &imageAtomicInt64Features};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceFeatures2>("vkGetPhysicalDeviceFeatures2")(selected, &features);
|
||||
}
|
||||
const bool imageInt64Atomics = imageAtomicInt64Features.shaderImageInt64Atomics == VK_TRUE;
|
||||
imageAtomicInt64Features = {VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_IMAGE_ATOMIC_INT64_FEATURES_EXT};
|
||||
imageAtomicInt64Features.shaderImageInt64Atomics = VK_TRUE;
|
||||
if (imageInt64Atomics) {
|
||||
deviceExtensions.push_back(VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME);
|
||||
state->capabilities.push_back(spv::CapabilityInt64ImageEXT);
|
||||
state->imageInt64Atomics = true;
|
||||
state->spirvExtensions.push_back("SPV_EXT_shader_image_int64");
|
||||
}
|
||||
deviceExtensions.push_back(VK_KHR_SHADER_FLOAT_CONTROLS_EXTENSION_NAME);
|
||||
state->capabilities.push_back(spv::CapabilitySignedZeroInfNanPreserve);
|
||||
state->spirvExtensions.push_back("SPV_KHR_float_controls");
|
||||
|
|
@ -755,6 +804,13 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
// drops those writes instead of faulting the device.
|
||||
const bool imageRobustness = hasExtension(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME);
|
||||
if (imageRobustness) deviceExtensions.push_back(VK_EXT_IMAGE_ROBUSTNESS_EXTENSION_NAME);
|
||||
if (hasExtension(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME)) {
|
||||
VkPhysicalDeviceSamplerFilterMinmaxPropertiesEXT minmaxProperties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SAMPLER_FILTER_MINMAX_PROPERTIES_EXT};
|
||||
VkPhysicalDeviceProperties2 properties{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_PROPERTIES_2, &minmaxProperties};
|
||||
state->InstanceFunction<PFN_vkGetPhysicalDeviceProperties2>("vkGetPhysicalDeviceProperties2")(selected, &properties);
|
||||
state->samplerFilterMinmax = minmaxProperties.filterMinmaxSingleComponentFormats == VK_TRUE && minmaxProperties.filterMinmaxImageComponentMapping == VK_TRUE;
|
||||
if (state->samplerFilterMinmax) deviceExtensions.push_back(VK_EXT_SAMPLER_FILTER_MINMAX_EXTENSION_NAME);
|
||||
}
|
||||
// Indirect draws with a GPU-side count (DRAW_INDIRECT_MULTI with count_indirect); a device
|
||||
// without it resolves such draws on the CPU.
|
||||
state->drawIndirectCount = hasExtension(VK_KHR_DRAW_INDIRECT_COUNT_EXTENSION_NAME);
|
||||
|
|
@ -863,6 +919,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
if (enabled.shaderImageGatherExtended) state->capabilities.push_back(spv::CapabilityImageGatherExtended);
|
||||
enabled.shaderResourceMinLod = available.shaderResourceMinLod;
|
||||
if (enabled.shaderResourceMinLod) state->capabilities.push_back(spv::CapabilityMinLod);
|
||||
enabled.sampleRateShading = available.sampleRateShading;
|
||||
enabled.geometryShader = available.geometryShader;
|
||||
if (enabled.geometryShader) state->capabilities.push_back(spv::CapabilityGeometry);
|
||||
enabled.shaderClipDistance = available.shaderClipDistance;
|
||||
if (enabled.shaderStorageImageWriteWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageWriteWithoutFormat);
|
||||
if (enabled.shaderStorageImageReadWithoutFormat) state->capabilities.push_back(spv::CapabilityStorageImageReadWithoutFormat);
|
||||
// Bindless image tables index an image array with a wave-uniform runtime slot.
|
||||
|
|
@ -924,6 +984,10 @@ VulkanDevice::VulkanDevice(const PresentationWindow* window) : state(std::make_u
|
|||
atomicInt64Features.pNext = byteFeatures.pNext;
|
||||
byteFeatures.pNext = &atomicInt64Features;
|
||||
}
|
||||
if (imageInt64Atomics) {
|
||||
imageAtomicInt64Features.pNext = byteFeatures.pNext;
|
||||
byteFeatures.pNext = &imageAtomicInt64Features;
|
||||
}
|
||||
VkPhysicalDeviceImageRobustnessFeaturesEXT imageRobustnessFeatures{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_IMAGE_ROBUSTNESS_FEATURES_EXT, nullptr, VK_TRUE};
|
||||
if (imageRobustness) {
|
||||
imageRobustnessFeatures.pNext = byteFeatures.pNext;
|
||||
|
|
@ -2316,11 +2380,16 @@ ShaderRecompiler::SpirvTarget VulkanDevice::ComputeTarget(std::uint32_t waveSize
|
|||
return target;
|
||||
}
|
||||
|
||||
std::string VulkanDevice::DeviceName() const {
|
||||
return state->properties.deviceName;
|
||||
}
|
||||
|
||||
ShaderRecompiler::SpirvTarget VulkanDevice::Target() 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, {}, {}};
|
||||
target.fragmentShaderBarycentricEnabled = state->fragmentShaderBarycentric;
|
||||
target.nonConstantImageOffsets = state->maintenance8;
|
||||
target.srgbDecodeFormats = state->srgbDecodeFormats;
|
||||
if (state->meshShader) {
|
||||
const auto& mesh = state->meshLimits;
|
||||
target.mesh = ShaderRecompiler::MeshTargetLimits{{mesh.maxMeshWorkGroupSize[0], mesh.maxMeshWorkGroupSize[1], mesh.maxMeshWorkGroupSize[2]}, mesh.maxMeshWorkGroupInvocations, std::min(mesh.maxMeshSharedMemorySize, mesh.maxMeshPayloadAndSharedMemorySize), mesh.maxMeshOutputVertices, mesh.maxMeshOutputPrimitives, mesh.maxMeshOutputComponents, std::min(mesh.maxMeshOutputMemorySize, mesh.maxMeshPayloadAndOutputMemorySize), mesh.meshOutputPerVertexGranularity, mesh.meshOutputPerPrimitiveGranularity};
|
||||
|
|
@ -2333,6 +2402,10 @@ bool VulkanDevice::PrimitiveListRestart() const {
|
|||
return state->primitiveListRestart;
|
||||
}
|
||||
|
||||
bool VulkanDevice::SamplerFilterMinmax() const {
|
||||
return state->samplerFilterMinmax;
|
||||
}
|
||||
|
||||
Graphics::Context VulkanDevice::graphicsContext() const {
|
||||
static const bool noCache = std::getenv("APS5_NO_CONTEXT_CACHE") != nullptr;
|
||||
if (state->contextReady && !noCache) return state->context;
|
||||
|
|
@ -2375,15 +2448,18 @@ Graphics::Context VulkanDevice::buildContext() const {
|
|||
context.multiDrawIndirect = state->multiDrawIndirect;
|
||||
context.depthBounds = state->depthBounds;
|
||||
context.depthBiasClamp = state->depthBiasClamp;
|
||||
context.samplerFilterMinmax = state->samplerFilterMinmax;
|
||||
context.drawIndirectCount = state->drawIndirectCount;
|
||||
context.occlusionQueryPrecise = state->occlusionQueryPrecise;
|
||||
context.emptyBuffer = state->emptyBuffer ? state->emptyBuffer->Handle() : VK_NULL_HANDLE;
|
||||
context.copiedWriters = state->copiedWriters.get();
|
||||
context.functions = state->functionsReady ? &state->deviceFunctions : nullptr;
|
||||
context.descriptorIndexing = state->descriptorIndexing;
|
||||
context.imageInt64Atomics = state->imageInt64Atomics;
|
||||
context.primitiveListRestart = state->primitiveListRestart;
|
||||
context.imageViewMinLod = state->imageViewMinLod;
|
||||
context.pipelineExecutableInfo = state->pipelineExecutableInfo;
|
||||
context.srgbDecodeFormats = state->srgbDecodeFormats;
|
||||
return context;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ struct Context {
|
|||
bool occlusionQueryPrecise = false;
|
||||
bool depthBounds = false;
|
||||
bool depthBiasClamp = false;
|
||||
bool samplerFilterMinmax = false;
|
||||
VkBuffer emptyBuffer = VK_NULL_HANDLE;
|
||||
// The device's list of recorded dispatches whose copied written buffers await a CPU write-back
|
||||
// (VulkanDevice's State::copiedWriters; the draw counterpart is DrawCopiedWriters): an indirect
|
||||
|
|
@ -133,9 +134,11 @@ struct Context {
|
|||
// VK_EXT_descriptor_indexing with non-uniform sampled/storage image array indexing enabled
|
||||
// (bindless image tables in graphics stages).
|
||||
bool descriptorIndexing = false;
|
||||
bool imageInt64Atomics = false;
|
||||
bool primitiveListRestart = false;
|
||||
bool imageViewMinLod = false;
|
||||
bool pipelineExecutableInfo = false;
|
||||
std::uint32_t srgbDecodeFormats = 0;
|
||||
|
||||
template<typename TFunction>
|
||||
TFunction Function(const char* name) const {
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ struct MirrorStats {
|
|||
std::uint64_t heapRefills = 0;
|
||||
};
|
||||
MirrorStats MirrorCounters();
|
||||
void ClearImageMirrors(VkDevice device);
|
||||
|
||||
struct AddressCopy {
|
||||
std::uint64_t begin;
|
||||
|
|
|
|||
|
|
@ -23,11 +23,13 @@ struct GuestSamplerResource {
|
|||
float maxLod;
|
||||
float lodBias;
|
||||
VkBorderColor borderColor;
|
||||
VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT;
|
||||
bool compareEnable = false;
|
||||
VkCompareOp compareOp = VK_COMPARE_OP_NEVER;
|
||||
bool unnormalizedCoordinates = false;
|
||||
};
|
||||
|
||||
GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words);
|
||||
GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words, bool unnormalizedProven = false);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ void ValidateDepthBounds(const Context& context, const State& state);
|
|||
|
||||
void ValidateShaderPair(const ShaderRecompiler::RecompileResult& vertex, const ShaderRecompiler::RecompileResult& fragment);
|
||||
// Returns the color attachment locations the pixel shader writes.
|
||||
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false);
|
||||
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,25 +20,28 @@ public:
|
|||
Sampler& operator=(const Sampler&) = delete;
|
||||
|
||||
VkSampler Handle() const;
|
||||
bool RequiresFilterMinmax() const;
|
||||
|
||||
private:
|
||||
void release() noexcept;
|
||||
|
||||
Context context;
|
||||
VkSampler sampler = VK_NULL_HANDLE;
|
||||
bool requiresFilterMinmax = false;
|
||||
};
|
||||
|
||||
// One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use): samplers are
|
||||
// immutable and DecodeSamplerResource is a pure function of the words, so equal keys mean equal
|
||||
// samplers and nothing ever invalidates an entry. Holders keep a shared_ptr, so an entry evicted from
|
||||
// the LRU-capped cache lives on while a descriptor set in flight references it. One cache per device.
|
||||
// One VkSampler per distinct S# (its 4 words plus the shader's depth-compare use and the
|
||||
// unnormalized proof): samplers are immutable and DecodeSamplerResource is a pure function of the
|
||||
// words and the proof, so equal keys mean equal samplers and nothing ever invalidates an entry.
|
||||
// Holders keep a shared_ptr, so an entry evicted from the LRU-capped cache lives on while a
|
||||
// descriptor set in flight references it. One cache per device.
|
||||
// APS5_NO_SAMPLER_CACHE=1 creates a sampler per binding as before.
|
||||
class SamplerCache {
|
||||
public:
|
||||
explicit SamplerCache(std::size_t capacity = 1024);
|
||||
SamplerCache(const SamplerCache&) = delete;
|
||||
SamplerCache& operator=(const SamplerCache&) = delete;
|
||||
std::shared_ptr<Sampler> Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable);
|
||||
std::shared_ptr<Sampler> Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable, bool unnormalizedProven = false);
|
||||
// APS5_PROFILE_DRAW counters: lookups served by an existing sampler, and samplers created.
|
||||
std::uint64_t Hits() const { return hits; }
|
||||
std::uint64_t Misses() const { return misses; }
|
||||
|
|
@ -56,6 +59,8 @@ private:
|
|||
std::uint64_t misses = 0;
|
||||
};
|
||||
|
||||
void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ private:
|
|||
// Stage B: the record's texture when the fastRevalidate predicate proves it current under the
|
||||
// lock and the cache still holds it; null sends the element to cachedTexture.
|
||||
std::shared_ptr<Texture> fastTexture(const ImageRecord& record);
|
||||
void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item);
|
||||
void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span<const std::shared_ptr<Sampler>> shaderSamplers);
|
||||
void forgetDeferredInputs();
|
||||
void release() noexcept;
|
||||
void prepareAddressBindings(std::span<const CompiledShader> shaders, std::span<const GuestMemorySnapshot> snapshots);
|
||||
|
|
@ -412,6 +412,7 @@ private:
|
|||
std::vector<bool> storageFirstLayer;
|
||||
std::vector<bool> storageWritten;
|
||||
std::vector<bool> storageAtomic;
|
||||
std::vector<bool> storageAtomic64;
|
||||
std::vector<std::shared_ptr<Sampler>> samplers;
|
||||
bool reusable = false;
|
||||
std::vector<DirectRegion> directRegions;
|
||||
|
|
@ -436,6 +437,8 @@ private:
|
|||
struct DeferredImages {
|
||||
const ShaderRecompiler::DescriptorBinding* binding;
|
||||
std::size_t index;
|
||||
std::size_t firstSampler = 0;
|
||||
std::size_t samplerCount = 0;
|
||||
};
|
||||
std::vector<DeferredImages> deferredImages;
|
||||
std::uint64_t storageBuffers = 0;
|
||||
|
|
|
|||
|
|
@ -66,9 +66,16 @@ public:
|
|||
|
||||
VkImageView View() const;
|
||||
VkImageView FirstLayerView() const { return firstLayerView; }
|
||||
struct ViewRange {
|
||||
VkImageViewType type;
|
||||
std::uint32_t levels;
|
||||
std::uint32_t layers;
|
||||
};
|
||||
ViewRange SampledViewRange(bool firstLayer) const { return firstLayer ? firstLayerRange : viewRange; }
|
||||
// The layout the image is kept in while sampled.
|
||||
VkImageLayout Layout() const { return layout; }
|
||||
VkDeviceSize AllocationBytes() const { return allocationBytes; }
|
||||
VkFormat ViewFormat() const { return viewFormat; }
|
||||
// Whether this texture is a view of a storage image (no snapshot of its own).
|
||||
bool ViewsStorageImage() const { return storageSource != nullptr; }
|
||||
const StorageTexture* StorageSource() const { return storageSource.get(); }
|
||||
|
|
@ -90,8 +97,11 @@ private:
|
|||
std::shared_ptr<OwnedImage> owned;
|
||||
VkImageView view = VK_NULL_HANDLE;
|
||||
VkImageView firstLayerView = VK_NULL_HANDLE;
|
||||
ViewRange viewRange{};
|
||||
ViewRange firstLayerRange{};
|
||||
VkImageLayout layout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
|
||||
VkDeviceSize allocationBytes = 0;
|
||||
VkFormat viewFormat = VK_FORMAT_UNDEFINED;
|
||||
std::shared_ptr<ResidentColor> source;
|
||||
std::shared_ptr<StorageTexture> storageSource;
|
||||
std::unique_ptr<CommandBatch> upload;
|
||||
|
|
@ -114,7 +124,9 @@ public:
|
|||
// so successive mip writes of a chain share one image and one write-back.
|
||||
VkImageView View(std::uint32_t mip);
|
||||
VkImageView FirstLayerView(std::uint32_t mip);
|
||||
VkImageView StorageView(std::uint32_t mip, bool firstLayer);
|
||||
VkImageView AtomicView(std::uint32_t mip, bool firstLayer);
|
||||
VkImageView Atomic64View(std::uint32_t mip, bool firstLayer);
|
||||
// Render targets live in the same images: draws attach mip 0 through a view of the color
|
||||
// buffer's format and mark the image dirty like a storage write.
|
||||
bool Attachable() const { return attachable; }
|
||||
|
|
@ -444,6 +456,7 @@ private:
|
|||
std::map<std::uint32_t, VkImageView> extraViews;
|
||||
std::map<std::uint32_t, VkImageView> firstLayerViews;
|
||||
std::map<std::pair<std::uint32_t, bool>, VkImageView> atomicViews;
|
||||
std::map<std::pair<std::uint32_t, bool>, VkImageView> uintViews;
|
||||
bool attachable = false;
|
||||
std::map<std::tuple<VkFormat, std::uint32_t, std::uint32_t>, VkImageView> attachmentViews;
|
||||
VkFormat storageFormat = VK_FORMAT_UNDEFINED;
|
||||
|
|
|
|||
|
|
@ -10,10 +10,15 @@
|
|||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
struct Context;
|
||||
|
||||
VkFormat ResolveTextureFormat(std::uint32_t guestFormat);
|
||||
std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical);
|
||||
VkFormat SampledTextureFormat(const Context& context, 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);
|
||||
bool IsConvertedTextureFormat(std::uint32_t guestFormat);
|
||||
bool IsBlockCompressed(std::uint32_t guestFormat);
|
||||
std::uint32_t BlockWidth(std::uint32_t guestFormat);
|
||||
std::uint32_t BlockHeight(std::uint32_t guestFormat);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -427,6 +429,7 @@ DccKeys textureClearKeys(const GuestTextureResource& resource, std::uint64_t gue
|
|||
if (resource.dccAddress == 0) return DccKeys::Uncompressed;
|
||||
const auto keys = readDccKeys(resource.dccAddress, guestBytes, memoized);
|
||||
if (keys == DccKeys::Uncompressed) return keys;
|
||||
if (IsConvertedTextureFormat(resource.format) && (keys == DccKeys::Clear0001 || keys == DccKeys::Clear1110)) throw std::runtime_error(std::string("AGC graphics: DCC clear code ") + DccKeysName(keys) + " of converted texture format " + std::to_string(resource.format) + " is not implemented");
|
||||
std::byte probe[16]{};
|
||||
if (!IsDccClear(keys) || !FillDccClear(ResolveTextureFormat(resource.format), keys, resource.dccAlphaOnMsb, std::span(probe, std::min<std::size_t>(sizeof(probe), BytesPerElement(resource.format))))) {
|
||||
static std::mutex reportedMutex;
|
||||
|
|
|
|||
|
|
@ -518,7 +518,7 @@ std::set<std::uint32_t> CachedFragmentOutputs(const Context& context, std::span<
|
|||
}
|
||||
std::set<std::uint32_t> outputs;
|
||||
try {
|
||||
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing);
|
||||
outputs = ValidateShaders(shaders, state, context.subgroup, context.fragmentShaderBarycentric, context.descriptorIndexing, context.imageInt64Atomics);
|
||||
} catch (const std::exception& error) {
|
||||
if (keyed) {
|
||||
std::lock_guard lock(validationMutex());
|
||||
|
|
|
|||
|
|
@ -1002,6 +1002,19 @@ MirrorStats MirrorCounters() {
|
|||
return stats;
|
||||
}
|
||||
|
||||
void ClearImageMirrors(VkDevice device) {
|
||||
auto& state = Mirrors();
|
||||
std::map<std::uint64_t, std::shared_ptr<ImageMirror>> entries;
|
||||
{
|
||||
std::lock_guard lock(state.mutex);
|
||||
if (state.device != device) return;
|
||||
entries.swap(state.entries);
|
||||
state.failed.clear();
|
||||
state.heapBytes = 0;
|
||||
state.device = VK_NULL_HANDLE;
|
||||
}
|
||||
}
|
||||
|
||||
ImportProbe ProbeImportWriteProtection(const Context& context) {
|
||||
ImportProbe probe;
|
||||
#ifdef _WIN32
|
||||
|
|
|
|||
|
|
@ -20,6 +20,15 @@ bool isAnisoFilter(std::uint32_t raw) {
|
|||
return raw == 2 || raw == 3;
|
||||
}
|
||||
|
||||
VkSamplerReductionMode toVkReductionMode(std::uint32_t raw) {
|
||||
switch (raw) {
|
||||
case 0: return VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT;
|
||||
case 1: return VK_SAMPLER_REDUCTION_MODE_MIN_EXT;
|
||||
case 2: return VK_SAMPLER_REDUCTION_MODE_MAX_EXT;
|
||||
default: throw std::runtime_error("AGC graphics: guest sampler descriptor uses an unknown reduction filter mode " + std::to_string(raw));
|
||||
}
|
||||
}
|
||||
|
||||
VkSamplerAddressMode toVkAddressMode(std::uint32_t raw) {
|
||||
switch (raw) {
|
||||
case 0: return VK_SAMPLER_ADDRESS_MODE_REPEAT;
|
||||
|
|
@ -34,6 +43,10 @@ VkSamplerAddressMode toVkAddressMode(std::uint32_t raw) {
|
|||
}
|
||||
}
|
||||
|
||||
bool readsBorderColor(std::uint32_t raw) {
|
||||
return raw >= 4u && raw <= 7u;
|
||||
}
|
||||
|
||||
float toSignedLodBias(std::uint32_t raw) {
|
||||
const auto extended = static_cast<std::int32_t>((raw ^ 0x2000u) - 0x2000u);
|
||||
return static_cast<float>(extended) / 256.0f;
|
||||
|
|
@ -41,7 +54,7 @@ float toSignedLodBias(std::uint32_t raw) {
|
|||
|
||||
}
|
||||
|
||||
GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words) {
|
||||
GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words, bool unnormalizedProven) {
|
||||
Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords");
|
||||
|
||||
const auto clampX = (words[0] >> 0u) & 0x7u;
|
||||
|
|
@ -51,6 +64,7 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words)
|
|||
const auto depthCompareFunc = (words[0] >> 12u) & 0x7u;
|
||||
const auto forceUnormCoords = ((words[0] >> 15u) & 0x1u) != 0;
|
||||
const auto anisoThreshold = (words[0] >> 16u) & 0x7u;
|
||||
const auto mcCoordTrunc = ((words[0] >> 19u) & 0x1u) != 0;
|
||||
const auto forceSrgb = ((words[0] >> 20u) & 0x1u) != 0;
|
||||
const auto anisoBias = (words[0] >> 21u) & 0x3fu;
|
||||
const auto truncCoord = ((words[0] >> 27u) & 0x1u) != 0;
|
||||
|
|
@ -74,7 +88,17 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words)
|
|||
|
||||
const auto borderColorType = (words[3] >> 30u) & 0x3u;
|
||||
|
||||
Require(!forceUnormCoords, "guest sampler descriptor uses unnormalized coordinates which are not implemented");
|
||||
Require(!forceUnormCoords || unnormalizedProven, "guest sampler descriptor uses unnormalized coordinates which are not implemented");
|
||||
if (forceUnormCoords) {
|
||||
Require(xyMagFilter == xyMinFilter, "guest sampler descriptor uses unnormalized coordinates with different minification and magnification filters, which is not implemented");
|
||||
Require(!isAnisoFilter(xyMagFilter), "guest sampler descriptor uses unnormalized coordinates with anisotropic filtering, which is not implemented");
|
||||
Require(clampX == 2u || clampX == 6u, "guest sampler descriptor uses unnormalized coordinates with clamp mode " + std::to_string(clampX) + " on X; only clamp-to-last-texel and clamp-to-border are implemented");
|
||||
Require(clampY == 2u || clampY == 6u, "guest sampler descriptor uses unnormalized coordinates with clamp mode " + std::to_string(clampY) + " on Y; only clamp-to-last-texel and clamp-to-border are implemented");
|
||||
Require(!truncCoord, "guest sampler descriptor uses unnormalized coordinates with TRUNC_COORD, which is not implemented");
|
||||
Require(!mcCoordTrunc, "guest sampler descriptor uses unnormalized coordinates with MC_COORD_TRUNC, which is not implemented");
|
||||
} else {
|
||||
Require(!unnormalizedProven, "guest sampler descriptor is bound as unnormalized without FORCE_UNNORMALIZED");
|
||||
}
|
||||
Require(!forceSrgb, "guest sampler descriptor forces sRGB decoding which is not implemented");
|
||||
// TRUNC_COORD picks point-sampled texels by truncation instead of rounding, and the perf fields
|
||||
// trade mip/depth precision for speed; Vulkan's nearest filtering already floors, so these only
|
||||
|
|
@ -84,15 +108,17 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words)
|
|||
static_cast<void>(anisoThreshold);
|
||||
static_cast<void>(anisoBias);
|
||||
Require(!disableCubeWrap, "guest sampler descriptor disables seamless cube filtering which is not implemented");
|
||||
Require(filterMode == 0, "guest sampler descriptor uses a reduction filter mode which is not implemented");
|
||||
const auto reductionMode = toVkReductionMode(filterMode);
|
||||
Require(!disableDegamma, "guest sampler descriptor disables degamma which is not implemented");
|
||||
Require(lodBiasSec == 0, "guest sampler descriptor uses a secondary LOD bias which is not implemented");
|
||||
Require(!pointPreclamp, "guest sampler descriptor uses point preclamping which is not implemented");
|
||||
Require(!anisoOverride, "guest sampler descriptor uses an anisotropy override which is not implemented");
|
||||
Require(!blendZeroPrt, "guest sampler descriptor uses PRT blend-zero which is not implemented");
|
||||
if (mipFilter > 2u) Require(false, "guest sampler descriptor uses an unknown mip filter " + std::to_string(mipFilter));
|
||||
Require(mipFilter != 2u || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with a linear mip filter, which is not implemented");
|
||||
|
||||
const auto aniso = isAnisoFilter(xyMagFilter) || isAnisoFilter(xyMinFilter);
|
||||
Require(!aniso || reductionMode == VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, "guest sampler descriptor combines a min or max reduction with anisotropic filtering, which is not implemented");
|
||||
auto anisoRatio = 1.0f;
|
||||
if (aniso) {
|
||||
if (maxAnisoRatio > 4u) Require(false, "guest sampler descriptor uses an unknown anisotropy ratio " + std::to_string(maxAnisoRatio));
|
||||
|
|
@ -113,7 +139,10 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words)
|
|||
case 0: border = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK; break;
|
||||
case 1: border = VK_BORDER_COLOR_INT_OPAQUE_BLACK; break;
|
||||
case 2: border = VK_BORDER_COLOR_INT_OPAQUE_WHITE; break;
|
||||
default: throw std::runtime_error("AGC graphics: guest sampler descriptor uses a border color table which is not implemented");
|
||||
default:
|
||||
if (readsBorderColor(clampX) || readsBorderColor(clampY) || readsBorderColor(clampZ)) throw std::runtime_error("AGC graphics: guest sampler descriptor uses a border color table which is not implemented");
|
||||
border = VK_BORDER_COLOR_INT_TRANSPARENT_BLACK;
|
||||
break;
|
||||
}
|
||||
|
||||
GuestSamplerResource result{};
|
||||
|
|
@ -129,8 +158,18 @@ GuestSamplerResource DecodeSamplerResource(std::span<const std::uint32_t> words)
|
|||
result.maxLod = maxLod;
|
||||
result.lodBias = toSignedLodBias(lodBiasRaw);
|
||||
result.borderColor = border;
|
||||
result.reductionMode = reductionMode;
|
||||
const std::array compareOps{VK_COMPARE_OP_NEVER, VK_COMPARE_OP_LESS, VK_COMPARE_OP_EQUAL, VK_COMPARE_OP_LESS_OR_EQUAL, VK_COMPARE_OP_GREATER, VK_COMPARE_OP_NOT_EQUAL, VK_COMPARE_OP_GREATER_OR_EQUAL, VK_COMPARE_OP_ALWAYS};
|
||||
result.compareOp = compareOps.at(depthCompareFunc);
|
||||
if (forceUnormCoords) {
|
||||
result.unnormalizedCoordinates = true;
|
||||
result.mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST;
|
||||
result.minLod = 0.0f;
|
||||
result.maxLod = 0.0f;
|
||||
result.lodBias = 0.0f;
|
||||
result.anisotropyEnable = false;
|
||||
result.maxAnisotropy = 1.0f;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace AgcDriver::Graphics {
|
||||
|
||||
|
|
@ -8,8 +9,16 @@ namespace AgcDriver::Graphics {
|
|||
Require(!descriptor.anisotropyEnable || context.samplerAnisotropy, "guest sampler descriptor requests anisotropic filtering which the device does not support");
|
||||
Require(descriptor.maxAnisotropy <= context.limits.maxSamplerAnisotropy, "guest sampler descriptor requests an anisotropy ratio beyond the device limit");
|
||||
Require(descriptor.lodBias >= -context.limits.maxSamplerLodBias && descriptor.lodBias <= context.limits.maxSamplerLodBias, "guest sampler descriptor requests a LOD bias beyond the device limit");
|
||||
Require(!(descriptor.unnormalizedCoordinates && descriptor.compareEnable), "guest sampler descriptor with unnormalized coordinates enables depth comparison, which is not implemented");
|
||||
|
||||
VkSamplerCreateInfo info{VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};
|
||||
VkSamplerReductionModeCreateInfoEXT reduction{VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT, nullptr, descriptor.reductionMode};
|
||||
if (descriptor.reductionMode != VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT) {
|
||||
Require(context.samplerFilterMinmax, "guest sampler descriptor uses a min or max reduction which the device does not support");
|
||||
Require(!descriptor.compareEnable, "guest sampler descriptor combines a min or max reduction with depth comparison, which is not implemented");
|
||||
info.pNext = &reduction;
|
||||
requiresFilterMinmax = descriptor.magFilter == VK_FILTER_LINEAR || descriptor.minFilter == VK_FILTER_LINEAR;
|
||||
}
|
||||
info.magFilter = descriptor.magFilter;
|
||||
info.minFilter = descriptor.minFilter;
|
||||
info.mipmapMode = descriptor.mipmapMode;
|
||||
|
|
@ -24,7 +33,7 @@ namespace AgcDriver::Graphics {
|
|||
info.minLod = descriptor.minLod;
|
||||
info.maxLod = descriptor.maxLod;
|
||||
info.borderColor = descriptor.borderColor;
|
||||
info.unnormalizedCoordinates = VK_FALSE;
|
||||
info.unnormalizedCoordinates = descriptor.unnormalizedCoordinates ? VK_TRUE : VK_FALSE;
|
||||
Check(context.Function<PFN_vkCreateSampler>("vkCreateSampler")(context.device, &info, nullptr, &sampler), "vkCreateSampler");
|
||||
}
|
||||
|
||||
|
|
@ -40,11 +49,15 @@ namespace AgcDriver::Graphics {
|
|||
return sampler;
|
||||
}
|
||||
|
||||
bool Sampler::RequiresFilterMinmax() const {
|
||||
return requiresFilterMinmax;
|
||||
}
|
||||
|
||||
SamplerCache::SamplerCache(std::size_t capacity) : capacity(std::max<std::size_t>(capacity, 1)) {}
|
||||
|
||||
std::shared_ptr<Sampler> SamplerCache::Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable) {
|
||||
std::shared_ptr<Sampler> SamplerCache::Get(const Context& context, std::span<const std::uint32_t> words, bool compareEnable, bool unnormalizedProven) {
|
||||
Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords");
|
||||
const std::array<std::uint32_t, 5> key{words[0], words[1], words[2], words[3], compareEnable ? 1u : 0u};
|
||||
const std::array<std::uint32_t, 5> key{words[0], words[1], words[2], words[3], (compareEnable ? 1u : 0u) | (unnormalizedProven ? 2u : 0u)};
|
||||
std::lock_guard lock(mutex);
|
||||
++clock;
|
||||
if (const auto found = entries.find(key); found != entries.end()) {
|
||||
|
|
@ -53,7 +66,7 @@ namespace AgcDriver::Graphics {
|
|||
return found->second.sampler;
|
||||
}
|
||||
++misses;
|
||||
auto resource = DecodeSamplerResource(words);
|
||||
auto resource = DecodeSamplerResource(words, unnormalizedProven);
|
||||
resource.compareEnable = compareEnable;
|
||||
auto sampler = std::make_shared<Sampler>(context, resource);
|
||||
// The cap keeps live samplers well below the device's limit (NVIDIA: ~4000); a set in flight
|
||||
|
|
@ -66,4 +79,17 @@ namespace AgcDriver::Graphics {
|
|||
return sampler;
|
||||
}
|
||||
|
||||
void RequireFilterMinmax(const Context& context, VkFormat format, std::uint32_t samplerMask, std::span<const std::shared_ptr<Sampler>> samplers) {
|
||||
bool filtered = false;
|
||||
for (std::uint32_t element = 0; element < 32u; ++element) {
|
||||
if (((samplerMask >> element) & 1u) == 0u) continue;
|
||||
if (element >= samplers.size()) Require(false, "a sampled texture is paired with sampler element " + std::to_string(element) + ", which its shader does not bind");
|
||||
filtered = filtered || samplers[element]->RequiresFilterMinmax();
|
||||
}
|
||||
if (!filtered) return;
|
||||
VkFormatProperties properties{};
|
||||
context.formatProperties(context.physical, format, &properties);
|
||||
if ((properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT) == 0) Require(false, "a sampled texture whose format " + std::to_string(format) + " does not support min/max filtering is sampled through a min or max reduction sampler with linear filtering");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ VkComponentSwizzle ComponentSwizzleFor(std::uint8_t dstSel) {
|
|||
}
|
||||
}
|
||||
|
||||
VkComponentMapping ViewComponents(const GuestTextureResource& resource) {
|
||||
if (IsConvertedTextureFormat(resource.format)) return {VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A};
|
||||
return {ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)};
|
||||
}
|
||||
|
||||
// Sampled textures are reused across draws and dispatches while their guest bytes are unchanged; a
|
||||
// byte copy of the guest surface validates each reuse, so CPU or GPU writes to it force a re-upload.
|
||||
// The key is everything a lookup matches: the device, the eight descriptor words and the swizzle.
|
||||
|
|
@ -891,9 +896,12 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
|||
for (const auto& shader : shaders) {
|
||||
Require(shader.program != nullptr, "missing compiled shader");
|
||||
const VkShaderStageFlags flags = VulkanStage(shader.stage);
|
||||
std::uint64_t stageDescriptors = 0;
|
||||
std::uint64_t stageStorageBuffers = 0;
|
||||
std::uint64_t stageResources = 0;
|
||||
std::vector<std::size_t> offsetsInData;
|
||||
std::int64_t shaderData = -1;
|
||||
const auto firstSampler = samplers.size();
|
||||
const auto firstDeferred = deferredImages.size();
|
||||
for (const auto& binding : shader.program->bindings) {
|
||||
Require(binding.descriptorSet == 0, "unexpected descriptor set: every shader resource must use descriptor set zero");
|
||||
Require(occupied.insert(binding.binding).second, "duplicate shader binding");
|
||||
|
|
@ -901,6 +909,8 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
|||
const bool bufferRole = addressRole || binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers || binding.role == ShaderRecompiler::DescriptorRole::ShaderData || binding.role == ShaderRecompiler::DescriptorRole::FlattenedSrt;
|
||||
const bool imageRole = binding.role == ShaderRecompiler::DescriptorRole::GuestImages || binding.role == ShaderRecompiler::DescriptorRole::GuestSamplers;
|
||||
if (imageRole) {
|
||||
if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage || binding.kind == ShaderRecompiler::DescriptorKind::StorageImage) stageResources += binding.count;
|
||||
Require(stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits");
|
||||
addImageBinding(binding, flags);
|
||||
continue;
|
||||
}
|
||||
|
|
@ -908,9 +918,10 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
|||
if (binding.kind != ShaderRecompiler::DescriptorKind::StorageBuffer) Require(false, std::string("unsupported descriptor kind ") + kindName(binding.kind) + " for role " + roleName(binding.role) + ": only StorageBuffer is supported");
|
||||
Require(!binding.readOnly, "read-only descriptors are unsupported because the recompiler emits no NonWritable decoration");
|
||||
Require(binding.count != 0, "empty descriptor binding");
|
||||
stageDescriptors += binding.count;
|
||||
stageStorageBuffers += binding.count;
|
||||
stageResources += binding.count;
|
||||
storageBuffers += binding.count;
|
||||
Require(stageDescriptors <= context.limits.maxPerStageDescriptorStorageBuffers && stageDescriptors <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits");
|
||||
Require(stageStorageBuffers <= context.limits.maxPerStageDescriptorStorageBuffers && stageResources <= context.limits.maxPerStageResources, "shader descriptors exceed per-stage limits");
|
||||
Binding item{{binding.binding, VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, binding.count, flags, nullptr}, {}};
|
||||
if (binding.role == ShaderRecompiler::DescriptorRole::GuestBuffers) {
|
||||
Require(binding.guestDescriptor.size() == static_cast<std::uint64_t>(binding.count) * 4, "guest buffer descriptor must contain four DWORDs per array element");
|
||||
|
|
@ -943,6 +954,10 @@ void ShaderResources::buildPrepare(std::span<const CompiledShader> shaders, cons
|
|||
bindings.push_back(std::move(item));
|
||||
}
|
||||
for (const auto index : offsetsInData) allocations[index].dataAllocation = shaderData;
|
||||
for (auto index = firstDeferred; index < deferredImages.size(); ++index) {
|
||||
deferredImages[index].firstSampler = firstSampler;
|
||||
deferredImages[index].samplerCount = samplers.size() - firstSampler;
|
||||
}
|
||||
}
|
||||
Require(storageBuffers <= context.limits.maxDescriptorSetStorageBuffers, "pipeline descriptors exceed device limits");
|
||||
timing.bindingsMs = phase(BuildPhase::Bindings);
|
||||
|
|
@ -1010,7 +1025,7 @@ void ShaderResources::buildComplete() {
|
|||
try {
|
||||
// The lookups run in plan order: Revalidate walks the bindings the same way, and consecutive
|
||||
// storage elements of one mip chain share the previous element's image.
|
||||
for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index]);
|
||||
for (const auto& deferred : deferredImages) resolveImageBinding(*deferred.binding, bindings[deferred.index], std::span<const std::shared_ptr<Sampler>>(samplers).subspan(deferred.firstSampler, deferred.samplerCount));
|
||||
deferredImages.clear();
|
||||
// The records served their purpose: each holds the cache entry's objects as of stage A,
|
||||
// which would otherwise keep a replaced texture or an evicted storage image (and its device
|
||||
|
|
@ -1057,7 +1072,7 @@ void ShaderResources::buildComplete() {
|
|||
break;
|
||||
case VK_DESCRIPTOR_TYPE_STORAGE_IMAGE:
|
||||
write.pImageInfo = images.data() + images.size();
|
||||
for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageFirstLayer[index] ? storageTextures[index]->FirstLayerView(storageMips[index]) : storageTextures[index]->View(storageMips[index]), VK_IMAGE_LAYOUT_GENERAL});
|
||||
for (const auto index : binding.imageAllocations) images.push_back({VK_NULL_HANDLE, storageAtomic64[index] ? storageTextures[index]->Atomic64View(storageMips[index], storageFirstLayer[index]) : storageAtomic[index] ? storageTextures[index]->AtomicView(storageMips[index], storageFirstLayer[index]) : storageTextures[index]->StorageView(storageMips[index], storageFirstLayer[index]), VK_IMAGE_LAYOUT_GENERAL});
|
||||
break;
|
||||
case VK_DESCRIPTOR_TYPE_SAMPLER:
|
||||
write.pImageInfo = images.data() + images.size();
|
||||
|
|
@ -1203,9 +1218,13 @@ std::vector<std::uint32_t> ShaderResources::ContentKey(const CompiledShader& sha
|
|||
packBits(binding.samplerDepthCompare);
|
||||
packBits(binding.imageDepthCompare);
|
||||
packBits(binding.imageAtomic);
|
||||
packBits(binding.samplerUnnormalized);
|
||||
packBits(binding.imageUnnormalized);
|
||||
// Read-only elements are bound without a write set: an object built for one written set
|
||||
// must not serve a build with another (the variant implies it, this makes it explicit).
|
||||
packBits(binding.bufferWritten);
|
||||
key.push_back(static_cast<std::uint32_t>(binding.imageSamplers.size()));
|
||||
key.insert(key.end(), binding.imageSamplers.begin(), binding.imageSamplers.end());
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
|
@ -1697,7 +1716,7 @@ bool ShaderResources::Revalidate(std::span<const CompiledShader> shaders, ProofR
|
|||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
|
||||
const auto resource = DecodeTextureResource(words);
|
||||
const VkComponentMapping components{ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)};
|
||||
const VkComponentMapping components = ViewComponents(resource);
|
||||
if (textureIndex >= textures.size() || cachedTexture(context, words, resource, components, 0, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element)) != textures[textureIndex]) return false;
|
||||
++textureIndex;
|
||||
}
|
||||
|
|
@ -2368,14 +2387,16 @@ void ShaderResources::addImageBinding(const ShaderRecompiler::DescriptorBinding&
|
|||
Require(elementWords == 4, "guest sampler descriptor must contain 4 dwords");
|
||||
Require(binding.count <= context.limits.maxPerStageDescriptorSamplers, "shader sampler descriptors exceed per-stage limits");
|
||||
Require(binding.samplerDepthCompare.size() == binding.count, "guest sampler binding is missing depth comparison metadata");
|
||||
Require(binding.samplerUnnormalized.empty() || binding.samplerUnnormalized.size() == binding.count, "guest sampler binding has unnormalized coordinate metadata of another size");
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
|
||||
const bool compareEnable = binding.samplerDepthCompare.at(element);
|
||||
const bool unnormalized = element < binding.samplerUnnormalized.size() && binding.samplerUnnormalized[element];
|
||||
static const bool noSamplerCache = std::getenv("APS5_NO_SAMPLER_CACHE") != nullptr;
|
||||
if (context.samplerCache != nullptr && !noSamplerCache) {
|
||||
samplers.push_back(context.samplerCache->Get(context, words, compareEnable));
|
||||
samplers.push_back(context.samplerCache->Get(context, words, compareEnable, unnormalized));
|
||||
} else {
|
||||
auto resource = DecodeSamplerResource(words);
|
||||
auto resource = DecodeSamplerResource(words, unnormalized);
|
||||
resource.compareEnable = compareEnable;
|
||||
samplers.push_back(std::make_shared<Sampler>(context, resource));
|
||||
}
|
||||
|
|
@ -2438,7 +2459,7 @@ bool ShaderResources::precollectImages() {
|
|||
record.decoded = true;
|
||||
if (record.sampled && !noRecords && words.size() == 8 && (binding.imageDepthCompare.empty() || !binding.imageDepthCompare.at(element))) {
|
||||
std::copy(words.begin(), words.end(), record.words.begin());
|
||||
record.components = {ComponentSwizzleFor(record.resource.dstSelX), ComponentSwizzleFor(record.resource.dstSelY), ComponentSwizzleFor(record.resource.dstSelZ), ComponentSwizzleFor(record.resource.dstSelW)};
|
||||
record.components = ViewComponents(record.resource);
|
||||
record.keys = TextureClearKeys(record.resource, record.guestBytes);
|
||||
auto& cache = Textures();
|
||||
std::lock_guard lock(cache.mutex);
|
||||
|
|
@ -2514,11 +2535,12 @@ std::shared_ptr<Texture> ShaderResources::fastTexture(const ImageRecord& record)
|
|||
return record.texture;
|
||||
}
|
||||
|
||||
void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item) {
|
||||
void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span<const std::shared_ptr<Sampler>> shaderSamplers) {
|
||||
auto& counters = TextureCounts();
|
||||
// The element's stage-A record, when the pass ran (records follow the plan order exactly).
|
||||
const auto nextRecord = [&]() -> const ImageRecord* { return nextImageRecord < imageRecords.size() ? &imageRecords[nextImageRecord++] : nullptr; };
|
||||
if (binding.kind == ShaderRecompiler::DescriptorKind::SampledImage) {
|
||||
Require(binding.imageSamplers.size() == binding.count, "guest sampled image binding is missing its image-sampler pairs");
|
||||
const auto elementWords = binding.guestDescriptor.size() / binding.count;
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
const auto words = std::span<const std::uint32_t>(binding.guestDescriptor).subspan(static_cast<std::size_t>(element) * elementWords, elementWords);
|
||||
|
|
@ -2526,7 +2548,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
|
|||
const auto resource = record != nullptr && record->decoded ? record->resource : DecodeTextureResource(words);
|
||||
const bool firstLayer = binding.imageShape == ShaderRecompiler::DescriptorImageShape::Image2D && resource.dimension == TextureDimension::k2DArray;
|
||||
if (!firstLayer && !MatchesGuestDimension(*binding.imageShape, resource.dimension)) throw std::runtime_error("AGC graphics: guest texture dimension disagrees with the shader's declared image shape (shape " + std::to_string(static_cast<int>(*binding.imageShape)) + ", dimension " + std::to_string(static_cast<int>(resource.dimension)) + ")");
|
||||
const VkComponentMapping components{ComponentSwizzleFor(resource.dstSelX), ComponentSwizzleFor(resource.dstSelY), ComponentSwizzleFor(resource.dstSelZ), ComponentSwizzleFor(resource.dstSelW)};
|
||||
const VkComponentMapping components = ViewComponents(resource);
|
||||
const auto guestBytes = record != nullptr && record->decoded ? record->guestBytes : DescribeSurface(resource).guestBytes;
|
||||
std::shared_ptr<Texture> texture;
|
||||
if (record != nullptr && record->texture != nullptr) {
|
||||
|
|
@ -2534,6 +2556,12 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
|
|||
(texture != nullptr ? counters.fastHits : counters.fastMisses).fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
if (texture == nullptr) texture = cachedTexture(context, words, resource, components, guestBytes, !binding.imageDepthCompare.empty() && binding.imageDepthCompare.at(element));
|
||||
if (element < binding.imageUnnormalized.size() && binding.imageUnnormalized[element]) {
|
||||
const auto range = texture->SampledViewRange(firstLayer);
|
||||
const bool singleLevel = range.levels == 1u && range.layers == 1u && resource.baseLevel == 0u && EffectiveMinLod(resource) == 0.0f;
|
||||
if (!singleLevel || (range.type != VK_IMAGE_VIEW_TYPE_1D && range.type != VK_IMAGE_VIEW_TYPE_2D)) throw std::runtime_error("AGC graphics: guest texture sampled with unnormalized coordinates is not a single-level, single-layer 1D or 2D view starting at mip 0, which is not implemented (base level " + std::to_string(resource.baseLevel) + ", levels " + std::to_string(range.levels) + ", layers " + std::to_string(range.layers) + ", view type " + std::to_string(static_cast<int>(range.type)) + ")");
|
||||
}
|
||||
RequireFilterMinmax(context, texture->ViewFormat(), binding.imageSamplers[element], shaderSamplers);
|
||||
textures.push_back(std::move(texture));
|
||||
textureFirstLayer.push_back(firstLayer);
|
||||
describedRanges.push_back({"texture", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
|
||||
|
|
@ -2567,6 +2595,7 @@ void ShaderResources::resolveImageBinding(const ShaderRecompiler::DescriptorBind
|
|||
// Images the shader only reads have nothing to store back.
|
||||
storageWritten.push_back(element >= binding.imageWritten.size() || binding.imageWritten[element]);
|
||||
storageAtomic.push_back(element < binding.imageAtomic.size() && binding.imageAtomic[element]);
|
||||
storageAtomic64.push_back(element < binding.imageAtomic64.size() && binding.imageAtomic64[element]);
|
||||
describedRanges.push_back({"storage", resource.baseAddress, guestBytes, resource.width, resource.height, resource.format, static_cast<int>(resource.tileMode), resource.dccAddress});
|
||||
item.imageAllocations.push_back(storageTextures.size() - 1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ struct Module {
|
|||
}
|
||||
};
|
||||
|
||||
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing) {
|
||||
Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics) {
|
||||
using Stage = ShaderRecompiler::ShaderStage;
|
||||
Require(compiled.program != nullptr, "missing compiled shader");
|
||||
const auto& shader = *compiled.program;
|
||||
|
|
@ -235,7 +235,8 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
isTessellationCapability ||
|
||||
isMeshCapability ||
|
||||
isFeatureCapability ||
|
||||
isDescriptorIndexingCapability,
|
||||
isDescriptorIndexingCapability ||
|
||||
(imageInt64Atomics && (capability == spv::CapabilityInt64Atomics || capability == spv::CapabilityInt64ImageEXT)),
|
||||
std::string("SPIR-V requires unsupported device capability ") +
|
||||
std::to_string(static_cast<std::uint32_t>(capability)));
|
||||
|
||||
|
|
@ -252,7 +253,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
Require(fragment && fragmentShaderBarycentric, "SPV_KHR_fragment_shader_barycentric requires enabled fragmentShaderBarycentric in a fragment shader");
|
||||
break;
|
||||
}
|
||||
Require(extension == "SPV_KHR_float_controls" || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
|
||||
Require(extension == "SPV_KHR_float_controls" || (imageInt64Atomics && extension == "SPV_EXT_shader_image_int64") || (mesh && (extension == "SPV_EXT_mesh_shader" || extension == "SPV_KHR_physical_storage_buffer")) || (descriptorIndexing && extension == "SPV_EXT_descriptor_indexing") || (shader.bdaAbiVersion == ShaderRecompiler::BdaAbi::Version && (extension == "SPV_KHR_physical_storage_buffer" || extension == "SPV_KHR_8bit_storage")), "unsupported SPIR-V extension");
|
||||
break;
|
||||
}
|
||||
case spv::OpDecorateId:
|
||||
|
|
@ -491,7 +492,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi
|
|||
|
||||
}
|
||||
|
||||
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing) {
|
||||
std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing, bool imageInt64Atomics) {
|
||||
using Stage = ShaderRecompiler::ShaderStage;
|
||||
const bool tessellation = state.stages.path == ShaderPath::Tessellation;
|
||||
const bool mesh = state.stages.path == ShaderPath::Geometry;
|
||||
|
|
@ -507,7 +508,7 @@ std::set<std::uint32_t> ValidateShaders(std::span<const CompiledShader> shaders,
|
|||
Require(shaders[i].program != nullptr, "missing compiled shader");
|
||||
Require(shaders[i].stage == expected, "graphics stage order disagrees");
|
||||
for (const auto& binding : shaders[i].program->bindings) Require(binding.descriptorSet == 0, "graphics resource uses a descriptor set other than zero");
|
||||
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing);
|
||||
const auto current = Inspect(shaders[i], state, subgroup, fragmentShaderBarycentric, descriptorIndexing, imageInt64Atomics);
|
||||
if (i != 0) {
|
||||
for (const auto& [location, signature] : current.inputs) {
|
||||
const auto output = previous.outputs.find(location);
|
||||
|
|
|
|||
|
|
@ -67,11 +67,24 @@ constexpr std::uint32_t DepthControlMask = ~0x007007f0u;
|
|||
constexpr std::uint32_t ShaderControlMask = ~(0x0000f870u | 0x00020600u);
|
||||
constexpr std::uint32_t AlphaToCoverageMask = ~0x0001ff00u;
|
||||
constexpr std::uint32_t ScanModeMask = ~2u;
|
||||
constexpr std::uint32_t ScanControlMask = ~0x06003fffu;
|
||||
constexpr std::uint32_t ScanControlMask = ~0x06023fffu;
|
||||
constexpr std::uint32_t ScreenOffsetMask = ~0x01ff01ffu;
|
||||
// Bits 26/27 (ZCLIP_NEAR/FAR_DISABLE) become depth clamping; bit 19 selects the [0, 1] clip space.
|
||||
constexpr std::uint32_t ClipControlMask = ~(0x80000u | 0x01000000u | 0x0c000000u);
|
||||
|
||||
bool zFormatSupported(std::uint32_t format) {
|
||||
return format == 0u || format == 1u || format == 2u || format == 3u || format == 9u;
|
||||
}
|
||||
|
||||
std::uint32_t shaderControlMask(std::uint32_t zFormat) {
|
||||
constexpr std::uint32_t zExportEnable = 0x1u;
|
||||
constexpr std::uint32_t maskExportEnable = 0x100u;
|
||||
auto mask = ShaderControlMask;
|
||||
if (zFormat != 0u && zFormatSupported(zFormat)) mask &= ~zExportEnable;
|
||||
if (zFormat == 9u) mask &= ~maskExportEnable;
|
||||
return mask;
|
||||
}
|
||||
|
||||
// Debug aid: APS5_IGNORE_DEPTH_TEST=1 renders depth- and stencil-tested draws without a depth
|
||||
// target as if their tests always passed (wrong occlusion, but the draws run), so stages that
|
||||
// depth-test everything can exercise the draw paths before depth targets exist.
|
||||
|
|
@ -469,7 +482,7 @@ State DecodeState(const QueueState& queue) {
|
|||
Require((depthControl & 8u) == 0 || result.depth.has_value(), "depth bounds without a depth surface");
|
||||
Require((depthControl & 0xc0000000u) == 0, "depth-conditional color writes are unsupported");
|
||||
}
|
||||
zero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution");
|
||||
zero(cx, 0x203, shaderControlMask(read(cx, 0x1c4)), "depth export, shader coverage or ordered fragment execution");
|
||||
zero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage");
|
||||
zero(cx, 0x2f8, ~0u, "multisampling or coverage conversion");
|
||||
zero(cx, 0x292, ScanModeMask, "scan conversion mode");
|
||||
|
|
@ -515,7 +528,7 @@ State DecodeState(const QueueState& queue) {
|
|||
|
||||
// CB_COLOR_CONTROL mode 0 disables color writes, which only matters when a target is written.
|
||||
if (const auto colorControl = read(cx, 0x202); !colorControlSupported(colorControl, result.hasColorTarget)) throw std::runtime_error(colorControlMessage(colorControl));
|
||||
zero(cx, 0x1c4, ~0u, "depth or sample-mask export");
|
||||
zero(cx, 0x1c4, zFormatSupported(read(cx, 0x1c4)) ? 0u : ~0u, "depth or sample-mask export");
|
||||
const auto exportFormat = result.hasColorTarget ? read(cx, 0x1c5) : 0u;
|
||||
APS5_LOG_OUT_DEBUG("Export format=%u", exportFormat);
|
||||
// SPI_SHADER_POS_FORMAT: POS0 must be a 4-component position; later vectors carry the misc/clip
|
||||
|
|
@ -758,7 +771,9 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
|
|||
if (auto reason = require((word & 8u) == 0 || surface, "depth bounds without a depth surface"); !reason.empty()) return reason;
|
||||
if (auto reason = require((word & 0xc0000000u) == 0, "depth-conditional color writes are unsupported"); !reason.empty()) return reason;
|
||||
}
|
||||
if (auto reason = nonzero(cx, 0x203, ShaderControlMask, "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason;
|
||||
std::uint32_t zFormat = 0;
|
||||
static_cast<void>(value(cx, 0x1c4, zFormat));
|
||||
if (auto reason = nonzero(cx, 0x203, shaderControlMask(zFormat), "depth export, shader coverage or ordered fragment execution"); !reason.empty()) return reason;
|
||||
if (auto reason = nonzero(cx, 0x2dc, AlphaToCoverageMask, "alpha-to-coverage"); !reason.empty()) return reason;
|
||||
if (auto reason = nonzero(cx, 0x2f8, ~0u, "multisampling or coverage conversion"); !reason.empty()) return reason;
|
||||
if (auto reason = nonzero(cx, 0x292, ScanModeMask, "scan conversion mode"); !reason.empty()) return reason;
|
||||
|
|
@ -775,7 +790,7 @@ std::string DrawRejection(const QueueState& queue, bool indexed) {
|
|||
if (auto reason = nonzero(cx, 0x204, ClipControlMask, "unsupported PA_CL_CLIP_CNTL flags"); !reason.empty()) return reason;
|
||||
std::uint32_t targetMask = 0, shaderMask = 0;
|
||||
if (value(cx, 0x8e, targetMask) && value(cx, 0x8f, shaderMask) && value(cx, 0x202, word) && !colorControlSupported(word, (targetMask & shaderMask) != 0)) return colorControlMessage(word);
|
||||
if (auto reason = nonzero(cx, 0x1c4, ~0u, "depth or sample-mask export"); !reason.empty()) return reason;
|
||||
if (auto reason = nonzero(cx, 0x1c4, zFormatSupported(zFormat) ? 0u : ~0u, "depth or sample-mask export"); !reason.empty()) return reason;
|
||||
if (PixelProgramUnset(queue)) return NullPixelProgramRejection(queue);
|
||||
for (const auto offset : {0x1b3u, 0x1b4u, 0x1c5u}) {
|
||||
if (find(cx, offset) != cx.end()) continue;
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
|||
static const bool profile = std::getenv("APS5_PROFILE_DRAW") != nullptr;
|
||||
PhaseTimer timer;
|
||||
try {
|
||||
const auto colorFormat = ResolveTextureFormat(descriptor.format);
|
||||
const auto colorFormat = SampledTextureFormat(context, descriptor.format);
|
||||
Require(!depthCompare || colorFormat == VK_FORMAT_R32_SFLOAT || colorFormat == VK_FORMAT_R16_UNORM, "comparison sampling requires an R32 float or R16 unorm depth texture");
|
||||
Require(!depthCompare || descriptor.dimension != TextureDimension::k3D, "comparison sampling does not support 3D depth textures");
|
||||
const auto vkFormat = depthCompare ? (colorFormat == VK_FORMAT_R32_SFLOAT ? VK_FORMAT_D32_SFLOAT : VK_FORMAT_D16_UNORM) : colorFormat;
|
||||
|
|
@ -423,12 +423,14 @@ Texture::Texture(const Context& context, TextureDetiler& detiler, const GuestTex
|
|||
viewInfo.image = image;
|
||||
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
|
||||
viewInfo.format = vkFormat;
|
||||
viewFormat = vkFormat;
|
||||
viewInfo.components = depthCompare ? VkComponentMapping{} : components;
|
||||
viewInfo.subresourceRange = {aspect, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
|
||||
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
|
||||
ChainMinLod(context, descriptor, viewInfo, minLod);
|
||||
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView");
|
||||
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
|
||||
createFirstLayerView(descriptor, viewInfo);
|
||||
if (profile) {
|
||||
auto& totals = Profile();
|
||||
|
|
@ -454,7 +456,7 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
|||
PhaseTimer timer;
|
||||
try {
|
||||
Require(source != nullptr && CanCopyFrom(*source, descriptor), "storage image does not match the sampled texture");
|
||||
const auto vkFormat = ResolveTextureFormat(descriptor.format);
|
||||
const auto vkFormat = SampledTextureFormat(context, descriptor.format);
|
||||
const auto geometry = DescribeSurface(descriptor);
|
||||
APS5_LOG_OUT("Texture address=0x%llx %ux%u mips=%u viewed from storage image (vk=%d)", static_cast<unsigned long long>(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, static_cast<int>(vkFormat));
|
||||
// Storage images stay in the general layout; the view samples them there.
|
||||
|
|
@ -468,11 +470,13 @@ Texture::Texture(const Context& context, const std::shared_ptr<StorageTexture>&
|
|||
viewInfo.image = source->Image();
|
||||
viewInfo.viewType = ViewTypeFor(descriptor.dimension, viewLayerCount);
|
||||
viewInfo.format = vkFormat;
|
||||
viewFormat = vkFormat;
|
||||
viewInfo.components = components;
|
||||
viewInfo.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, descriptor.baseLevel, viewLevelCount, descriptor.baseArray, viewLayerCount};
|
||||
VkImageViewMinLodCreateInfoEXT minLod{VK_STRUCTURE_TYPE_IMAGE_VIEW_MIN_LOD_CREATE_INFO_EXT};
|
||||
ChainMinLod(context, descriptor, viewInfo, minLod);
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView storage view");
|
||||
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
|
||||
createFirstLayerView(descriptor, viewInfo);
|
||||
if (profile) {
|
||||
auto& totals = Profile();
|
||||
|
|
@ -492,9 +496,11 @@ Texture::Texture(const Context& context, VkImage depthImage, VkFormat depthForma
|
|||
viewInfo.image = depthImage;
|
||||
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
viewInfo.format = depthFormat;
|
||||
viewFormat = depthFormat;
|
||||
viewInfo.components = components;
|
||||
viewInfo.subresourceRange = {aspect, 0, 1, 0, 1};
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &view), "vkCreateImageView depth plane");
|
||||
viewRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
|
||||
}
|
||||
|
||||
Texture::~Texture() {
|
||||
|
|
@ -506,6 +512,7 @@ void Texture::createFirstLayerView(const GuestTextureResource& descriptor, VkIma
|
|||
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
|
||||
viewInfo.subresourceRange.layerCount = 1;
|
||||
Check(context.Function<PFN_vkCreateImageView>("vkCreateImageView")(context.device, &viewInfo, nullptr, &firstLayerView), "vkCreateImageView first layer");
|
||||
firstLayerRange = {viewInfo.viewType, viewInfo.subresourceRange.levelCount, viewInfo.subresourceRange.layerCount};
|
||||
}
|
||||
|
||||
void Texture::release() noexcept {
|
||||
|
|
@ -580,6 +587,20 @@ VkFormat StorageFormatOrUndefined(const Context& context, VkFormat format) {
|
|||
return storage;
|
||||
}
|
||||
|
||||
VkFormat UintFormatOfSint(VkFormat format) {
|
||||
switch (format) {
|
||||
case VK_FORMAT_R8G8_SINT: return VK_FORMAT_R8G8_UINT;
|
||||
case VK_FORMAT_R8G8B8A8_SINT: return VK_FORMAT_R8G8B8A8_UINT;
|
||||
case VK_FORMAT_R16_SINT: return VK_FORMAT_R16_UINT;
|
||||
case VK_FORMAT_R16G16_SINT: return VK_FORMAT_R16G16_UINT;
|
||||
case VK_FORMAT_R16G16B16A16_SINT: return VK_FORMAT_R16G16B16A16_UINT;
|
||||
case VK_FORMAT_R32_SINT: return VK_FORMAT_R32_UINT;
|
||||
case VK_FORMAT_R32G32_SINT: return VK_FORMAT_R32G32_UINT;
|
||||
case VK_FORMAT_R32G32B32A32_SINT: return VK_FORMAT_R32G32B32A32_UINT;
|
||||
default: return VK_FORMAT_UNDEFINED;
|
||||
}
|
||||
}
|
||||
|
||||
VkFormat StorageFormatFor(const Context& context, VkFormat format) {
|
||||
const auto storage = StorageFormatOrUndefined(context, format);
|
||||
if (storage == VK_FORMAT_UNDEFINED) Require(false, "guest storage texture format " + std::to_string(format) + " cannot be used as a storage image");
|
||||
|
|
@ -1001,6 +1022,18 @@ VkImageView StorageTexture::FirstLayerView(std::uint32_t mip) {
|
|||
return created;
|
||||
}
|
||||
|
||||
VkImageView StorageTexture::StorageView(std::uint32_t mip, bool firstLayer) {
|
||||
const auto format = UintFormatOfSint(storageFormat);
|
||||
if (format == VK_FORMAT_UNDEFINED) return firstLayer ? FirstLayerView(mip) : View(mip);
|
||||
if (format == VK_FORMAT_R32_UINT) return AtomicView(mip, firstLayer);
|
||||
const auto found = uintViews.find({mip, firstLayer});
|
||||
if (found != uintViews.end()) return found->second;
|
||||
Require(StorageFormatOrUndefined(context, format) == format, "storage image of format " + std::to_string(storageFormat) + " has no storage view of its UINT format " + std::to_string(format));
|
||||
const auto created = createView(mip, firstLayer, format);
|
||||
uintViews.emplace(std::pair{mip, firstLayer}, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
VkImageView StorageTexture::AtomicView(std::uint32_t mip, bool firstLayer) {
|
||||
if (storageFormat == VK_FORMAT_R32_UINT) return firstLayer ? FirstLayerView(mip) : View(mip);
|
||||
Require(storageFormat == VK_FORMAT_R32_SINT || storageFormat == VK_FORMAT_R32_SFLOAT, "storage image atomics need a surface of one 32-bit component");
|
||||
|
|
@ -1011,6 +1044,16 @@ VkImageView StorageTexture::AtomicView(std::uint32_t mip, bool firstLayer) {
|
|||
return created;
|
||||
}
|
||||
|
||||
VkImageView StorageTexture::Atomic64View(std::uint32_t mip, bool firstLayer) {
|
||||
if (storageFormat == VK_FORMAT_R64_UINT) return firstLayer ? FirstLayerView(mip) : View(mip);
|
||||
Require(storageFormat == VK_FORMAT_R32G32_UINT || storageFormat == VK_FORMAT_R32G32_SINT || storageFormat == VK_FORMAT_R32G32_SFLOAT, "64-bit storage image atomics need a surface of two 32-bit components");
|
||||
const auto found = atomicViews.find({mip, firstLayer});
|
||||
if (found != atomicViews.end()) return found->second;
|
||||
const auto created = createView(mip, firstLayer, VK_FORMAT_R64_UINT);
|
||||
atomicViews.emplace(std::pair{mip, firstLayer}, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
const char* LookupOutcomes::Name(Kind kind) {
|
||||
static constexpr const char* names[Count] = {"sampled fast hit", "sampled fast miss", "sampled hit view", "sampled hit cleared view", "sampled hit snapshot", "sampled made view", "sampled made snapshot", "storage hit", "storage made", "refresh unchanged", "refresh compared", "upload direct", "upload cpu", "upload clear", "dcc scan", "pending flush"};
|
||||
return kind < Count ? names[kind] : "?";
|
||||
|
|
@ -3575,6 +3618,8 @@ void StorageTexture::release() noexcept {
|
|||
firstLayerViews.clear();
|
||||
for (const auto& [key, atomic] : atomicViews) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, atomic, nullptr);
|
||||
atomicViews.clear();
|
||||
for (const auto& [key, uint] : uintViews) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, uint, nullptr);
|
||||
uintViews.clear();
|
||||
for (const auto& [format, attachment] : attachmentViews) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, attachment, nullptr);
|
||||
attachmentViews.clear();
|
||||
if (view) context.Function<PFN_vkDestroyImageView>("vkDestroyImageView")(context.device, view, nullptr);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Context.hpp"
|
||||
#include "RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp"
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
|
|
@ -18,6 +21,7 @@ struct FormatEntry {
|
|||
constexpr FormatEntry kFormatLookup[] = {
|
||||
{1, VK_FORMAT_R8_UNORM, 1, false},
|
||||
{5, VK_FORMAT_R8_UINT, 1, false},
|
||||
{6, VK_FORMAT_R8_SINT, 1, false},
|
||||
{7, VK_FORMAT_R16_UNORM, 2, false},
|
||||
{8, VK_FORMAT_R16_SNORM, 2, false},
|
||||
{11, VK_FORMAT_R16_UINT, 2, false},
|
||||
|
|
@ -92,7 +96,7 @@ constexpr auto MakeFormatLookupTable() {
|
|||
constexpr auto kFormatLookupTable = MakeFormatLookupTable();
|
||||
|
||||
std::uint32_t remapGuestFormat(std::uint32_t guestFormat) {
|
||||
return guestFormat == 34 ? 20 : guestFormat;
|
||||
return guestFormat == 30 || guestFormat == 34 ? 20 : guestFormat;
|
||||
}
|
||||
|
||||
const FormatEntry& findFormatEntry(std::uint32_t guestFormat) {
|
||||
|
|
@ -108,6 +112,27 @@ VkFormat ResolveTextureFormat(std::uint32_t guestFormat) {
|
|||
return findFormatEntry(guestFormat).vkFormat;
|
||||
}
|
||||
|
||||
std::uint32_t SrgbDecodeFormats(PFN_vkGetPhysicalDeviceFormatProperties formatProperties, VkPhysicalDevice physical) {
|
||||
const char* forced = std::getenv("APS5_SRGB_SHADER_DECODE");
|
||||
const bool always = forced != nullptr && std::strcmp(forced, "1") == 0;
|
||||
const auto sampled = [&](ShaderRecompiler::IrBufferFormat format) {
|
||||
VkFormatProperties properties{};
|
||||
formatProperties(physical, ResolveTextureFormat(static_cast<std::uint32_t>(format)), &properties);
|
||||
return (properties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT) != 0;
|
||||
};
|
||||
std::uint32_t formats = 0;
|
||||
for (const auto format : {ShaderRecompiler::IrBufferFormat::Format8Srgb, ShaderRecompiler::IrBufferFormat::Format8_8Srgb}) {
|
||||
if ((always || !sampled(format)) && sampled(ShaderRecompiler::SrgbUnormFormat(format))) formats |= ShaderRecompiler::SrgbDecodeBit(format);
|
||||
}
|
||||
return formats;
|
||||
}
|
||||
|
||||
VkFormat SampledTextureFormat(const Context& context, std::uint32_t guestFormat) {
|
||||
const auto format = static_cast<ShaderRecompiler::IrBufferFormat>(guestFormat);
|
||||
if ((context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) return ResolveTextureFormat(guestFormat);
|
||||
return ResolveTextureFormat(static_cast<std::uint32_t>(ShaderRecompiler::SrgbUnormFormat(format)));
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes) {
|
||||
for (const auto& entry : kFormatLookup) {
|
||||
if (!entry.blockCompressed && entry.vkFormat == format && entry.bytesPerElement == elementBytes) return entry.guestFormat;
|
||||
|
|
@ -119,6 +144,10 @@ std::uint32_t BytesPerElement(std::uint32_t guestFormat) {
|
|||
return findFormatEntry(guestFormat).bytesPerElement;
|
||||
}
|
||||
|
||||
bool IsConvertedTextureFormat(std::uint32_t guestFormat) {
|
||||
return remapGuestFormat(guestFormat) != guestFormat;
|
||||
}
|
||||
|
||||
std::optional<std::uint32_t> FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes) {
|
||||
switch (format) {
|
||||
case VK_FORMAT_B8G8R8A8_UNORM: format = VK_FORMAT_R8G8B8A8_UNORM; break;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
// Resource registration feeds GPU debugging tools, none of which are attached; the game treats this code as benign.
|
||||
static constexpr int SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE = static_cast<int>(0x8A6C9018);
|
||||
static constexpr uint32_t RESOURCE_REGISTRATION_MAX_NAME_LENGTH = 0xfc;
|
||||
|
||||
extern "C" {
|
||||
|
||||
|
|
@ -69,6 +70,12 @@ int APS5_VABI sceAgcDriverGetDefaultOwner(uint32_t* owner_handle) {
|
|||
return SCE_AGC_ERROR_RESOURCE_REGISTRATION_UNAVAILABLE;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverGetResourceRegistrationMaxNameLength(uint32_t* max_length) {
|
||||
if (max_length == nullptr) APS5_INVALID_ARG_EX;
|
||||
*max_length = RESOURCE_REGISTRATION_MAX_NAME_LENGTH;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverGetOwnerName(uint32_t owner_handle, const char** name) {
|
||||
(void)owner_handle;
|
||||
(void)name;
|
||||
|
|
|
|||
|
|
@ -5,5 +5,7 @@
|
|||
|
||||
extern "C" int APS5_VABI sceAgcDriverSubmitDcb(const Packet* packet);
|
||||
extern "C" int APS5_VABI sceAgcDriverAgrSubmitDcb(const Packet* packet);
|
||||
extern "C" int APS5_VABI sceAgcDriverSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count);
|
||||
extern "C" int APS5_VABI sceAgcDriverAgrSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count);
|
||||
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -10,6 +10,18 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include "prx/libc/include/General.hpp"
|
||||
|
||||
static int SubmitMany(std::uint32_t* const* addresses, const std::uint32_t* sizes, std::uint32_t count) {
|
||||
if (count == 0) return 0;
|
||||
if (!addresses || !sizes) throw std::runtime_error("AGC driver: null multi-DCB arrays");
|
||||
AgcDriver::GuestMemory::CheckRange(addresses, count * sizeof(*addresses), alignof(std::uint32_t*));
|
||||
AgcDriver::GuestMemory::CheckRange(sizes, count * sizeof(*sizes), alignof(std::uint32_t));
|
||||
for (std::uint32_t i = 0; i < count; ++i) {
|
||||
const Packet packet{addresses[i], sizes[i], 0, {}};
|
||||
AgcDriver::Submit(&packet, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
std::uint32_t APS5_VABI sceAgcDriverGetWaitRenderingPacketSizeInDwords() {
|
||||
|
|
@ -33,12 +45,8 @@ int APS5_VABI sceAgcDriverSubmitDcb(const Packet* packet) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverSubmitMultiDcbs(uint32_t* const* dcb_gpu_addrs, const uint32_t* dcb_sizes_in_dwords, uint32_t count) {
|
||||
(void)dcb_gpu_addrs;
|
||||
(void)dcb_sizes_in_dwords;
|
||||
(void)count;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
int APS5_VABI sceAgcDriverSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count) {
|
||||
return SubmitMany(dcbGpuAddrs, dcbSizesInDwords, count);
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverAgrSubmitDcb(const Packet* packet) {
|
||||
|
|
@ -47,11 +55,7 @@ int APS5_VABI sceAgcDriverAgrSubmitDcb(const Packet* packet) {
|
|||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverAgrSubmitMultiDcbs(std::uint32_t* const* dcbGpuAddrs, const std::uint32_t* dcbSizesInDwords, std::uint32_t count) {
|
||||
(void)dcbGpuAddrs;
|
||||
(void)dcbSizesInDwords;
|
||||
(void)count;
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
return SubmitMany(dcbGpuAddrs, dcbSizesInDwords, count);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@
|
|||
|
||||
extern "C" {
|
||||
|
||||
int APS5_VABI sceAgcDriverGetResourceRegistrationMaxNameLength() {
|
||||
int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int APS5_VABI sceAgcDriverGetShaderDebuggingStatus() {
|
||||
int APS5_VABI sceAgcDriverRegisterMultipleResources() {
|
||||
NotImplemented_nid_no_patch(__func__);
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,39 @@ void RunBdaExecutionTests(const Context& context) {
|
|||
run(guest, 8, 0, Abi::FaultReason::InvalidTable, 3);
|
||||
run(std::numeric_limits<std::uint64_t>::max() - 2, 8, 0, Abi::FaultReason::Overflow, 2, 1, 4);
|
||||
run(1, 8, 0, Abi::FaultReason::Overflow, 2, 1, -4);
|
||||
Buffer words(context, 16, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT);
|
||||
const auto read = [&](std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops, std::uint32_t value, bool faults) {
|
||||
const Abi::Header header{Abi::Version, 2, sizeof(Abi::Range), 0};
|
||||
std::memcpy(table.Bytes().data(), &header, sizeof(header));
|
||||
std::memcpy(table.Bytes().data() + sizeof(header), ranges.data(), sizeof(ranges));
|
||||
std::memset(fault.Bytes().data(), 0, fault.Bytes().size());
|
||||
const std::array<std::uint32_t, 4> sentinel{0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef};
|
||||
std::memcpy(words.Bytes().data(), sentinel.data(), sizeof(sentinel));
|
||||
Pipeline pipeline(context, MakeBdaDwordReadTestShader(address, dwords, coherent, stops), {&table, &fault, &words});
|
||||
pipeline.Run(1);
|
||||
Abi::Fault report{};
|
||||
std::array<std::uint32_t, 4> result{};
|
||||
std::memcpy(&report, fault.Bytes().data(), sizeof(report));
|
||||
std::memcpy(result.data(), words.Bytes().data(), sizeof(result));
|
||||
auto expected = sentinel;
|
||||
if (!faults || !stops) {
|
||||
expected[0] = value;
|
||||
for (std::uint32_t dword = 1; dword < dwords; ++dword) expected[dword] = 0;
|
||||
}
|
||||
if (faults) {
|
||||
Require(report.state == Abi::FaultState::Ready && report.reason == Abi::FaultReason::Unmapped && report.address == guest + 5 && report.bytes == 1 && report.instruction == 0x1234, "BDA dword read did not publish its first fault");
|
||||
} else {
|
||||
Require(report.state == Abi::FaultState::Empty, "mapped BDA dword read published a fault");
|
||||
}
|
||||
Require(result == expected, faults && stops ? "faulting BDA dword read continued to output a substitute value" : "BDA dword read produced incorrect data");
|
||||
};
|
||||
for (const bool stops : {true, false}) {
|
||||
for (const bool coherent : {false, true}) {
|
||||
read(guest + 1, 1, coherent, stops, 0x55443322, false);
|
||||
read(guest + 3, 1, coherent, stops, 0x5544, true);
|
||||
read(guest + 1, 4, coherent, stops, 0x55443322, true);
|
||||
}
|
||||
}
|
||||
ranges[0].permissions = 0;
|
||||
run(guest, 8, 0, Abi::FaultReason::Permission);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,19 @@
|
|||
#include "SpirvBackend/SpirvMemory/SpirvTypes.hpp"
|
||||
#include "SpirvBackend/SpirvMemory/SpirvConstants.hpp"
|
||||
|
||||
std::vector<std::uint32_t> MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset) {
|
||||
namespace {
|
||||
|
||||
template<typename TEmit>
|
||||
std::vector<std::uint32_t> MakeShader(std::uint64_t address, bool coherent, bool stops, const TEmit& emit) {
|
||||
using namespace ShaderRecompiler;
|
||||
IrProgram program;
|
||||
program.Resources().stage = IrShaderStage::Compute;
|
||||
program.Info().usesDma = true;
|
||||
if (coherent) {
|
||||
MemoryInfo memory;
|
||||
memory.coherent = true;
|
||||
program.Resources().memoryInfo = {memory};
|
||||
}
|
||||
SpirvEmitterState state(program, {});
|
||||
EmitBaseHeader(state.module, program);
|
||||
const auto define = [&](std::uint32_t binding) {
|
||||
|
|
@ -19,6 +27,12 @@ std::vector<std::uint32_t> MakeBdaTestShader(std::uint64_t address, std::uint32_
|
|||
state.bdaPagetableVariable = define(0);
|
||||
state.faultBufferVariable = define(1);
|
||||
const auto output = define(2);
|
||||
std::vector<std::uint32_t> interfaceVariables;
|
||||
if (!stops) {
|
||||
interfaceVariables.push_back(state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassInput, TypeU32(state)), spv::StorageClassInput));
|
||||
state.module.AddAnnotation(spv::OpDecorate, interfaceVariables[0], spv::DecorationBuiltIn, spv::BuiltInLocalInvocationIndex);
|
||||
}
|
||||
state.bdaStopsInvocations = stops;
|
||||
DefineGetBdaPointer(state);
|
||||
const auto main = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpFunction, TypeVoid(state), main, spv::FunctionControlMaskNone, TypeFunction(state));
|
||||
|
|
@ -29,12 +43,34 @@ std::vector<std::uint32_t> MakeBdaTestShader(std::uint64_t address, std::uint32_
|
|||
flags.pc = 0x1234;
|
||||
instruction.SetFlags(flags);
|
||||
auto base = BdaConstant(state, address);
|
||||
if (offset != 0) base = AddBdaAddress(ctx, instruction, base, BdaConstant(state, offset < 0 ? std::uint64_t{0} - static_cast<std::uint64_t>(offset) : static_cast<std::uint64_t>(offset)), offset < 0);
|
||||
const auto value = EmitBdaRead(ctx, instruction, base, bits);
|
||||
state.module.AddFunction(spv::OpStore, BdaWord(state, output, ConstantU32(state, 0u)), value);
|
||||
if (!stops) {
|
||||
const auto index = state.module.AllocateId();
|
||||
state.module.AddFunction(spv::OpLoad, TypeU32(state), index, interfaceVariables[0]);
|
||||
base = Binary(state, spv::OpIAdd, TypeScalarU64(state), base, Unary(state, spv::OpUConvert, TypeScalarU64(state), index));
|
||||
}
|
||||
const auto values = emit(ctx, instruction, base);
|
||||
for (std::uint32_t index = 0; index < values.size(); ++index) state.module.AddFunction(spv::OpStore, BdaWord(state, output, ConstantU32(state, index)), values[index]);
|
||||
state.module.AddFunction(spv::OpReturn);
|
||||
state.module.AddFunction(spv::OpFunctionEnd);
|
||||
state.module.AddExecutionMode(main, spv::ExecutionModeLocalSize, 1u, 1u, 1u);
|
||||
state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", {});
|
||||
state.module.EmitEntryPoint(spv::ExecutionModelGLCompute, main, "main", interfaceVariables);
|
||||
return state.module.Finalize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset) {
|
||||
using namespace ShaderRecompiler;
|
||||
return MakeShader(address, false, true, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) {
|
||||
if (offset != 0) base = AddBdaAddress(ctx, instruction, base, BdaConstant(ctx.state, offset < 0 ? std::uint64_t{0} - static_cast<std::uint64_t>(offset) : static_cast<std::uint64_t>(offset)), offset < 0);
|
||||
return std::vector<std::uint32_t>{EmitBdaRead(ctx, instruction, base, bits)};
|
||||
});
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops) {
|
||||
using namespace ShaderRecompiler;
|
||||
return MakeShader(address, coherent, stops, [&](SpirvValueEmitContext& ctx, const IrValue& instruction, std::uint32_t base) {
|
||||
const auto values = EmitBdaDwordReads(ctx, instruction, base, 0u, dwords, true);
|
||||
return std::vector<std::uint32_t>(values.begin(), values.begin() + dwords);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
#include <vector>
|
||||
|
||||
std::vector<std::uint32_t> MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset = 0);
|
||||
std::vector<std::uint32_t> MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops);
|
||||
void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context);
|
||||
void RunBdaContractTests();
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,13 @@ std::vector<std::uint32_t> constructBlocks(const ControlFlowGraph& graph, std::u
|
|||
return blocks;
|
||||
}
|
||||
|
||||
bool inEnclosingContinueConstruct(const ControlFlowGraph& graph, std::uint32_t header, std::uint32_t member) {
|
||||
return std::any_of(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& loop) {
|
||||
const auto continueBlock = loop.terminator.continueBlock;
|
||||
return loop.terminator.loopHeader && graph.Dominates(loop.id, header) && !graph.Dominates(continueBlock, header) && graph.Dominates(continueBlock, member);
|
||||
});
|
||||
}
|
||||
|
||||
void verifyStructured(const std::string& prefix, const ControlFlowGraph& graph) {
|
||||
std::map<std::uint32_t, std::uint32_t> mergeOwners;
|
||||
std::vector<std::pair<std::uint32_t, std::uint32_t>> loopExits;
|
||||
|
|
@ -66,6 +73,7 @@ void verifyStructured(const std::string& prefix, const ControlFlowGraph& graph)
|
|||
const auto& terminator = block.terminator;
|
||||
if (terminator.loopHeader || terminator.mergeBlock == InvalidControlFlowId) continue;
|
||||
for (const auto member : constructBlocks(graph, block.id, terminator.mergeBlock)) {
|
||||
if (inEnclosingContinueConstruct(graph, block.id, member)) continue;
|
||||
for (const auto successor : graph.FindBlock(member).successors) {
|
||||
if (successor == terminator.mergeBlock || (graph.Dominates(block.id, successor) && !graph.Dominates(terminator.mergeBlock, successor))) continue;
|
||||
const bool loopExit = std::any_of(loopExits.begin(), loopExits.end(), [&](const auto& exits) { return successor == exits.first || successor == exits.second; });
|
||||
|
|
@ -363,6 +371,29 @@ latch:
|
|||
buffer_store_dword v1, off, s[0:3], 0
|
||||
s_endpgm)",
|
||||
Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860004u, 0x7d880088u, 0xbf870003u, 0x4a020281u, 0xbf820002u, 0x4a020282u, 0x4a020283u, 0x80089008u, 0xbf0ac008u, 0xbf85fff5u}), Split::Clone},
|
||||
{"continue beside the inner merge of a nested selection in a loop", R"(
|
||||
s_mov_b32 s8, 0
|
||||
v_mov_b32 v1, 0
|
||||
loop:
|
||||
v_cmp_gt_u32 vcc, s8, v0
|
||||
s_cbranch_vccz join
|
||||
v_cmp_gt_u32 vcc, 8, v0
|
||||
s_cbranch_vccnz inner
|
||||
v_cmp_gt_u32 vcc, 4, v0
|
||||
s_cbranch_vccnz latch
|
||||
inner:
|
||||
v_add_nc_u32 v1, 1, v1
|
||||
join:
|
||||
v_add_nc_u32 v1, 2, v1
|
||||
s_cmp_ge_u32 s8, 64
|
||||
s_cbranch_scc1 done
|
||||
latch:
|
||||
s_add_u32 s8, s8, 16
|
||||
s_branch loop
|
||||
done:
|
||||
buffer_store_dword v1, off, s[0:3], 0
|
||||
s_endpgm)",
|
||||
Store({0xbe880380u, 0x7e020280u, 0x7d880008u, 0xbf860005u, 0x7d880088u, 0xbf870002u, 0x7d880084u, 0xbf870004u, 0x4a020281u, 0x4a020282u, 0xbf09c008u, 0xbf850002u, 0x80089008u, 0xbf82fff4u}), Split::None},
|
||||
{"loop exit to the end of the program beside a kill exit", R"(
|
||||
v_mov_b32 v1, 0
|
||||
s_mov_b64 s[20:21], exec
|
||||
|
|
|
|||
|
|
@ -108,6 +108,13 @@ void stateTests() {
|
|||
queue.context[0x103] = 5;
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, true).find("all ones") != std::string::npos, "a restart index other than all ones was accepted");
|
||||
queue = makeState();
|
||||
queue.context[0x293] = 0x06020000u;
|
||||
(void)AgcDriver::Graphics::DecodeState(queue);
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") == std::string::npos, "per-engine primitive discard was rejected");
|
||||
queue.context[0x293] = 0x06030000u;
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, false).find("sample iteration") != std::string::npos, "per-sample shading was accepted");
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "sample iteration");
|
||||
queue = makeState();
|
||||
queue.userConfig.erase(0x24b);
|
||||
queue.context[0x2a5] = 0;
|
||||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "user-config bank at DWORD 0x24b");
|
||||
|
|
@ -575,6 +582,35 @@ void DepthStencilTests() {
|
|||
expectFailure([&] { AgcDriver::Graphics::DecodeState(queue); }, "DCC 3D color targets");
|
||||
}
|
||||
|
||||
// SPI_SHADER_Z_FORMAT (0x1c4) and the export enables of DB_SHADER_CONTROL (0x203): Z export needs a
|
||||
// format with a depth channel (1, 2, 3 or 32_ABGR 9), the sample mask needs 32_ABGR, and the
|
||||
// formats the export path does not lay out are refused. A missing 0x1c4 gives no verdict here.
|
||||
void ZExportTests() {
|
||||
constexpr std::uint32_t zExportEnable = 0x1u;
|
||||
constexpr std::uint32_t maskExportEnable = 0x100u;
|
||||
const auto withExports = [](std::uint32_t format, std::uint32_t exports) {
|
||||
auto queue = makeState();
|
||||
queue.context[0x1b3] = 2;
|
||||
queue.context[0x1b4] = 2;
|
||||
queue.context[0x1c4] = format;
|
||||
queue.context[0x203] = 0x800u | exports;
|
||||
return queue;
|
||||
};
|
||||
for (const std::uint32_t format : {1u, 2u, 3u, 9u}) {
|
||||
const auto queue = withExports(format, zExportEnable);
|
||||
Require(AgcDriver::Graphics::DrawRejection(queue, true).empty(), "a Z export with format " + std::to_string(format) + " was rejected");
|
||||
}
|
||||
Require(!AgcDriver::Graphics::DrawRejection(withExports(0, zExportEnable), true).empty(), "a Z export without a Z format was accepted");
|
||||
Require(AgcDriver::Graphics::DrawRejection(withExports(9, zExportEnable | maskExportEnable), true).empty(), "a sample-mask export with 32_ABGR was rejected");
|
||||
Require(!AgcDriver::Graphics::DrawRejection(withExports(1, zExportEnable | maskExportEnable), true).empty(), "a sample-mask export with 32_R was accepted");
|
||||
for (std::uint32_t format = 4; format <= 8; ++format) {
|
||||
Require(!AgcDriver::Graphics::DrawRejection(withExports(format, 0), true).empty(), "Z format " + std::to_string(format) + " was accepted");
|
||||
}
|
||||
auto absent = withExports(0, 0);
|
||||
absent.context.erase(0x1c4);
|
||||
Require(AgcDriver::Graphics::DrawRejection(absent, true).empty(), "a missing SPI_SHADER_Z_FORMAT was rejected (or threw) in the precheck");
|
||||
}
|
||||
|
||||
void DepthBoundsBiasTests() {
|
||||
const auto bits = [](float value) {
|
||||
std::uint32_t word = 0;
|
||||
|
|
@ -1268,6 +1304,27 @@ void resourceTests() {
|
|||
expectSingleAccepted(changed([](auto& binding) { binding.count = 3; binding.guestDescriptor = join(join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32)), vsharp(reinterpret_cast<const void*>(0x1000), 8)); }), "an unmapped V# element");
|
||||
expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast<const void*>(state.color.address), 64)); }), "aliases the render target");
|
||||
expectSingleFailure(changed([](auto& binding) { binding.count = 17; binding.guestDescriptor.assign(68, 0); }), "per-stage limits");
|
||||
{
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
ShaderRecompiler::RecompileResult fragment;
|
||||
vertex.bindings.push_back(makeBinding(Role::GuestBuffers, 0, 2, join(vsharp(guestFirst.data(), 16), vsharp(guestSecond.data(), 32))));
|
||||
vertex.bindings.push_back(makeBinding(Role::GuestImages, 1, 1, std::vector<std::uint32_t>(8, 0)));
|
||||
const auto expectStageResources = [&](Role role, Kind kind, std::uint32_t words, std::uint32_t limit, std::string_view reason) {
|
||||
vertex.bindings.back().role = role;
|
||||
vertex.bindings.back().kind = kind;
|
||||
vertex.bindings.back().guestDescriptor.assign(words, 0);
|
||||
mock = MockVulkan{};
|
||||
auto limited = mockContext();
|
||||
limited.limits.maxPerStageResources = limit;
|
||||
expectFailure([&] { AgcDriver::Graphics::ShaderResources resources(limited, vertex, fragment, state.color, 0, 0); }, reason);
|
||||
Require(mock.live == 0, "failed shader resources leaked Vulkan objects");
|
||||
};
|
||||
expectStageResources(Role::GuestImages, Kind::SampledImage, 8, 2, "shader descriptors exceed per-stage limits");
|
||||
expectStageResources(Role::GuestImages, Kind::StorageImage, 8, 2, "shader descriptors exceed per-stage limits");
|
||||
expectStageResources(Role::GuestImages, Kind::SampledImage, 8, 3, "missing an image shape");
|
||||
expectStageResources(Role::GuestImages, Kind::StorageImage, 8, 3, "detiler is unavailable");
|
||||
expectStageResources(Role::GuestSamplers, Kind::Sampler, 4, 2, "shader sampler descriptors exceed per-stage limits");
|
||||
}
|
||||
{
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
ShaderRecompiler::RecompileResult fragment;
|
||||
|
|
@ -1797,6 +1854,15 @@ void validationTests() {
|
|||
attribute.resource.fields[1] |= 0x80000000u;
|
||||
expectFailure([&] { AgcDriver::Graphics::BuildVertexInputLayout(context, std::span(&attribute, 1)); }, "descriptor flags");
|
||||
}
|
||||
for (const auto capability : {spv::CapabilityInt64Atomics, spv::CapabilityInt64ImageEXT}) {
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
vertex.spirv = makeModule({});
|
||||
vertex.spirv.insert(vertex.spirv.begin() + 5, {(2u << 16u) | spv::OpCapability, static_cast<std::uint32_t>(capability)});
|
||||
const std::array<AgcDriver::Graphics::CompiledShader, 2> shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &fragment, 0}}};
|
||||
const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES};
|
||||
expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported device capability");
|
||||
AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false, false, true);
|
||||
}
|
||||
for (const auto capability : {spv::CapabilityGroupNonUniform, spv::CapabilityGroupNonUniformBallot, spv::CapabilityGroupNonUniformShuffle}) {
|
||||
ShaderRecompiler::RecompileResult vertex;
|
||||
vertex.spirv = makeModule({});
|
||||
|
|
@ -2025,6 +2091,7 @@ int main() {
|
|||
hardwareScreenOffsetTests();
|
||||
DepthClipTests();
|
||||
DepthStencilTests();
|
||||
ZExportTests();
|
||||
DepthBoundsBiasTests();
|
||||
conservativeZExportTests();
|
||||
DisabledColorTests();
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
#include "GraphicsTests.hpp"
|
||||
#include "Optimization/DescriptorBindingBuilder.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp"
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
namespace {
|
||||
|
||||
|
|
@ -69,10 +74,153 @@ void rejectFields(const Fields& f, std::string_view reason) {
|
|||
reject([&] { DecodeSamplerResource(words); }, reason);
|
||||
}
|
||||
|
||||
void rejectUnnormalized(const Fields& f, std::string_view reason) {
|
||||
const auto words = pack(f);
|
||||
reject([&] { DecodeSamplerResource(words, true); }, reason);
|
||||
}
|
||||
|
||||
void requireUnnormalized(const GuestSamplerResource& result, const char* what) {
|
||||
Require(result.unnormalizedCoordinates, std::string(what) + ": unnormalized coordinates were not decoded");
|
||||
Require(result.mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST && result.minLod == 0.0f && result.maxLod == 0.0f, std::string(what) + ": unnormalized coordinates need nearest mips and a zero LOD range");
|
||||
Require(result.lodBias == 0.0f && !result.anisotropyEnable && result.maxAnisotropy == 1.0f, std::string(what) + ": unnormalized coordinates need no LOD bias and no anisotropy");
|
||||
}
|
||||
|
||||
bool nearlyEqual(float a, float b) {
|
||||
return std::fabs(a - b) < 0.001f;
|
||||
}
|
||||
|
||||
struct SamplerCapture {
|
||||
bool created = false;
|
||||
const void* next = nullptr;
|
||||
VkSamplerReductionMode reductionMode = VK_SAMPLER_REDUCTION_MODE_MAX_ENUM;
|
||||
};
|
||||
SamplerCapture samplerCapture;
|
||||
|
||||
template<typename THandle>
|
||||
THandle fakeHandle(std::uint64_t value) {
|
||||
if constexpr (std::is_pointer_v<THandle>) return reinterpret_cast<THandle>(static_cast<std::uintptr_t>(value));
|
||||
else return static_cast<THandle>(value);
|
||||
}
|
||||
|
||||
VKAPI_ATTR VkResult VKAPI_CALL captureCreateSampler(VkDevice, const VkSamplerCreateInfo* info, const VkAllocationCallbacks*, VkSampler* sampler) {
|
||||
samplerCapture.created = true;
|
||||
samplerCapture.next = info->pNext;
|
||||
const auto* reduction = static_cast<const VkSamplerReductionModeCreateInfoEXT*>(info->pNext);
|
||||
if (reduction != nullptr && reduction->sType == VK_STRUCTURE_TYPE_SAMPLER_REDUCTION_MODE_CREATE_INFO_EXT && reduction->pNext == nullptr) samplerCapture.reductionMode = reduction->reductionMode;
|
||||
*sampler = fakeHandle<VkSampler>(0x5a);
|
||||
return VK_SUCCESS;
|
||||
}
|
||||
|
||||
VKAPI_ATTR void VKAPI_CALL captureDestroySampler(VkDevice, VkSampler, const VkAllocationCallbacks*) {}
|
||||
|
||||
PFN_vkVoidFunction VKAPI_CALL captureProc(VkDevice, const char* name) {
|
||||
if (std::strcmp(name, "vkCreateSampler") == 0) return reinterpret_cast<PFN_vkVoidFunction>(captureCreateSampler);
|
||||
if (std::strcmp(name, "vkDestroySampler") == 0) return reinterpret_cast<PFN_vkVoidFunction>(captureDestroySampler);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
SamplerCapture createSampler(const Context& context, const GuestSamplerResource& resource) {
|
||||
samplerCapture = {};
|
||||
const Sampler sampler(context, resource);
|
||||
Require(sampler.Handle() != VK_NULL_HANDLE, "sampler test did not create a sampler");
|
||||
return samplerCapture;
|
||||
}
|
||||
|
||||
void RunSamplerReductionTests(const Fields& base) {
|
||||
Context context{};
|
||||
context.limits.maxSamplerAnisotropy = 1.0f;
|
||||
context.deviceProc = captureProc;
|
||||
context.samplerFilterMinmax = true;
|
||||
|
||||
const std::array<std::uint32_t, 4> capturedSampler{0x20000092u, 0x00fff000u, 0x05500000u, 0u};
|
||||
const auto captured = DecodeSamplerResource(capturedSampler);
|
||||
Require(captured.reductionMode == VK_SAMPLER_REDUCTION_MODE_MIN_EXT, "captured min-reduction sampler decoded to another reduction mode");
|
||||
Require(captured.magFilter == VK_FILTER_LINEAR && captured.minFilter == VK_FILTER_LINEAR && captured.mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST, "captured min-reduction sampler filters decoded incorrectly");
|
||||
Require(captured.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && captured.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && captured.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured min-reduction sampler address modes decoded incorrectly");
|
||||
Require(!captured.anisotropyEnable && captured.compareOp == VK_COMPARE_OP_NEVER && nearlyEqual(captured.maxLod, 4095.0f / 256.0f), "captured min-reduction sampler fields decoded incorrectly");
|
||||
|
||||
const std::array modes{VK_SAMPLER_REDUCTION_MODE_WEIGHTED_AVERAGE_EXT, VK_SAMPLER_REDUCTION_MODE_MIN_EXT, VK_SAMPLER_REDUCTION_MODE_MAX_EXT};
|
||||
for (std::uint32_t mode = 0; mode < modes.size(); ++mode) {
|
||||
for (const std::uint32_t filter : {0u, 1u}) {
|
||||
Fields reduced = base;
|
||||
reduced.filterMode = mode;
|
||||
reduced.xyMagFilter = filter;
|
||||
reduced.xyMinFilter = filter;
|
||||
reduced.mipFilter = 1;
|
||||
const auto decoded = DecodeSamplerResource(pack(reduced));
|
||||
Require(decoded.reductionMode == modes.at(mode), "sampler reduction mode decoded incorrectly");
|
||||
const auto created = createSampler(context, decoded);
|
||||
Require(created.created, "sampler test did not reach vkCreateSampler");
|
||||
if (mode == 0u) Require(created.next == nullptr, "a weighted-average sampler chained a reduction mode");
|
||||
else Require(created.reductionMode == modes.at(mode), "a min or max sampler did not chain its reduction mode");
|
||||
const Sampler sampler(context, decoded);
|
||||
Require(sampler.RequiresFilterMinmax() == (mode != 0u && filter == 1u), "sampler min/max format requirement is wrong");
|
||||
}
|
||||
}
|
||||
Fields noMip = base;
|
||||
noMip.filterMode = 2;
|
||||
noMip.mipFilter = 0;
|
||||
Require(DecodeSamplerResource(pack(noMip)).reductionMode == VK_SAMPLER_REDUCTION_MODE_MAX_EXT, "a max sampler without mip filtering was not decoded");
|
||||
Fields linearMip = base;
|
||||
linearMip.filterMode = 1;
|
||||
rejectFields(linearMip, "min or max reduction with a linear mip filter");
|
||||
linearMip.xyMagFilter = 0;
|
||||
linearMip.xyMinFilter = 0;
|
||||
rejectFields(linearMip, "min or max reduction with a linear mip filter");
|
||||
|
||||
Fields badFilterMode = base;
|
||||
badFilterMode.filterMode = 3;
|
||||
rejectFields(badFilterMode, "unknown reduction filter mode 3");
|
||||
Fields anisoReduction = base;
|
||||
anisoReduction.filterMode = 1;
|
||||
anisoReduction.xyMagFilter = 3;
|
||||
anisoReduction.xyMinFilter = 3;
|
||||
anisoReduction.maxAnisoRatio = 2;
|
||||
anisoReduction.mipFilter = 1;
|
||||
rejectFields(anisoReduction, "min or max reduction with anisotropic filtering");
|
||||
anisoReduction.filterMode = 0;
|
||||
Require(DecodeSamplerResource(pack(anisoReduction)).anisotropyEnable, "anisotropic weighted-average sampler was rejected");
|
||||
|
||||
auto compared = captured;
|
||||
compared.compareEnable = true;
|
||||
reject([&] { createSampler(context, compared); }, "min or max reduction with depth comparison");
|
||||
auto weightedCompare = DecodeSamplerResource(pack(base));
|
||||
weightedCompare.compareEnable = true;
|
||||
Require(createSampler(context, weightedCompare).created, "weighted-average comparison sampler was rejected");
|
||||
context.samplerFilterMinmax = false;
|
||||
reject([&] { createSampler(context, captured); }, "min or max reduction which the device does not support");
|
||||
Require(createSampler(context, DecodeSamplerResource(pack(base))).created, "weighted-average sampler needs min/max support");
|
||||
|
||||
context.formatProperties = [](VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) {
|
||||
*properties = {};
|
||||
if (format == VK_FORMAT_R32_SFLOAT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT;
|
||||
if (format == VK_FORMAT_R32G32_SFLOAT) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
|
||||
if (format == VK_FORMAT_R8G8B8A8_UNORM) properties->linearTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_MINMAX_BIT_EXT;
|
||||
};
|
||||
context.samplerFilterMinmax = true;
|
||||
Fields minPoint = base;
|
||||
minPoint.filterMode = 1;
|
||||
minPoint.xyMagFilter = 0;
|
||||
minPoint.xyMinFilter = 0;
|
||||
minPoint.mipFilter = 1;
|
||||
const std::array samplers{std::make_shared<Sampler>(context, DecodeSamplerResource(pack(base))), std::make_shared<Sampler>(context, captured), std::make_shared<Sampler>(context, DecodeSamplerResource(pack(minPoint)))};
|
||||
RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b010u, samplers);
|
||||
RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0u, samplers);
|
||||
RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0b101u, samplers);
|
||||
reject([&] { RequireFilterMinmax(context, VK_FORMAT_R8G8B8A8_UNORM, 0b010u, samplers); }, "format 37 does not support min/max filtering is sampled through a min or max reduction sampler");
|
||||
reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32G32_SFLOAT, 0b011u, samplers); }, "does not support min/max filtering");
|
||||
reject([&] { RequireFilterMinmax(context, VK_FORMAT_R32_SFLOAT, 0b1000u, samplers); }, "sampler element 3, which its shader does not bind");
|
||||
|
||||
const auto pointFiltered = [](const std::array<std::uint32_t, 4>& words) { return ShaderRecompiler::PointFilteredSamplerWord(words[0], words[2]); };
|
||||
Require(pointFiltered(pack(base)) == 0x05000000u, "a point-filtered weighted-average sampler kept its bilinear or linear mip filter");
|
||||
Require(pointFiltered(pack(minPoint)) == pack(minPoint)[2], "a point-filtered min sampler that already point-samples changed");
|
||||
reject([&] { pointFiltered(capturedSampler); }, "needs point filtering");
|
||||
Fields maxLinearMip = minPoint;
|
||||
maxLinearMip.filterMode = 2;
|
||||
maxLinearMip.mipFilter = 2;
|
||||
reject([&] { pointFiltered(pack(maxLinearMip)); }, "needs point filtering");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RunGuestSamplerResourceTests() {
|
||||
|
|
@ -149,6 +297,69 @@ void RunGuestSamplerResourceTests() {
|
|||
badUnorm.forceUnormCoords = true;
|
||||
rejectFields(badUnorm, "unnormalized coordinates");
|
||||
|
||||
const std::array<std::uint32_t, 4> capturedUnnormalized{0x00008092u, 0x00fff000u, 0x05500000u, 0u};
|
||||
reject([&] { DecodeSamplerResource(capturedUnnormalized); }, "uses unnormalized coordinates which are not implemented");
|
||||
const auto unnormalized = DecodeSamplerResource(capturedUnnormalized, true);
|
||||
requireUnnormalized(unnormalized, "captured unnormalized S#");
|
||||
Require(unnormalized.magFilter == VK_FILTER_LINEAR && unnormalized.minFilter == VK_FILTER_LINEAR, "captured unnormalized S# filters decoded incorrectly");
|
||||
Require(unnormalized.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && unnormalized.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured unnormalized S# clamp modes decoded incorrectly");
|
||||
Require(unnormalized.borderColor == VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, "captured unnormalized S# border color decoded incorrectly");
|
||||
Require(!DecodeSamplerResource(pack(base)).unnormalizedCoordinates, "a normalized S# decoded to unnormalized coordinates");
|
||||
|
||||
Fields unnormalizedBase = base;
|
||||
unnormalizedBase.forceUnormCoords = true;
|
||||
Fields unnormalizedPoint = unnormalizedBase;
|
||||
unnormalizedPoint.xyMagFilter = 0;
|
||||
unnormalizedPoint.xyMinFilter = 0;
|
||||
const auto point = DecodeSamplerResource(pack(unnormalizedPoint), true);
|
||||
requireUnnormalized(point, "unnormalized point S#");
|
||||
Require(point.magFilter == VK_FILTER_NEAREST && point.minFilter == VK_FILTER_NEAREST, "unnormalized point S# filters decoded incorrectly");
|
||||
const std::array borders{VK_BORDER_COLOR_INT_TRANSPARENT_BLACK, VK_BORDER_COLOR_INT_OPAQUE_BLACK, VK_BORDER_COLOR_INT_OPAQUE_WHITE};
|
||||
for (std::uint32_t type = 0; type < borders.size(); ++type) {
|
||||
Fields border = unnormalizedBase;
|
||||
border.clampX = 6;
|
||||
border.clampY = 6;
|
||||
border.borderColorType = type;
|
||||
const auto decoded = DecodeSamplerResource(pack(border), true);
|
||||
requireUnnormalized(decoded, "unnormalized clamp-to-border S#");
|
||||
Require(decoded.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER && decoded.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER && decoded.borderColor == borders[type], "unnormalized clamp-to-border S# decoded incorrectly");
|
||||
}
|
||||
for (std::uint32_t mipFilter = 0; mipFilter < 3; ++mipFilter) {
|
||||
Fields mips = unnormalizedBase;
|
||||
mips.mipFilter = mipFilter;
|
||||
requireUnnormalized(DecodeSamplerResource(pack(mips), true), "unnormalized S# mip filter");
|
||||
}
|
||||
Fields unnormalizedBias = unnormalizedBase;
|
||||
unnormalizedBias.lodBiasRaw = 256;
|
||||
requireUnnormalized(DecodeSamplerResource(pack(unnormalizedBias), true), "unnormalized S# with a LOD bias");
|
||||
Fields unnormalizedMinLod = unnormalizedBase;
|
||||
unnormalizedMinLod.minLodRaw = 0x100;
|
||||
unnormalizedMinLod.maxLodRaw = 0x400;
|
||||
requireUnnormalized(DecodeSamplerResource(pack(unnormalizedMinLod), true), "unnormalized S# with a minimum LOD");
|
||||
|
||||
Fields unequalFilters = unnormalizedBase;
|
||||
unequalFilters.xyMinFilter = 0;
|
||||
rejectUnnormalized(unequalFilters, "unnormalized coordinates with different minification and magnification filters");
|
||||
Fields unnormalizedAniso = unnormalizedBase;
|
||||
unnormalizedAniso.xyMagFilter = 2;
|
||||
unnormalizedAniso.xyMinFilter = 2;
|
||||
rejectUnnormalized(unnormalizedAniso, "unnormalized coordinates with anisotropic filtering");
|
||||
for (const std::uint32_t clamp : {0u, 1u, 3u, 4u, 5u, 7u}) {
|
||||
Fields clampX = unnormalizedBase;
|
||||
clampX.clampX = clamp;
|
||||
rejectUnnormalized(clampX, "unnormalized coordinates with clamp mode " + std::to_string(clamp) + " on X");
|
||||
}
|
||||
Fields clampY = unnormalizedBase;
|
||||
clampY.clampY = 0;
|
||||
rejectUnnormalized(clampY, "unnormalized coordinates with clamp mode 0 on Y");
|
||||
Fields unnormalizedTruncated = unnormalizedBase;
|
||||
unnormalizedTruncated.truncCoord = true;
|
||||
rejectUnnormalized(unnormalizedTruncated, "unnormalized coordinates with TRUNC_COORD");
|
||||
auto truncatedBlend = pack(unnormalizedBase);
|
||||
truncatedBlend[0] |= 1u << 19u;
|
||||
reject([&] { DecodeSamplerResource(truncatedBlend, true); }, "unnormalized coordinates with MC_COORD_TRUNC");
|
||||
rejectUnnormalized(base, "bound as unnormalized without FORCE_UNNORMALIZED");
|
||||
|
||||
Fields badSrgb = base;
|
||||
badSrgb.forceSrgb = true;
|
||||
rejectFields(badSrgb, "forces sRGB decoding");
|
||||
|
|
@ -165,9 +376,7 @@ void RunGuestSamplerResourceTests() {
|
|||
badCubeWrap.disableCubeWrap = true;
|
||||
rejectFields(badCubeWrap, "seamless cube filtering");
|
||||
|
||||
Fields badFilterMode = base;
|
||||
badFilterMode.filterMode = 1;
|
||||
rejectFields(badFilterMode, "reduction filter mode");
|
||||
RunSamplerReductionTests(base);
|
||||
|
||||
Fields badDegamma = base;
|
||||
badDegamma.disableDegamma = true;
|
||||
|
|
@ -189,9 +398,28 @@ void RunGuestSamplerResourceTests() {
|
|||
badBlendZero.blendZeroPrt = true;
|
||||
rejectFields(badBlendZero, "PRT blend-zero");
|
||||
|
||||
Fields badBorder = base;
|
||||
badBorder.borderColorType = 3;
|
||||
rejectFields(badBorder, "border color table");
|
||||
Fields unusedTable = base;
|
||||
unusedTable.borderColorType = 3;
|
||||
const auto unread = DecodeSamplerResource(pack(unusedTable));
|
||||
Require(unread.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && unread.addressModeV == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && unread.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
||||
"a table border colour with clamp-to-edge addressing must decode");
|
||||
for (std::uint32_t mode : {0u, 1u, 2u, 3u}) {
|
||||
Fields unreadMode = base;
|
||||
unreadMode.borderColorType = 3;
|
||||
unreadMode.clampX = mode;
|
||||
DecodeSamplerResource(pack(unreadMode));
|
||||
}
|
||||
for (std::uint32_t mode : {4u, 5u, 6u, 7u}) {
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
Fields readTable = base;
|
||||
readTable.borderColorType = 3;
|
||||
(axis == 0 ? readTable.clampX : axis == 1 ? readTable.clampY : readTable.clampZ) = mode;
|
||||
rejectFields(readTable, "border color table");
|
||||
}
|
||||
}
|
||||
const std::array<std::uint32_t, 4> capturedTable{0x00007092u, 0x00fff000u, 0x05000000u, 0xc0000000u};
|
||||
const auto capturedUnread = DecodeSamplerResource(capturedTable);
|
||||
Require(capturedUnread.addressModeU == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE && capturedUnread.addressModeW == VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, "captured table-border sampler decoded incorrectly");
|
||||
|
||||
Fields opaqueBlack = base;
|
||||
opaqueBlack.borderColorType = 1;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Pm4.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
|
|
@ -12,6 +13,7 @@
|
|||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
|
|
@ -59,7 +61,7 @@ void testCatalog() {
|
|||
const auto reason = AgcDriver::Pm4::UnsupportedReason(packet[0]);
|
||||
if (!reason.empty()) expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, std::string(reason).c_str());
|
||||
}
|
||||
check(values.size() == 54, "reference opcode catalog is incomplete");
|
||||
check(values.size() == 55, "reference opcode catalog is incomplete");
|
||||
expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0xff, {0}), 0); }, "not known");
|
||||
const std::array<std::pair<std::uint32_t, const char*>, 11> custom{{
|
||||
{5, "DRAW_RESET"}, {6, "WAIT_FLIP_DONE"}, {9, "DISPATCH_RESET"}, {11, "PUSH_MARKER"},
|
||||
|
|
@ -737,6 +739,41 @@ void testConditionalSubmission() {
|
|||
check(results[13] == 0, "a rejected conditional submission executed a guarded packet");
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> branch(std::uint32_t mode, std::uint32_t function, const std::vector<std::uint32_t>* first, const std::vector<std::uint32_t>* second) {
|
||||
const auto address = [](const std::vector<std::uint32_t>* target) { return target ? reinterpret_cast<std::uintptr_t>(target->data()) : std::uintptr_t{0}; };
|
||||
const auto size = [](const std::vector<std::uint32_t>* target) { return target ? static_cast<std::uint32_t>(target->size()) : 0u; };
|
||||
return makePacket(0x3f, {mode | (function << 8u), 0, 0, 0, 0, 0, 0, static_cast<std::uint32_t>(address(first)), static_cast<std::uint32_t>(address(first) >> 32u), size(first), static_cast<std::uint32_t>(address(second)), static_cast<std::uint32_t>(address(second) >> 32u), size(second)});
|
||||
}
|
||||
|
||||
void testBranchSubmission() {
|
||||
static std::array<std::uint32_t, 4> results{};
|
||||
static std::vector<std::uint32_t> first, second;
|
||||
results.fill(0);
|
||||
first = joinPackets({writeWord(results[0], 71)});
|
||||
second = joinPackets({writeWord(results[1], 72)});
|
||||
auto words = joinPackets({branch(1, 0, &first, nullptr), writeWord(results[2], 73)});
|
||||
submitWords(words);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(results[0] == 71 && results[1] == 0 && results[2] == 73, "an always-taken if-then COND_INDIRECT_BUFFER did not run its buffer");
|
||||
results.fill(0);
|
||||
words = joinPackets({branch(2, 0, &first, &second), writeWord(results[2], 74)});
|
||||
submitWords(words);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(results[0] == 71 && results[1] == 0 && results[2] == 74, "an always-taken if-then-else COND_INDIRECT_BUFFER ran the wrong buffer");
|
||||
results.fill(0);
|
||||
words = joinPackets({branch(1, 0, nullptr, nullptr), writeWord(results[2], 75)});
|
||||
submitWords(words);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(results[2] == 75, "an empty COND_INDIRECT_BUFFER skipped the next packet");
|
||||
for (const auto& [mode, function, text] : {std::tuple{1u, 3u, "with a comparison"}, std::tuple{0u, 0u, "invalid COND_INDIRECT_BUFFER mode"}}) {
|
||||
results.fill(0);
|
||||
words = joinPackets({writeWord(results[3], 1), branch(mode, function, &first, nullptr)});
|
||||
expectFailure([&] { submitWords(words); }, text);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(results[0] == 0 && results[3] == 0, "a rejected COND_INDIRECT_BUFFER submission executed a packet");
|
||||
}
|
||||
}
|
||||
|
||||
void testPredicatedSubmission() {
|
||||
alignas(16) std::uint64_t flag[2] = {0, 0};
|
||||
alignas(16) std::array<std::uint32_t, 4> written{};
|
||||
|
|
@ -780,6 +817,81 @@ void testPredicatedSubmission() {
|
|||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> condWrite(std::uint32_t control, const std::uint32_t& poll, std::uint32_t reference, std::uint32_t mask, std::uint32_t& target, std::uint32_t value) {
|
||||
return makePacket(0x45, {control, low(&poll), high(&poll), reference, mask, low(&target), high(&target), value});
|
||||
}
|
||||
|
||||
void testConditionalWrite() {
|
||||
AgcDriver::QueueState state;
|
||||
alignas(4) std::uint32_t poll = 0x1234u;
|
||||
alignas(4) std::uint32_t target = 0;
|
||||
const auto valid = condWrite(0x113, poll, 0x34, 0xff, target, 7);
|
||||
check(AgcDriver::Pm4::Name(valid[0]) == "COND_WRITE" && valid[0] == 0xc0074500u, "COND_WRITE header or name mismatch");
|
||||
check(AgcDriver::Pm4::UnsupportedReason(valid[0]).empty() && AgcDriver::Pm4::AccessesMemory(valid[0]), "COND_WRITE is rejected or does not synchronize guest memory");
|
||||
|
||||
const std::array<std::pair<std::uint32_t, bool>, 7> functions{{{0, true}, {1, false}, {2, true}, {3, true}, {4, false}, {5, true}, {6, false}}};
|
||||
for (const auto& [function, writes] : functions) {
|
||||
target = 0;
|
||||
execute(state, condWrite(0x110 | function, poll, 0x34, 0xff, target, 9));
|
||||
check(target == (writes ? 9u : 0u), "COND_WRITE compared the masked poll value incorrectly");
|
||||
}
|
||||
target = 0;
|
||||
execute(state, condWrite(0x111, poll, 0x35, 0xff, target, 10));
|
||||
check(target == 10, "COND_WRITE less-than did not write");
|
||||
target = 0;
|
||||
execute(state, condWrite(0x116, poll, 0x1233, 0xffffffffu, target, 11));
|
||||
check(target == 11, "COND_WRITE greater-than with a full mask did not write");
|
||||
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x45, {0x113, low(&poll), high(&poll), 0, 0, low(&target), high(&target)}), 0); }, "packet size");
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x2000113, poll, 0, 0, target, 0), 0); }, "reserved");
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x103, poll, 0, 0, target, 0), 0); }, "register-space");
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x117, poll, 0, 0, target, 0), 0); }, "compare function");
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x013, poll, 0, 0, target, 0), 0); }, "destination");
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(condWrite(0x213, poll, 0, 0, target, 0), 0); }, "destination");
|
||||
auto misaligned = condWrite(0x113, poll, 0, 0, target, 0);
|
||||
misaligned[2] += 2;
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(misaligned, 0); }, "misaligned");
|
||||
misaligned = condWrite(0x113, poll, 0, 0, target, 0);
|
||||
misaligned[6] += 1;
|
||||
expectFailure([&] { AgcDriver::Pm4::Validate(misaligned, 0); }, "misaligned");
|
||||
|
||||
struct MemoryState {
|
||||
std::uint32_t poll = 0;
|
||||
std::uint32_t target = 0;
|
||||
bool read = false;
|
||||
bool written = false;
|
||||
};
|
||||
static MemoryState memory;
|
||||
memory = {};
|
||||
struct FlushHookReset {
|
||||
~FlushHookReset() { AgcDriver::GuestMemory::SetFlushHook(nullptr); }
|
||||
} reset;
|
||||
AgcDriver::GuestMemory::SetFlushHook([](std::uint64_t address, std::size_t bytes) {
|
||||
check(bytes == sizeof(std::uint32_t), "COND_WRITE resolved an unrelated range");
|
||||
if (address == reinterpret_cast<std::uintptr_t>(&memory.target)) {
|
||||
memory.written = true;
|
||||
} else {
|
||||
check(address == reinterpret_cast<std::uintptr_t>(&memory.poll), "COND_WRITE resolved an unrelated poll address");
|
||||
memory.poll = 5;
|
||||
memory.read = true;
|
||||
}
|
||||
});
|
||||
execute(state, condWrite(0x113, memory.poll, 5, 0xffffffffu, memory.target, 12));
|
||||
check(memory.read && memory.written && memory.target == 12, "COND_WRITE used a stale poll value or skipped synchronizing its target");
|
||||
}
|
||||
|
||||
void testConditionalWriteSubmission() {
|
||||
alignas(4) static std::uint32_t poll = 0;
|
||||
static std::array<std::uint32_t, 2> results{};
|
||||
results.fill(0);
|
||||
poll = 0;
|
||||
auto words = joinPackets({writeWord(poll, 5), condWrite(0x113, poll, 5, 0xffffffffu, results[0], 51), condWrite(0x114, poll, 5, 0xffffffffu, results[1], 52)});
|
||||
submitWords(words);
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
check(results[0] == 51, "COND_WRITE read its poll value before an earlier packet of its queue stored it");
|
||||
check(results[1] == 0, "COND_WRITE wrote although its comparison failed");
|
||||
}
|
||||
|
||||
void testAsyncMemoryFailure() {
|
||||
auto commands = makePacket(0x37, {0x100, 0x1000, 0, 1});
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
|
|
@ -792,6 +904,14 @@ void testAsyncMemoryFailure() {
|
|||
|
||||
}
|
||||
|
||||
void testUnwrittenUserData() {
|
||||
AgcDriver::Registers shader{{0x8c, 0x100}, {0x8d, 0}, {0x240, 0x200}};
|
||||
check(AgcDriver::DriverDetail::readUserData(shader, 0x8c) == 0x100 && AgcDriver::DriverDetail::readUserData(shader, 0x240) == 0x200, "a written user data register was not read");
|
||||
check(AgcDriver::DriverDetail::readUserData(shader, 0x8d) == 0, "a user data register written as zero was not read");
|
||||
check(AgcDriver::DriverDetail::readUserData(shader, 0x95) == 0 && AgcDriver::DriverDetail::readUserData(shader, 0x241) == 0, "an unwritten user data register does not read zero");
|
||||
expectFailure([&] { static_cast<void>(AgcDriver::DriverDetail::readRegister(shader, 0x95)); }, "required shader register");
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2 && std::string(argv[1]) == "failure") {
|
||||
|
|
@ -815,10 +935,14 @@ int main(int argc, char** argv) {
|
|||
testConditionReadSynchronization();
|
||||
testEventWrite();
|
||||
testAcquireMem();
|
||||
testConditionalWrite();
|
||||
testConditionalWriteSubmission();
|
||||
testPredication();
|
||||
testUnwrittenUserData();
|
||||
testDriverSubmission();
|
||||
testPredicatedSubmission();
|
||||
testConditionalSubmission();
|
||||
testBranchSubmission();
|
||||
LibcRunShutdown_nid_postfix();
|
||||
std::puts("PM4 catalog, registers, state, memory, conditional execution and submission tests passed");
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/Driver.hpp"
|
||||
#include "prx/libc/include/Shutdown.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Acb.hpp"
|
||||
#include "prx/libSceAgcDriver/Submit/include/Dcb.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
try {
|
||||
alignas(256) const std::array<std::uint32_t, 1> code{0xbf810000};
|
||||
Shader shader{};
|
||||
shader.file_header = 0x34333231;
|
||||
shader.version = 0x18;
|
||||
shader.header_size = sizeof(Shader);
|
||||
shader.shader_size = sizeof(code);
|
||||
shader.code = code.data();
|
||||
AgcDriverRegisterShader_nid_postfix(&shader);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(code.data());
|
||||
std::vector<std::uint32_t> commands{
|
||||
0xc0027600, 0x20c, static_cast<std::uint32_t>(address >> 8u), static_cast<std::uint32_t>(address >> 40u),
|
||||
0xc0017600, 0x213, 0,
|
||||
0xc0031500, 1, 1, 1, 0x8041
|
||||
};
|
||||
Packet packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
const std::array<std::uint32_t, 3> arguments{1, 1, 1};
|
||||
alignas(256) const std::array<std::uint32_t, 65> vertexCode{0xbf810000};
|
||||
Shader vertexShader = shader;
|
||||
const std::array<std::uint16_t, 4> indices{0, 1, 2, 0};
|
||||
std::uint32_t destination = 0;
|
||||
const bool graphics = argc == 2 && (std::string(argv[1]) == "draw" || std::string(argv[1]) == "vertex");
|
||||
if (graphics) {
|
||||
shader.type = 1;
|
||||
AgcDriverRegisterShader_nid_postfix(&shader);
|
||||
vertexShader.type = 2;
|
||||
vertexShader.shader_size = sizeof(vertexCode);
|
||||
vertexShader.code = vertexCode.data();
|
||||
AgcDriverRegisterShader_nid_postfix(&vertexShader);
|
||||
const auto vertexAddress = reinterpret_cast<std::uintptr_t>(vertexCode.data()) + 256;
|
||||
const auto indexAddress = reinterpret_cast<std::uintptr_t>(indices.data());
|
||||
const auto pixelAddress = std::string(argv[1]) == "vertex" ? 0 : address;
|
||||
commands = {
|
||||
0xc0027600, 0x0c8, static_cast<std::uint32_t>(vertexAddress >> 8u), static_cast<std::uint32_t>(vertexAddress >> 40u),
|
||||
0xc0017600, 0x08b, 0,
|
||||
0xc0027600, 0x008, static_cast<std::uint32_t>(pixelAddress >> 8u), static_cast<std::uint32_t>(pixelAddress >> 40u),
|
||||
0xc0017600, 0x00b, 0x08000002,
|
||||
0xc0217600, 0x00c
|
||||
};
|
||||
for (std::uint32_t i = 0; i < 33; ++i) commands.push_back(100 + i);
|
||||
const auto destinationAddress = reinterpret_cast<std::uintptr_t>(&destination);
|
||||
const std::array<std::uint32_t, 13> draw{
|
||||
0xc0012600, static_cast<std::uint32_t>(indexAddress), static_cast<std::uint32_t>(indexAddress >> 32u),
|
||||
0xc0033500, 3, 1, 3, 0x20,
|
||||
0xc0033700, 0x100, static_cast<std::uint32_t>(destinationAddress), static_cast<std::uint32_t>(destinationAddress >> 32u), 7
|
||||
};
|
||||
commands.insert(commands.end(), draw.begin(), draw.end());
|
||||
packet = Packet{commands.data(), static_cast<std::uint32_t>(commands.size()), 0, {}};
|
||||
}
|
||||
if (argc == 2 && std::string(argv[1]) == "indirect") {
|
||||
const auto argumentAddress = reinterpret_cast<std::uintptr_t>(arguments.data());
|
||||
commands[7] = 0xc0021600;
|
||||
commands[8] = static_cast<std::uint32_t>(argumentAddress);
|
||||
commands[9] = static_cast<std::uint32_t>(argumentAddress >> 32u);
|
||||
commands[10] = 0x8041;
|
||||
packet.dw_num = 11;
|
||||
}
|
||||
if (graphics) sceAgcDriverSubmitDcb(&packet);
|
||||
else sceAgcDriverSubmitAcb(0x20, &packet);
|
||||
try {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
} catch (const std::runtime_error& error) {
|
||||
if (std::string(error.what()) != "ShaderRecompiler::Recompile not implemented") {
|
||||
throw;
|
||||
}
|
||||
if (destination != 0) throw std::runtime_error("failed draw executed a later memory write");
|
||||
std::puts("Vulkan device initialized; real recompiler exception propagated");
|
||||
try {
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
throw std::runtime_error("idle lost recompiler failure");
|
||||
} catch (const std::runtime_error& idle) {
|
||||
if (std::string(idle.what()) != "ShaderRecompiler::Recompile not implemented") throw;
|
||||
}
|
||||
try {
|
||||
LibcRunShutdown_nid_postfix();
|
||||
throw std::runtime_error("shutdown lost recompiler failure");
|
||||
} catch (const std::runtime_error& shutdown) {
|
||||
if (std::string(shutdown.what()) != "ShaderRecompiler::Recompile not implemented") throw;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
throw std::runtime_error("dispatch unexpectedly completed without a recompiler");
|
||||
} catch (const std::exception& error) {
|
||||
std::fprintf(stderr, "%s\n", error.what());
|
||||
try { LibcRunShutdown_nid_postfix(); }
|
||||
catch (const std::exception& shutdown) { std::fprintf(stderr, "shutdown: %s\n", shutdown.what()); }
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "ShaderDiskCache.hpp"
|
||||
#include "ShaderCacheDirectory.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
|
|
@ -7,6 +8,7 @@
|
|||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
|
|
@ -31,7 +33,7 @@ void setEnvironment(const char* name, const std::string& value) {
|
|||
}
|
||||
|
||||
bool sameBinding(const DescriptorBinding& left, const DescriptorBinding& right) {
|
||||
return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten;
|
||||
return left.kind == right.kind && left.role == right.role && left.descriptorSet == right.descriptorSet && left.binding == right.binding && left.count == right.count && left.guestDescriptor == right.guestDescriptor && left.readOnly == right.readOnly && left.imageShape == right.imageShape && left.samplerDepthCompare == right.samplerDepthCompare && left.imageWritten == right.imageWritten && left.imageDepthCompare == right.imageDepthCompare && left.imageAtomic == right.imageAtomic && left.bufferAtomic == right.bufferAtomic && left.bufferWritten == right.bufferWritten && left.samplerUnnormalized == right.samplerUnnormalized && left.imageUnnormalized == right.imageUnnormalized && left.imageSamplers == right.imageSamplers;
|
||||
}
|
||||
|
||||
bool sameBindings(const std::vector<DescriptorBinding>& left, const std::vector<DescriptorBinding>& right) {
|
||||
|
|
@ -95,6 +97,9 @@ DescriptorBinding sampleBinding(std::uint32_t seed) {
|
|||
binding.imageAtomic = {false, true, false};
|
||||
binding.bufferAtomic = {true};
|
||||
binding.bufferWritten = {false, false, true, true, false};
|
||||
binding.samplerUnnormalized = {false, true, true};
|
||||
binding.imageUnnormalized = {true, false, seed % 2 == 0};
|
||||
binding.imageSamplers = {0x5u, 0u, 0x80000000u};
|
||||
return binding;
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +164,7 @@ CompiledVariant sampleVariant() {
|
|||
image.mipCount = 4;
|
||||
image.shaderSwizzle = 0x321u;
|
||||
image.written = true;
|
||||
image.srgbDecode = true;
|
||||
image.cube = true;
|
||||
image.r128 = true;
|
||||
image.indirectRoot = 0;
|
||||
|
|
@ -166,7 +172,7 @@ CompiledVariant sampleVariant() {
|
|||
image.indirectSearchIterations = 3;
|
||||
image.indirectResources = {1, 2, 3};
|
||||
info.info.images = {image};
|
||||
info.info.samplers = {{7, 0x10, true, false}};
|
||||
info.info.samplers = {{7, 0x10, true, false, SamplerUseExplicitLod | SamplerUseGather}};
|
||||
info.info.sampledPairs = {{0, 0, 0x10}};
|
||||
StageInput input{};
|
||||
input.kind = StageInputKind::GlobalInvocationId;
|
||||
|
|
@ -326,6 +332,7 @@ void verifyKeySensitivity() {
|
|||
changes("an extension", [](SampleRequest& sample) { sample.extensions[0] = "SPV_KHR_storage_buffer_storage_clasS"; });
|
||||
changes("barycentrics", [](SampleRequest& sample) { sample.request.target.fragmentShaderBarycentricEnabled = true; });
|
||||
changes("non-constant texel offsets", [](SampleRequest& sample) { sample.request.target.nonConstantImageOffsets = true; });
|
||||
changes("the sRGB formats decoded in the shader", [](SampleRequest& sample) { sample.request.target.srgbDecodeFormats = 2u; });
|
||||
changes("the workgroup size limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSize[2] = 128; });
|
||||
changes("the invocation limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupInvocations = 512; });
|
||||
changes("the shared memory limit", [](SampleRequest& sample) { sample.request.target.maxWorkgroupSharedMemoryBytes = 32768; });
|
||||
|
|
@ -348,6 +355,7 @@ void verifyKeySensitivity() {
|
|||
changes("an image search depth", [](SampleRequest& sample) { sample.specialization.images[0].indirectSearchIterations = 2; });
|
||||
changes("an image cube flag", [](SampleRequest& sample) { sample.specialization.images[0].cube = true; });
|
||||
changes("an image FMASK flag", [](SampleRequest& sample) { sample.specialization.images[0].fmask = true; });
|
||||
changes("an image sRGB decode", [](SampleRequest& sample) { sample.specialization.images[0].srgbDecode = true; });
|
||||
changes("the image count", [](SampleRequest& sample) { sample.specialization.images.emplace_back(); });
|
||||
changes("the bound descriptors", [](SampleRequest& sample) { sample.specialization.boundDescriptors.push_back(1); });
|
||||
|
||||
|
|
@ -436,6 +444,96 @@ void verifyAcrossProcesses(const char* self) {
|
|||
require(std::system(command.c_str()) == 0, "the loading process failed");
|
||||
}
|
||||
|
||||
struct ClockRequest {
|
||||
std::vector<std::uint32_t> code{0xf4900100u, 0x00000000u, 0xbf8cc07fu, 0x7e020204u, 0xe0700000u, 0x80000100u, 0xf800180fu, 0x01010101u, 0xbf810000u};
|
||||
std::array<std::uint32_t, 4> userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu};
|
||||
std::array<std::uint32_t, 1> capabilities{1u};
|
||||
RecompileRequest request{};
|
||||
|
||||
explicit ClockRequest(std::uint32_t stride = 0) {
|
||||
userData[1] = stride << 16u;
|
||||
ShaderPixelStageInfo pixel{};
|
||||
pixel.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter);
|
||||
pixel.hasPerspectiveCenterVgpr = true;
|
||||
pixel.targetOutputMode[0] = 9u;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}};
|
||||
request.context.waveSize = 64;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.pixel = pixel;
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
}
|
||||
};
|
||||
|
||||
std::string emissionFailure(const RecompileRequest& request) {
|
||||
try {
|
||||
static_cast<void>(Recompile(request));
|
||||
} catch (const std::runtime_error& error) {
|
||||
const std::string message = error.what();
|
||||
require(message.find("reads the shader clock, which needs the device's VK_KHR_shader_clock") != std::string::npos, "unexpected failure: " + message.substr(0, message.find("RecompileRequest:")));
|
||||
return message;
|
||||
}
|
||||
throw std::runtime_error("a shader clock read compiled without VK_KHR_shader_clock");
|
||||
}
|
||||
|
||||
std::uint64_t diskMisses() {
|
||||
return ShaderDiskCache::Totals().misses;
|
||||
}
|
||||
|
||||
void verifyEmissionFailureMemo() {
|
||||
const auto before = ShaderDiskCache::Totals();
|
||||
ClockRequest clock;
|
||||
const auto first = emissionFailure(clock.request);
|
||||
require(diskMisses() == before.misses + 1, "the first compile was not looked up");
|
||||
require(emissionFailure(clock.request) == first, "the second compile failed with another message");
|
||||
require(diskMisses() == before.misses + 1, "the second compile looked up and emitted the program again");
|
||||
|
||||
ClockRequest layout;
|
||||
layout.request.layout = {0, 0, 16, 112};
|
||||
static_cast<void>(emissionFailure(layout.request));
|
||||
require(diskMisses() == before.misses + 2, "another binding layout reused the failure");
|
||||
ClockRequest stride(16);
|
||||
static_cast<void>(emissionFailure(stride.request));
|
||||
require(diskMisses() == before.misses + 3, "another buffer specialization reused the failure");
|
||||
ClockRequest relocated;
|
||||
relocated.request.shader.codeAddress += 0x1000u;
|
||||
static_cast<void>(emissionFailure(relocated.request));
|
||||
require(diskMisses() == before.misses + 4, "a relocated copy reused the failure");
|
||||
require(emissionFailure(clock.request) == first && diskMisses() == before.misses + 4, "the other requests replaced the failure");
|
||||
|
||||
ClockRequest concurrent;
|
||||
concurrent.request.layout.pushConstantSizeBytes = 64;
|
||||
std::array<std::future<std::string>, 4> failures;
|
||||
for (auto& future : failures) future = std::async(std::launch::async, [&concurrent] { return emissionFailure(concurrent.request); });
|
||||
std::vector<std::string> messages;
|
||||
for (auto& future : failures) messages.push_back(future.get());
|
||||
require(std::all_of(messages.begin(), messages.end(), [&](const std::string& message) { return message == messages.front(); }), "concurrent requests failed with different messages");
|
||||
require(diskMisses() == before.misses + 5, "concurrent requests emitted the program " + std::to_string(diskMisses() - before.misses - 4) + " times");
|
||||
|
||||
ShaderDiskCache::Flush();
|
||||
require(ShaderDiskCache::Totals().writes == before.writes, "an emission failure was stored in the disk cache");
|
||||
}
|
||||
|
||||
int runWithoutFailureMemo() {
|
||||
ClockRequest clock;
|
||||
const auto first = emissionFailure(clock.request);
|
||||
require(emissionFailure(clock.request) == first, "the second compile failed with another message");
|
||||
require(diskMisses() == 2, "APS5_NO_FAILURE_MEMO=1 did not emit the program again (" + std::to_string(diskMisses()) + " lookups)");
|
||||
std::cout << "APS5_NO_FAILURE_MEMO=1 emitted the failing program twice\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
void verifyFailureMemoSwitch(const char* self) {
|
||||
setEnvironment("APS5_NO_FAILURE_MEMO", "1");
|
||||
const std::string command = "\"" + std::string(self) + "\" --no-failure-memo";
|
||||
require(std::system(command.c_str()) == 0, "the process with APS5_NO_FAILURE_MEMO=1 failed");
|
||||
}
|
||||
|
||||
void verifyDefaultDirectory(const char* self) {
|
||||
setEnvironment("ANYPS5_SHADER_CACHE_DIR", "");
|
||||
setEnvironment("ANYPS5_NO_SHADER_CACHE", "1");
|
||||
|
|
@ -452,6 +550,7 @@ void verifyDefaultDirectory(const char* self) {
|
|||
int main(int argc, char** argv) {
|
||||
try {
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--load") return runLoadingProcess();
|
||||
if (argc == 2 && std::string_view(argv[1]) == "--no-failure-memo") return runWithoutFailureMemo();
|
||||
const auto directory = std::filesystem::temp_directory_path() / ("aps5-shader-disk-cache-test-" + std::to_string(std::random_device{}()));
|
||||
std::filesystem::remove_all(directory);
|
||||
verifyDefaultDirectory(argv[0]);
|
||||
|
|
@ -462,6 +561,8 @@ int main(int argc, char** argv) {
|
|||
verifyKeySensitivity();
|
||||
verifyStore();
|
||||
verifyAcrossProcesses(argv[0]);
|
||||
verifyEmissionFailureMemo();
|
||||
verifyFailureMemoSwitch(argv[0]);
|
||||
std::error_code error;
|
||||
std::filesystem::remove_all(directory, error);
|
||||
std::cout << "shader disk cache tests passed\n";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/ShaderMemory.hpp"
|
||||
#include "ControlFlow/RequestSerializer.hpp"
|
||||
#include "Optimization/DescriptorBindingBuilder.hpp"
|
||||
#include "Optimization/RequestMemoryView.hpp"
|
||||
#include "Optimization/ResourceMaterializer.hpp"
|
||||
#include "Optimization/ResourceProgram.hpp"
|
||||
|
|
@ -11,6 +12,7 @@
|
|||
#include "SpirvBackend/SpirvOptimizer.hpp"
|
||||
#endif
|
||||
#include "CacheKey.hpp"
|
||||
#include "BdaAbi.hpp"
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
|
@ -24,6 +26,7 @@
|
|||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -552,6 +555,59 @@ void verifyProgramCounterRelativeData() {
|
|||
require(moved.cacheHit, "program counter data: relocating the shader recompiled it");
|
||||
require(dataBase(moved) == codeAddress + 0x1000u + 56u, "program counter data: the relocated shader bound the old address");
|
||||
}
|
||||
|
||||
void verifyLanesOutsideHostSubgroup() {
|
||||
using namespace ShaderRecompiler;
|
||||
const auto pixel = [](std::span<const std::uint32_t> code, std::uint32_t subgroupSize) {
|
||||
ShaderPixelStageInfo info{};
|
||||
info.inputAddr = PixelInputBit(PixelInput::PerspectiveCenter);
|
||||
info.hasPerspectiveCenterVgpr = true;
|
||||
info.targetOutputMode[0] = 9u;
|
||||
info.targetExportMapping.fill(0xe4u);
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}};
|
||||
request.context.waveSize = 64;
|
||||
request.context.pixel = info;
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = subgroupSize;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
return !Recompile(request).spirv.empty();
|
||||
};
|
||||
const auto compute = [](std::span<const std::uint32_t> code, std::uint32_t subgroupSize) {
|
||||
static constexpr std::array<std::uint32_t, 4> userData{0x10000000u, 0x00100000u, 0x40u, 0x00027facu};
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x20000u, code, 0, {}};
|
||||
request.context.waveSize = 64;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{64u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = subgroupSize;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
return !Recompile(request).spirv.empty();
|
||||
};
|
||||
static constexpr std::array<std::uint32_t, 6> lane63{0xd7600006u, 0x00017f00u, 0x7e020206u, 0xf800180fu, 0x01010101u, 0xbf810000u};
|
||||
static constexpr std::array<std::uint32_t, 6> lane31{0xd7600006u, 0x00013f00u, 0x7e020206u, 0xf800180fu, 0x01010101u, 0xbf810000u};
|
||||
static constexpr std::array<std::uint32_t, 11> waterfall{0xbe80047eu, 0xbe821400u, 0xd7600003u, 0x00000500u, 0xbe801c02u, 0xbf138000u, 0xbf85fffau, 0x7e020203u, 0xf800180fu, 0x01010101u, 0xbf810000u};
|
||||
static constexpr std::array<std::uint32_t, 6> computeLane63{0xd7600006u, 0x00017f00u, 0x7e020206u, 0xe0700000u, 0x80000100u, 0xbf810000u};
|
||||
static constexpr std::array<std::uint32_t, 5> permlanex16{0xd7780001u, 0x02010100u, 0xf800180fu, 0x01010101u, 0xbf810000u};
|
||||
static constexpr std::array<std::uint32_t, 5> permlane16{0xd7770001u, 0x02010100u, 0xf800180fu, 0x01010101u, 0xbf810000u};
|
||||
expectFailure([&] { static_cast<void>(pixel(lane63, 32u)); }, "v_readlane_b32 of lane 63 is outside the 32-lane host subgroup that runs this wave64 program at one lane per invocation", "host lanes: a wave64 pixel program read lane 63 of a 32-lane subgroup");
|
||||
expectFailure([&] { static_cast<void>(pixel(lane63, 8u)); }, "v_readlane_b32 of lane 63 is outside the 8-lane host subgroup", "host lanes: a wave64 pixel program read lane 63 of an 8-lane subgroup");
|
||||
require(pixel(lane31, 32u), "host lanes: a wave64 pixel program did not build reading lane 31 of a 32-lane subgroup");
|
||||
expectFailure([&] { static_cast<void>(pixel(lane31, 8u)); }, "v_readlane_b32 of lane 31 is outside the 8-lane host subgroup", "host lanes: a wave64 pixel program read lane 31 of an 8-lane subgroup");
|
||||
require(pixel(lane63, 64u), "host lanes: a wave64 pixel program did not build reading lane 63 of a 64-lane subgroup");
|
||||
require(compute(computeLane63, 32u), "host lanes: a wave64 compute program did not build reading lane 63 at two lanes per invocation");
|
||||
require(compute(computeLane63, 8u), "host lanes: a wave64 compute program did not build reading lane 63 of an 8-lane subgroup");
|
||||
require(pixel(waterfall, 32u) && pixel(waterfall, 8u), "host lanes: an s_ff1_i32_b64 waterfall reading a lane in a register did not build");
|
||||
expectFailure([&] { static_cast<void>(pixel(permlanex16, 8u)); }, "v_permlanex16_b32 from lanes 16-31 is outside the 8-lane host subgroup", "host lanes: v_permlanex16_b32 read lanes 16-31 of an 8-lane subgroup");
|
||||
require(pixel(permlanex16, 32u) && pixel(permlane16, 8u), "host lanes: a v_permlane16_b32 row that the host subgroup may hold did not build");
|
||||
}
|
||||
|
||||
void verifyMeshConfiguration() {
|
||||
using namespace ShaderRecompiler;
|
||||
ShaderMeshInputInfo list;
|
||||
|
|
@ -601,6 +657,217 @@ ShaderRecompiler::ShaderPixelStageInfo twoParameterPixel() {
|
|||
return pixel;
|
||||
}
|
||||
|
||||
std::string requestPrefix(std::string_view text, std::size_t bytes) {
|
||||
constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string result(text.substr(0, ((bytes + 2u) / 3u) * 4u));
|
||||
if (bytes % 3u == 1u) {
|
||||
result[result.size() - 3u] = alphabet[alphabet.find(result[result.size() - 3u]) & 0x30u];
|
||||
result[result.size() - 2u] = '=';
|
||||
result.back() = '=';
|
||||
} else if (bytes % 3u == 2u) {
|
||||
result[result.size() - 2u] = alphabet[alphabet.find(result[result.size() - 2u]) & 0x3cu];
|
||||
result.back() = '=';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void verifyPixelRequestSerialization() {
|
||||
using namespace ShaderRecompiler;
|
||||
const auto fields = [](const ShaderPixelStageInfo& value) {
|
||||
return std::tie(value.interpolatorCount, value.interpolatorSettings, value.wave32, value.inputAddr,
|
||||
value.hasPerspectiveCenterVgpr, value.perspectiveCentroid, value.posX, value.posY,
|
||||
value.posZ, value.posW, value.frontFace, value.ancillary, value.sampleShading,
|
||||
value.noPerspective, value.linearCentroid, value.pixelKillEnable, value.depthExportEnable,
|
||||
value.sampleMaskExportEnable, value.earlyZ, value.executeOnNoop, value.conservativeZExport,
|
||||
value.targetOutputMode, value.targetExportMapping);
|
||||
};
|
||||
const std::array<std::uint32_t, 1> code{0xbf810000u};
|
||||
const std::array<std::uint32_t, 3> userData{0x12345678u, 0xabcdef01u, 0x87654321u};
|
||||
const std::array<MemoryRegion, 1> memory{{{0x60000u, std::as_bytes(std::span(userData))}}};
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}};
|
||||
request.context.waveSize = 64;
|
||||
request.context.userData = userData;
|
||||
request.context.memory = memory;
|
||||
request.context.vertex = ShaderVertexStageInfo{};
|
||||
request.context.vertex->fetchAttribReg = 17u;
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 64;
|
||||
request.target.nonConstantImageOffsets = true;
|
||||
request.layout = {0u, 11u, 16u, 128u};
|
||||
request.useCache = false;
|
||||
ShaderPixelStageInfo pixel{
|
||||
.interpolatorCount = 32u,
|
||||
.wave32 = true,
|
||||
.inputAddr = 0x7fffu,
|
||||
.hasPerspectiveCenterVgpr = true,
|
||||
.perspectiveCentroid = true,
|
||||
.posX = true,
|
||||
.posY = true,
|
||||
.posZ = true,
|
||||
.posW = true,
|
||||
.frontFace = true,
|
||||
.ancillary = true,
|
||||
.sampleShading = true,
|
||||
.noPerspective = true,
|
||||
.linearCentroid = true,
|
||||
.pixelKillEnable = true,
|
||||
.depthExportEnable = true,
|
||||
.sampleMaskExportEnable = true,
|
||||
.earlyZ = true,
|
||||
.executeOnNoop = true,
|
||||
.conservativeZExport = ConservativeZExport::GreaterThanZ,
|
||||
.targetOutputMode = {1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u}
|
||||
};
|
||||
for (std::uint32_t i = 0; i < pixel.interpolatorSettings.size(); ++i) pixel.interpolatorSettings[i] = 0x10101010u + i;
|
||||
const std::array<std::array<std::uint8_t, 8>, 3> mappings{{
|
||||
{0x00u, 0xe4u, 0xc6u, 0x1bu, 0xffu, 0x80u, 0x55u, 0xaau},
|
||||
{0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u, 0xe4u},
|
||||
{0xc6u, 0x1bu, 0x00u, 0xffu, 0x55u, 0xaau, 0x80u, 0x39u}
|
||||
}};
|
||||
const RequestSerializer serializer;
|
||||
for (const auto& mapping : mappings) {
|
||||
pixel.targetExportMapping = mapping;
|
||||
request.context.pixel = pixel;
|
||||
const auto replay = serializer.Deserialize(serializer.Serialize(request));
|
||||
require(replay.request.context.pixel.has_value() && fields(*replay.request.context.pixel) == fields(pixel), "pixel fields or export mappings were lost in serialization");
|
||||
require(replay.request.context.vertex.has_value() && replay.request.context.vertex->fetchAttribReg == 17u && replay.request.context.memory.size() == 1u && replay.request.context.memory[0].guestAddress == 0x60000u && replay.request.context.memory[0].bytes.size() == sizeof(userData), "pixel mappings displaced the following guest context");
|
||||
require(replay.request.target.subgroupSize == 64u && replay.request.target.nonConstantImageOffsets && replay.request.layout.firstBinding == 11u && replay.request.layout.pushConstantOffsetBytes == 16u && !replay.request.useCache, "pixel mappings displaced the following request fields");
|
||||
std::vector<std::uint64_t> key;
|
||||
RecompileCacheKey::Build(request, key);
|
||||
std::vector<std::uint64_t> replayKey;
|
||||
RecompileCacheKey::Build(replay.request, replayKey);
|
||||
require(key == replayKey && RecompileCacheKey::ContextHash(request) == RecompileCacheKey::ContextHash(replay.request), "pixel replay changed shader identity");
|
||||
for (std::size_t i = 0; i < mapping.size(); ++i) {
|
||||
auto changed = request;
|
||||
changed.context.pixel->targetExportMapping[i] ^= 1u;
|
||||
RecompileCacheKey::Build(changed, replayKey);
|
||||
require(key != replayKey && RecompileCacheKey::ContextHash(request) != RecompileCacheKey::ContextHash(changed), "shader identity ignored a pixel export mapping");
|
||||
}
|
||||
}
|
||||
request.context.pixel.reset();
|
||||
request.shader.stage = ShaderStage::Vertex;
|
||||
const auto withoutPixel = serializer.Deserialize(serializer.Serialize(request));
|
||||
require(!withoutPixel.request.context.pixel.has_value() && withoutPixel.request.context.vertex.has_value() && withoutPixel.request.context.vertex->fetchAttribReg == 17u && withoutPixel.request.context.memory.size() == 1u && withoutPixel.request.context.memory[0].guestAddress == 0x60000u && withoutPixel.request.target.nonConstantImageOffsets && withoutPixel.request.layout.firstBinding == 11u && !withoutPixel.request.useCache, "a request without pixel state was misaligned");
|
||||
|
||||
RecompileRequest minimal{};
|
||||
minimal.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}};
|
||||
minimal.context.waveSize = 64;
|
||||
minimal.context.pixel = ShaderPixelStageInfo{};
|
||||
const auto encoded = serializer.Serialize(minimal);
|
||||
require(requestPrefix(encoded, 8u) == "NVNQQQkAAAA=", "new requests did not use serialization version 9");
|
||||
constexpr std::size_t mappingOffset = 8u + 37u + 18u + 162u;
|
||||
for (std::size_t bytes = 0; bytes < 8u; ++bytes) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(requestPrefix(encoded, mappingOffset + bytes))); }, "truncated data", "a truncated version-8 pixel mapping was accepted");
|
||||
}
|
||||
for (const auto unsupported : {"NVNQQQAAAAA=", "NVNQQQoAAAA="}) {
|
||||
expectFailure([&] { static_cast<void>(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted");
|
||||
}
|
||||
}
|
||||
|
||||
void verifyLegacyPixelRequests() {
|
||||
using namespace ShaderRecompiler;
|
||||
static constexpr std::array<std::string_view, 8> legacyPixelRequests{
|
||||
"NVNQQQEAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA"
|
||||
"AAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAIAA"
|
||||
"AAAA",
|
||||
"NVNQQQIAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA"
|
||||
"AAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAIAA"
|
||||
"AAAAAA==",
|
||||
"NVNQQQMAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA"
|
||||
"AAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAIAA"
|
||||
"AAAAAA==",
|
||||
"NVNQQQQAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAA"
|
||||
"AAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAAAIAA"
|
||||
"AAAAAA==",
|
||||
"NVNQQQUAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAAAAAA"
|
||||
"gAAAAAAA",
|
||||
"NVNQQQYAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAAAAAAA"
|
||||
"gAAAAAAAAQ==",
|
||||
"NVNQQQcAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA"
|
||||
"AAAAAAAAAAAAABBAAAADAQBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALAAAAAAAA"
|
||||
"AIAAAAAAAAE=",
|
||||
"NVNQQQgAAAAFAAADAAAAAAABAAAAAAAAAAAAgb8AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAABAAAAAAAAAAAAAAAAAAAAAgkAAAAAAAAA"
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAEEAAAAMBAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
"AAsAAAAAAAAAgAAAAAAAAQ==",
|
||||
};
|
||||
const RequestSerializer serializer;
|
||||
for (std::size_t index = 0; index < legacyPixelRequests.size(); ++index) {
|
||||
const auto version = index + 1u;
|
||||
const auto replay = serializer.Deserialize(legacyPixelRequests[index]);
|
||||
const auto& request = replay.request;
|
||||
require(request.context.pixel.has_value(), "legacy pixel state was lost");
|
||||
const auto& pixel = *request.context.pixel;
|
||||
require(pixel.targetExportMapping == std::array<std::uint8_t, 8>{}, "legacy pixel mapping no longer defaults to zero");
|
||||
require(pixel.inputAddr == 2u && pixel.hasPerspectiveCenterVgpr && pixel.targetOutputMode[0] == 9u, "legacy pixel layout was misread");
|
||||
require(pixel.conservativeZExport == (version >= 7u ? ConservativeZExport::GreaterThanZ : ConservativeZExport::AnyZ), "legacy conservative Z layout was misread");
|
||||
require(request.shader.stage == ShaderStage::Fragment && request.shader.code.size() == 1u && request.shader.code[0] == 0xbf810000u && !request.context.vertex.has_value() && request.context.memory.empty(), "legacy guest context was misaligned");
|
||||
require(request.target.vulkanVersion == 0x00401000u && request.target.spirvVersion == 0x00010300u && request.target.subgroupSize == 64u && request.layout.firstBinding == 11u && request.layout.pushConstantSizeBytes == 128u, "legacy target or binding layout was misaligned");
|
||||
require(request.useCache == (version == 1u) && request.target.nonConstantImageOffsets == (version >= 6u) && request.target.srgbDecodeFormats == 0u, "legacy request trailer was misread");
|
||||
const auto upgraded = serializer.Deserialize(serializer.Serialize(request));
|
||||
require(upgraded.request.context.pixel->targetExportMapping == pixel.targetExportMapping && RecompileCacheKey::ContextHash(upgraded.request) == RecompileCacheKey::ContextHash(request), "upgrading a legacy capture changed its pixel mapping");
|
||||
}
|
||||
}
|
||||
|
||||
void verifyPixelExportReplay() {
|
||||
using namespace ShaderRecompiler;
|
||||
std::array<std::uint32_t, 11> code{
|
||||
0x7e0002ffu, 0x3e800000u, 0x7e0202ffu, 0x3f000000u,
|
||||
0x7e0402ffu, 0x3f400000u, 0x7e0602ffu, 0x3f800000u,
|
||||
0xf800180fu, 0x03020100u, 0xbf810000u
|
||||
};
|
||||
const RequestSerializer serializer;
|
||||
for (const auto target : {0u, 7u}) {
|
||||
code[8] = 0xf800180fu | (target << 4u);
|
||||
ShaderPixelStageInfo pixel{};
|
||||
pixel.targetOutputMode[target] = 9u;
|
||||
pixel.targetExportMapping.fill(0xe4u);
|
||||
pixel.targetExportMapping[target] = 0xc6u;
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Fragment, 0x30000u, code, 0, {}};
|
||||
request.context.waveSize = 64u;
|
||||
request.context.pixel = pixel;
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 64u;
|
||||
request.layout.pushConstantSizeBytes = 128u;
|
||||
request.useCache = false;
|
||||
const auto replay = serializer.Deserialize(serializer.Serialize(request));
|
||||
const auto original = Recompile(request);
|
||||
auto identity = request;
|
||||
identity.context.pixel->targetExportMapping[target] = 0xe4u;
|
||||
require(original.spirv != Recompile(identity).spirv, "the non-identity pixel export mapping did not affect the compiled shader");
|
||||
require(replay.request.context.pixel.has_value() && replay.request.context.pixel->targetExportMapping == pixel.targetExportMapping, "replay lost the non-identity pixel export mapping");
|
||||
const auto replayed = Recompile(replay.request);
|
||||
verifyResult(original, replayed);
|
||||
request.useCache = true;
|
||||
const auto cached = Recompile(request);
|
||||
const auto cachedReplay = serializer.Deserialize(serializer.Serialize(request));
|
||||
const auto hit = Recompile(cachedReplay.request);
|
||||
require(hit.cacheHit && hit.variantId == cached.variantId, "pixel replay did not reuse the original shader variant");
|
||||
verifyResult(cached, hit);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::uint32_t> noPerspectiveLocations(std::span<const std::uint32_t> code) {
|
||||
using namespace ShaderRecompiler;
|
||||
RecompileRequest request{};
|
||||
|
|
@ -800,6 +1067,159 @@ void verifyComputedTexelOffsets() {
|
|||
}
|
||||
}
|
||||
|
||||
void verifyUnnormalizedSamplers() {
|
||||
using namespace ShaderRecompiler;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
constexpr std::uint32_t Format11_11_10UInt = 34;
|
||||
constexpr std::uint32_t Format32Float = 22;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint32_t Type3D = 10;
|
||||
constexpr std::uint32_t TypeCube = 11;
|
||||
constexpr std::uint32_t Type2DArray = 13;
|
||||
struct alignas(256) Texture { std::array<std::uint8_t, 4096> bytes{}; };
|
||||
static Texture texture;
|
||||
static std::array<std::uint32_t, 64> output{};
|
||||
const auto textureBase = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texture.bytes.data()));
|
||||
const auto outputBase = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output.data()));
|
||||
const std::array<std::uint32_t, 4> unnormalized{0x00008092u, 0x00fff000u, 0x05500000u, 0u};
|
||||
const std::array<std::uint32_t, 4> normalized{0x00000092u, 0x00fff000u, 0x05500000u, 0u};
|
||||
const auto imageData = [&](const std::array<std::uint32_t, 4>& sampler, std::uint32_t type, std::uint32_t format, std::uint32_t depth) {
|
||||
return std::array<std::uint32_t, 16>{
|
||||
static_cast<std::uint32_t>(textureBase >> 8u), static_cast<std::uint32_t>((textureBase >> 40u) & 0xffu) | (format << 20u) | (3u << 30u), 3u << 14u, 0xfacu | (type << 28u), depth, 0u, 0u, 0u,
|
||||
sampler[0], sampler[1], sampler[2], sampler[3],
|
||||
static_cast<std::uint32_t>(outputBase), static_cast<std::uint32_t>((outputBase >> 32u) & 0xffffu), 64u, 0xfacu};
|
||||
};
|
||||
const auto userData = [&](const std::array<std::uint32_t, 4>& sampler) {
|
||||
return imageData(sampler, Type2D, Format8888UNorm, 0u);
|
||||
};
|
||||
const auto program = [](std::uint32_t mimg) {
|
||||
return std::vector<std::uint32_t>{0x7e020280u, 0x7e040280u, 0x7e060280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, mimg, 0x00400801u, 0xe0700000u, 0x80030800u, 0xbf810000u};
|
||||
};
|
||||
const std::array<std::uint32_t, 3> capabilities{1u, static_cast<std::uint32_t>(spv::CapabilityImageGatherExtended), static_cast<std::uint32_t>(spv::CapabilityMinLod)};
|
||||
std::uint64_t nextAddress = 0x40000u;
|
||||
const auto recompile = [&](const std::vector<std::uint32_t>& code, const std::array<std::uint32_t, 16>& data, std::uint64_t address = 0u) {
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, address != 0u ? address : (nextAddress += 0x1000u), code, 0, {}};
|
||||
request.context.waveSize = 32;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = data;
|
||||
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.fragmentShaderBarycentricEnabled = false;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
AgcDriver::ShaderMemory memory({});
|
||||
static_cast<void>(memory.Capture(request));
|
||||
request.context.memory = memory.Regions();
|
||||
return Recompile(request);
|
||||
};
|
||||
const auto flags = [](const RecompileResult& result, DescriptorRole role) {
|
||||
for (const auto& binding : result.bindings) {
|
||||
if (binding.role == role) return role == DescriptorRole::GuestSamplers ? binding.samplerUnnormalized : binding.imageUnnormalized;
|
||||
}
|
||||
throw std::runtime_error("unnormalized samplers: the program has no sampler or image binding");
|
||||
};
|
||||
const auto proven = [&](const RecompileResult& result, bool expected) {
|
||||
return flags(result, DescriptorRole::GuestSamplers) == std::vector<bool>{expected} && flags(result, DescriptorRole::GuestImages) == std::vector<bool>{expected};
|
||||
};
|
||||
const auto auditSpirv = [](const std::vector<std::uint32_t>& words) {
|
||||
std::size_t samples = 0;
|
||||
for (std::size_t cursor = 5; cursor < words.size();) {
|
||||
const auto count = words[cursor] >> 16u;
|
||||
require(count != 0 && count <= words.size() - cursor, "unnormalized samplers: truncated SPIR-V instruction");
|
||||
const auto op = words[cursor] & 0xffffu;
|
||||
require(op != spv::OpImageSampleImplicitLod && op != spv::OpImageSampleDrefImplicitLod && op != spv::OpImageSampleDrefExplicitLod && op != spv::OpImageGather && op != spv::OpImageDrefGather && op != spv::OpImageQueryLod, "unnormalized samplers: the SPIR-V samples in a form an unnormalized sampler does not allow");
|
||||
if (op == spv::OpImageSampleExplicitLod) {
|
||||
require(count == 7u && words[cursor + 5] == spv::ImageOperandsLodMask, "unnormalized samplers: an explicit-LOD sample has operands other than Lod");
|
||||
++samples;
|
||||
}
|
||||
cursor += count;
|
||||
}
|
||||
require(samples != 0u, "unnormalized samplers: the SPIR-V does not sample");
|
||||
};
|
||||
|
||||
const auto lz = program(0xf09c0f08u);
|
||||
const auto accepted = recompile(lz, userData(unnormalized), 0x40000u);
|
||||
require(proven(accepted, true), "unnormalized samplers: image_sample_lz through an unnormalized S# was not flagged");
|
||||
auditSpirv(accepted.spirv.Words());
|
||||
const auto plain = recompile(lz, userData(normalized), 0x40000u);
|
||||
require(proven(plain, false), "unnormalized samplers: a normalized S# was flagged");
|
||||
require(plain.variantId == accepted.variantId && plain.spirv.Words() == accepted.spirv.Words(), "unnormalized samplers: the unnormalized S# compiled another variant");
|
||||
|
||||
for (const std::uint32_t mimg : {0xf0900f08u, 0xf0800f08u, 0xf0940f08u, 0xf0840f08u}) {
|
||||
const auto result = recompile(program(mimg), userData(unnormalized));
|
||||
require(proven(result, true), "unnormalized samplers: a compute image_sample_l, image_sample, image_sample_b or image_sample_cl was not flagged");
|
||||
auditSpirv(result.spirv.Words());
|
||||
}
|
||||
|
||||
const auto reject = [&](std::uint32_t mimg, const std::array<std::uint32_t, 16>& data, const char* reason, const char* what) {
|
||||
expectFailure([&] { static_cast<void>(recompile(program(mimg), data)); }, reason, what);
|
||||
};
|
||||
reject(0xf0c00f08u, userData(unnormalized), "unnormalized guest sampler is used with a texel offset, which is not implemented", "unnormalized samplers: image_sample_o was accepted");
|
||||
reject(0xf0bc0f08u, imageData(unnormalized, Type2D, Format32Float, 0u), "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: image_sample_c_lz was accepted");
|
||||
reject(0xf11c0108u, userData(unnormalized), "unnormalized guest sampler is used by a gather, which is not implemented", "unnormalized samplers: image_gather4_lz was accepted");
|
||||
reject(0xf1800308u, userData(unnormalized), "unnormalized guest sampler is used by image_get_lod, which is not implemented", "unnormalized samplers: image_get_lod was accepted");
|
||||
reject(0xf0880f08u, userData(unnormalized), "unnormalized guest sampler is used by a sample with derivatives, which is not implemented", "unnormalized samplers: image_sample_d was accepted");
|
||||
reject(0xf0800f09u, userData(unnormalized), "unnormalized guest sampler is used by an image_sample_*_a variant, which is not implemented", "unnormalized samplers: image_sample_a was accepted");
|
||||
reject(0xf09c0f18u, imageData(unnormalized, TypeCube, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a cube T# was accepted");
|
||||
reject(0xf09c0f10u, imageData(unnormalized, Type3D, Format8888UNorm, 3u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 3D T# was accepted");
|
||||
reject(0xf09c0f28u, imageData(unnormalized, Type2DArray, Format8888UNorm, 5u), "unnormalized guest sampler samples a 1D-array, 2D-array, 3D, cube or multisampled image", "unnormalized samplers: a 2D-array sample was accepted");
|
||||
reject(0xf09c0f08u, imageData(unnormalized, Type2D, Format11_11_10UInt, 0u), "unnormalized guest sampler samples an image that needs a format conversion or packed access", "unnormalized samplers: a T# with a format conversion was accepted");
|
||||
for (const std::uint32_t mimg : {0xf0c00f08u, 0xf11c0108u, 0xf09c0f18u}) {
|
||||
static_cast<void>(recompile(program(mimg), imageData(normalized, mimg == 0xf09c0f18u ? TypeCube : Type2D, Format8888UNorm, mimg == 0xf09c0f18u ? 5u : 0u)));
|
||||
}
|
||||
|
||||
const std::array<std::uint32_t, 5> pixelCode{0xf0800f08u, 0x00400801u, 0xf800180fu, 0x0b0a0908u, 0xbf810000u};
|
||||
const auto pixelData = userData(unnormalized);
|
||||
RecompileRequest pixel{};
|
||||
pixel.shader = {ShaderStage::Fragment, 0x4f000u, pixelCode, 0, {}};
|
||||
pixel.context.waveSize = 64;
|
||||
pixel.context.userDataBaseRegister = 0;
|
||||
pixel.context.userData = pixelData;
|
||||
pixel.context.pixel = twoParameterPixel();
|
||||
pixel.target.vulkanVersion = 0x00401000u;
|
||||
pixel.target.spirvVersion = 0x00010300u;
|
||||
pixel.target.subgroupSize = 64;
|
||||
pixel.layout.pushConstantSizeBytes = 128;
|
||||
pixel.useCache = false;
|
||||
expectFailure([&] { static_cast<void>(Recompile(pixel)); }, "unnormalized guest sampler is used by an implicit-LOD sample, which is not implemented", "unnormalized samplers: a pixel image_sample was accepted");
|
||||
}
|
||||
|
||||
void verifyUnusedUnnormalizedSampler() {
|
||||
using namespace ShaderRecompiler;
|
||||
ImageResource image{};
|
||||
image.resourceClass = ImageResourceClass::Sampled;
|
||||
image.numericClass = IrTextureNumericClass::Float;
|
||||
image.dimension = RdnaImageDimension::Dim2D;
|
||||
image.read = true;
|
||||
ShaderInfo info;
|
||||
info.images = {image};
|
||||
info.samplers = {SamplerResource{}};
|
||||
info.sampledPairs = {{0u, 0u, 0u}};
|
||||
ResourceSnapshot snapshot;
|
||||
snapshot.images = {DescriptorValue{{0x00001000u, 0x03800000u, 0x0000c000u, 0x90000facu, 0u, 0u, 0u, 0u}, 8u}};
|
||||
snapshot.samplers = {DescriptorValue{{0x00008092u, 0x00fff000u, 0x05500000u, 0u}, 4u}};
|
||||
const auto populate = [&](const ShaderInfo& shader) {
|
||||
BindingAllocationResult allocation;
|
||||
allocation.layout.descriptors = {{DescriptorBindingForImage(shader.images[0]), {0u}}, {DescriptorBindingKind::Samplers, {0u}}};
|
||||
DescriptorBindingBuilder{}.Populate(allocation, shader, IrShaderStage::Compute, 0u, snapshot, {});
|
||||
return allocation.bindings;
|
||||
};
|
||||
const auto bindings = populate(info);
|
||||
require(bindings.size() == 2u && bindings[0].imageUnnormalized == std::vector<bool>{true} && bindings[1].samplerUnnormalized == std::vector<bool>{true}, "unnormalized samplers: an S# without live uses was not flagged");
|
||||
auto selected = info;
|
||||
selected.images[0].indirectRoot = 0u;
|
||||
expectFailure([&] { static_cast<void>(populate(selected)); }, "unnormalized guest sampler samples an image selected at run time, which is not implemented", "unnormalized samplers: an image table root was accepted");
|
||||
auto compared = info;
|
||||
compared.samplers[0].depthCompare = true;
|
||||
expectFailure([&] { static_cast<void>(populate(compared)); }, "unnormalized guest sampler is used with depth comparison, which is not implemented", "unnormalized samplers: a depth-compare S# without live uses was accepted");
|
||||
snapshot.samplers[0].dwords[0] = 0x00000092u;
|
||||
const auto normalized = populate(info);
|
||||
require(normalized[0].imageUnnormalized == std::vector<bool>{false} && normalized[1].samplerUnnormalized == std::vector<bool>{false}, "unnormalized samplers: a normalized S# was flagged");
|
||||
}
|
||||
|
||||
void verifyWaveUniformValues() {
|
||||
using namespace ShaderRecompiler;
|
||||
IrProgram program;
|
||||
|
|
@ -893,6 +1313,78 @@ void verifyTwoLaneUniformValues() {
|
|||
require(multiplies(32u) == oneLane, "two-lane uniform values: a two-lane invocation computes a scalar value once per lane");
|
||||
}
|
||||
|
||||
void verifyBdaReadFallbackFunctions() {
|
||||
using namespace ShaderRecompiler;
|
||||
const std::array<std::uint32_t, 3> capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess};
|
||||
const std::array<std::string_view, 2> extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"};
|
||||
alignas(256) static std::array<std::uint32_t, 32> output{};
|
||||
const auto outputAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(output.data()));
|
||||
const std::array<std::uint32_t, 8> userData{0x10000000u, 0u, 0u, 0u, static_cast<std::uint32_t>(outputAddress), static_cast<std::uint32_t>((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu};
|
||||
const auto compile = [&](std::uint32_t loads, bool barrier, bool coherent) {
|
||||
std::vector<std::uint32_t> code{0x7e020200u, 0x7e040201u};
|
||||
for (std::uint32_t load = 0; load < loads; ++load) {
|
||||
code.push_back(0xdc308000u | (coherent ? 0x10000u : 0u) | (4u * load + 4u));
|
||||
code.push_back(((3u + load) << 24u) | 0x007d0001u);
|
||||
}
|
||||
code.push_back(0xbf8c3f70u);
|
||||
for (std::uint32_t load = 1; load < loads; ++load) code.push_back(0x4a060103u | ((3u + load) << 9u));
|
||||
if (barrier) code.push_back(0xbf8a0000u);
|
||||
code.insert(code.end(), {0xe0700000u, 0x80010300u, 0xbf810000u});
|
||||
RecompileRequest request{};
|
||||
request.shader = {ShaderStage::Compute, 0x50000u, code, 0, {}};
|
||||
request.context.waveSize = 32;
|
||||
request.context.userDataBaseRegister = 0;
|
||||
request.context.userData = userData;
|
||||
request.context.compute = ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u};
|
||||
request.target.vulkanVersion = 0x00401000u;
|
||||
request.target.spirvVersion = 0x00010300u;
|
||||
request.target.subgroupSize = 32;
|
||||
request.target.bdaAbiVersion = BdaAbi::Version;
|
||||
request.target.supportedCapabilities = capabilities;
|
||||
request.target.supportedExtensions = extensions;
|
||||
request.target.fragmentShaderBarycentricEnabled = false;
|
||||
request.layout.pushConstantSizeBytes = 128;
|
||||
request.useCache = false;
|
||||
return Recompile(request).spirv;
|
||||
};
|
||||
for (const bool barrier : {false, true}) {
|
||||
for (const bool coherent : {false, true}) {
|
||||
const auto one = compile(1u, barrier, coherent);
|
||||
const auto five = compile(5u, barrier, coherent);
|
||||
std::map<std::uint32_t, std::string> names;
|
||||
std::map<std::string, std::uint32_t> definitions;
|
||||
const auto reader = std::string("read_bda_dword_bytes") + (barrier ? "" : "_stop") + (coherent ? "_coherent" : "");
|
||||
std::string function;
|
||||
std::size_t compareExchanges = 0;
|
||||
std::size_t mainLookups = 0;
|
||||
std::array<std::size_t, 2> readerLoads{};
|
||||
for (std::size_t cursor = 5; cursor < five.size();) {
|
||||
const auto length = five[cursor] >> 16u;
|
||||
require(length != 0 && length <= five.size() - cursor, "BDA read functions: truncated SPIR-V instruction");
|
||||
const auto op = five[cursor] & 0xffffu;
|
||||
if (op == spv::OpName) names[five[cursor + 1]] = reinterpret_cast<const char*>(&five[cursor + 2]);
|
||||
if (op == spv::OpFunction) {
|
||||
function = names[five[cursor + 2]];
|
||||
if (function == "record_bda_fault" || function.starts_with("read_bda_dword_bytes")) {
|
||||
require((five[cursor + 3] & spv::FunctionControlDontInlineMask) != 0u, "BDA read functions: a fault or byte read function may be inlined");
|
||||
++definitions[function];
|
||||
}
|
||||
}
|
||||
if (op == spv::OpAtomicCompareExchange) ++compareExchanges;
|
||||
if (op == spv::OpFunctionCall && function == "main" && names[five[cursor + 3]] == "get_bda_pointer") ++mainLookups;
|
||||
if (op == spv::OpLoad && function == reader) ++readerLoads[length > 4u && (five[cursor + 4] & spv::MemoryAccessVolatileMask) != 0u];
|
||||
cursor += length;
|
||||
}
|
||||
require(definitions["record_bda_fault"] == 1u && definitions[reader] == 1u, "BDA read functions: the fault and byte read functions are not defined");
|
||||
for (const auto& [name, count] : definitions) require(count == 1u, "BDA read functions: a function is defined twice");
|
||||
require(compareExchanges == 1u, "BDA read functions: a fault is recorded outside record_bda_fault");
|
||||
require(mainLookups == 0u, "BDA read functions: a read site looks up its bytes inline");
|
||||
require(readerLoads[coherent] == 4u && readerLoads[!coherent] == 0u, "BDA read functions: the byte loads do not keep the access's coherence");
|
||||
require(five.size() - one.size() < 4u * 300u, "BDA read functions: a read site takes 300 SPIR-V words or more");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void verifyFunctionLdsBound() {
|
||||
using namespace ShaderRecompiler;
|
||||
const auto build = [](const auto& body) {
|
||||
|
|
@ -1042,12 +1534,19 @@ int main() {
|
|||
verifyBindlessTable();
|
||||
verifyDescriptorPhis();
|
||||
verifyProgramCounterRelativeData();
|
||||
verifyLanesOutsideHostSubgroup();
|
||||
verifyMeshConfiguration();
|
||||
verifyPixelInputs();
|
||||
verifyPixelRequestSerialization();
|
||||
verifyLegacyPixelRequests();
|
||||
verifyPixelExportReplay();
|
||||
verifyPixelParameterSlots();
|
||||
verifyComputedTexelOffsets();
|
||||
verifyUnnormalizedSamplers();
|
||||
verifyUnusedUnnormalizedSampler();
|
||||
verifyWaveUniformValues();
|
||||
verifyTwoLaneUniformValues();
|
||||
verifyBdaReadFallbackFunctions();
|
||||
verifyFunctionLdsBound();
|
||||
#if ANYPS5_ENABLE_SPIRV_TOOLS
|
||||
const std::vector<std::uint32_t> minimalSpirv{
|
||||
|
|
@ -1142,6 +1641,9 @@ int main() {
|
|||
auto offsets = uncached;
|
||||
offsets.target.nonConstantImageOffsets = true;
|
||||
require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(offsets)).request.target.nonConstantImageOffsets, "non-constant texel offsets were lost in serialization");
|
||||
auto srgb = uncached;
|
||||
srgb.target.srgbDecodeFormats = 3u;
|
||||
require(RequestSerializer{}.Deserialize(RequestSerializer{}.Serialize(srgb)).request.target.srgbDecodeFormats == 3u, "the sRGB formats decoded in the shader were lost in serialization");
|
||||
auto changedLayout = request;
|
||||
changedLayout.layout.pushConstantSizeBytes = 64;
|
||||
require(!Recompile(changedLayout).cacheHit, "binding layout change reused an incompatible variant");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,203 @@
|
|||
#include "SpirvBackend/SpirvModule.hpp"
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <spirv/unified1/spirv.hpp>
|
||||
#include <vector>
|
||||
|
||||
using namespace ShaderRecompiler;
|
||||
|
||||
namespace {
|
||||
|
||||
int failures = 0;
|
||||
|
||||
void check(bool condition, const char* what) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "%s\n", what);
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::uint32_t, std::size_t> opcodeCounts(const std::vector<std::uint32_t>& words) {
|
||||
std::map<std::uint32_t, std::size_t> counts;
|
||||
std::size_t offset = 5;
|
||||
while (offset < words.size()) {
|
||||
const auto header = words[offset];
|
||||
const auto wordCount = header >> spv::WordCountShift;
|
||||
if (wordCount == 0 || offset + wordCount > words.size()) {
|
||||
return counts;
|
||||
}
|
||||
++counts[header & 0xffffu];
|
||||
offset += wordCount;
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
std::size_t declared(const SpirvModule& module, std::uint32_t opcode) {
|
||||
const auto counts = opcodeCounts(module.Finalize());
|
||||
const auto found = counts.find(opcode);
|
||||
return found == counts.end() ? 0 : found->second;
|
||||
}
|
||||
|
||||
void testRepeatedTypesShareOneId() {
|
||||
SpirvModule module;
|
||||
const auto first = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
const auto second = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
check(first == second, "a repeated OpTypeInt 32 0 returned a different id");
|
||||
check(declared(module, spv::OpTypeInt) == 1, "a repeated OpTypeInt 32 0 was declared more than once");
|
||||
check(module.Type(spv::OpTypeVoid) == module.Type(spv::OpTypeVoid), "a repeated OpTypeVoid returned a different id");
|
||||
check(declared(module, spv::OpTypeVoid) == 1, "OpTypeVoid was not declared exactly once");
|
||||
check(module.Type(spv::OpTypeBool) == module.Type(spv::OpTypeBool), "a repeated OpTypeBool returned a different id");
|
||||
check(module.Type(spv::OpTypeFloat, 32u) == module.Type(spv::OpTypeFloat, 32u), "a repeated OpTypeFloat 32 returned a different id");
|
||||
check(declared(module, spv::OpTypeFloat) == 1, "OpTypeFloat was not declared exactly once");
|
||||
}
|
||||
|
||||
void testOperandShapesDoNotCollide() {
|
||||
SpirvModule module;
|
||||
const auto oneWord = module.Type(spv::OpTypeInt, 32u);
|
||||
const auto twoWords = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
check(oneWord != twoWords, "OpTypeInt with one operand collided with OpTypeInt with two");
|
||||
check(declared(module, spv::OpTypeInt) == 2, "the two OpTypeInt shapes were not both declared");
|
||||
const auto voidType = module.Type(spv::OpTypeVoid);
|
||||
const auto boolType = module.Type(spv::OpTypeBool);
|
||||
check(voidType != boolType, "OpTypeVoid collided with OpTypeBool");
|
||||
check(module.Type(spv::OpTypeInt, 32u, 0u) != module.Type(spv::OpTypeInt, 32u, 1u), "unsigned and signed OpTypeInt 32 collided");
|
||||
check(module.Type(spv::OpTypeInt, 32u, 0u) != module.Type(spv::OpTypeInt, 64u, 0u), "OpTypeInt 32 collided with OpTypeInt 64");
|
||||
check(module.Type(spv::OpTypeFloat, 32u) != module.Type(spv::OpTypeFloat, 64u), "OpTypeFloat 32 collided with OpTypeFloat 64");
|
||||
}
|
||||
|
||||
void testOperandKindsAgree() {
|
||||
SpirvModule module;
|
||||
const auto fromUnsigned = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
const auto fromSigned = module.Type(spv::OpTypeInt, static_cast<std::int32_t>(32), static_cast<std::int32_t>(0));
|
||||
check(fromUnsigned == fromSigned, "OpTypeInt disagreed between unsigned and signed operands");
|
||||
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
const auto fromEnum = module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32);
|
||||
const auto fromWord = module.Type(spv::OpTypePointer, static_cast<std::uint32_t>(spv::StorageClassFunction), typeU32);
|
||||
check(fromEnum == fromWord, "OpTypePointer disagreed between enum and integer operands");
|
||||
check(fromEnum == module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32), "a repeated enum-operand OpTypePointer returned a different id");
|
||||
check(declared(module, spv::OpTypePointer) == 1, "OpTypePointer was not declared exactly once");
|
||||
}
|
||||
|
||||
void testDecoratedTypesStayDistinct() {
|
||||
SpirvModule module;
|
||||
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
const auto plain = module.Type(spv::OpTypeRuntimeArray, typeU32);
|
||||
const auto decorated = module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, typeU32);
|
||||
check(plain != decorated, "a plain OpTypeRuntimeArray collided with the decorated one");
|
||||
check(decorated == module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, typeU32), "a repeated decorated type returned a different id");
|
||||
check(declared(module, spv::OpTypeRuntimeArray) == 2, "the plain and decorated OpTypeRuntimeArray were not both declared");
|
||||
}
|
||||
|
||||
void testConstantsShareOneId() {
|
||||
SpirvModule module;
|
||||
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
const auto typeBool = module.Type(spv::OpTypeBool);
|
||||
check(module.Constant(spv::OpConstant, typeU32, 0u) == module.Constant(spv::OpConstant, typeU32, 0u), "a repeated OpConstant 0 returned a different id");
|
||||
check(module.Constant(spv::OpConstant, typeU32, 0u) != module.Constant(spv::OpConstant, typeU32, 1u), "OpConstant 0 collided with OpConstant 1");
|
||||
check(module.Constant(spv::OpConstant, typeU32, 1000u) != module.Constant(spv::OpConstant, typeU32, 0u), "OpConstant 1000 collided with OpConstant 0");
|
||||
check(module.Constant(spv::OpConstantTrue, typeBool) == module.Constant(spv::OpConstantTrue, typeBool), "a repeated OpConstantTrue returned a different id");
|
||||
check(module.Constant(spv::OpConstantTrue, typeBool) != module.Constant(spv::OpConstantFalse, typeBool), "OpConstantTrue collided with OpConstantFalse");
|
||||
check(declared(module, spv::OpConstant) == 3, "OpConstant was not declared once per distinct value");
|
||||
check(declared(module, spv::OpConstantTrue) == 1, "OpConstantTrue was not declared exactly once");
|
||||
const auto signedId = module.Constant(spv::OpConstant, module.Type(spv::OpTypeInt, 32u, 1u), static_cast<std::uint32_t>(-1));
|
||||
check(signedId != 0, "a signed OpConstant produced a zero id");
|
||||
}
|
||||
|
||||
void testOperandFreeConstantsShareOneId() {
|
||||
SpirvModule module;
|
||||
const auto typeBool = module.Type(spv::OpTypeBool);
|
||||
const auto typeVoid = module.Type(spv::OpTypeVoid);
|
||||
check(module.Constant(spv::OpConstantTrue, typeBool) == module.Constant(spv::OpConstantTrue, typeBool), "a repeated OpConstantTrue returned a different id");
|
||||
check(module.Constant(spv::OpConstantTrue, typeBool) != module.Constant(spv::OpConstantFalse, typeBool), "OpConstantTrue collided with OpConstantFalse");
|
||||
check(module.Constant(spv::OpConstantTrue, typeVoid) != module.Constant(spv::OpConstantTrue, typeBool), "OpConstantTrue of different types collided");
|
||||
check(declared(module, spv::OpConstantTrue) == 2, "OpConstantTrue was not declared once per type");
|
||||
check(declared(module, spv::OpConstantFalse) == 1, "OpConstantFalse was not declared exactly once");
|
||||
check(module.Type(spv::OpTypeVoid) == typeVoid, "a repeated operand-free type returned a different id");
|
||||
}
|
||||
|
||||
void testIdsSurviveInterleavedAllocation() {
|
||||
SpirvModule module;
|
||||
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
const auto constant = module.Constant(spv::OpConstant, typeU32, 4u);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
check(module.AllocateId() != 0, "AllocateId returned zero");
|
||||
check(module.Type(spv::OpTypeInt, 32u, 0u) == typeU32, "a memoized type changed id after AllocateId");
|
||||
check(module.Constant(spv::OpConstant, typeU32, 4u) == constant, "a memoized constant changed id after AllocateId");
|
||||
}
|
||||
}
|
||||
|
||||
void testModulesAreIndependent() {
|
||||
SpirvModule first;
|
||||
SpirvModule second;
|
||||
const auto firstId = first.Type(spv::OpTypeInt, 32u, 0u);
|
||||
const auto secondId = second.Type(spv::OpTypeInt, 32u, 0u);
|
||||
check(firstId == 1, "the first id of a fresh module was not 1");
|
||||
check(secondId == 1, "a second module did not start its ids from 1");
|
||||
first.Type(spv::OpTypeVoid);
|
||||
check(second.Type(spv::OpTypeInt, 32u, 0u) == secondId, "a second module's ids moved when the first module was used");
|
||||
}
|
||||
|
||||
void testIdsStayUniqueBeyondCacheCapacity() {
|
||||
SpirvModule module;
|
||||
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
std::vector<std::uint32_t> ids;
|
||||
ids.reserve(4096);
|
||||
for (std::uint32_t value = 0; value < 4096; ++value) {
|
||||
ids.push_back(module.Constant(spv::OpConstant, typeU32, value));
|
||||
}
|
||||
const std::set<std::uint32_t> distinct(ids.begin(), ids.end());
|
||||
check(distinct.size() == ids.size(), "distinct constants shared an id once the cache was overrun");
|
||||
for (std::uint32_t value = 0; value < 4096; ++value) {
|
||||
if (module.Constant(spv::OpConstant, typeU32, value) != ids[value]) {
|
||||
std::fprintf(stderr, "constant %u changed id after the cache was overrun\n", value);
|
||||
++failures;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::uint32_t largest = 0;
|
||||
for (const auto id : distinct) {
|
||||
largest = largest < id ? id : largest;
|
||||
}
|
||||
const auto first = ids.front();
|
||||
check(largest - first + 1 == distinct.size(), "the constant ids were not contiguous, so some were never declared");
|
||||
check(first > typeU32, "a constant was numbered before the type it uses");
|
||||
}
|
||||
|
||||
void testEmittedWordsAreStable() {
|
||||
SpirvModule module;
|
||||
const auto typeU32 = module.Type(spv::OpTypeInt, 32u, 0u);
|
||||
for (int i = 0; i < 32; ++i) {
|
||||
module.Type(spv::OpTypeVector, typeU32, 4u);
|
||||
module.Type(spv::OpTypePointer, spv::StorageClassFunction, typeU32);
|
||||
module.Constant(spv::OpConstant, typeU32, static_cast<std::uint32_t>(i % 8));
|
||||
module.AddFunction(spv::OpNop);
|
||||
}
|
||||
const auto words = module.Finalize();
|
||||
check(!words.empty(), "Finalize produced no words");
|
||||
const auto counts = opcodeCounts(words);
|
||||
check(counts.at(spv::OpTypeVector) == 1, "OpTypeVector was declared more than once");
|
||||
check(counts.at(spv::OpTypePointer) == 1, "OpTypePointer was declared more than once");
|
||||
check(counts.at(spv::OpConstant) == 8, "OpConstant was not declared once per distinct value");
|
||||
check(counts.at(spv::OpNop) == 32, "the function body lost instructions");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
testRepeatedTypesShareOneId();
|
||||
testOperandShapesDoNotCollide();
|
||||
testOperandKindsAgree();
|
||||
testDecoratedTypesStayDistinct();
|
||||
testConstantsShareOneId();
|
||||
testOperandFreeConstantsShareOneId();
|
||||
testIdsSurviveInterleavedAllocation();
|
||||
testModulesAreIndependent();
|
||||
testIdsStayUniqueBeyondCacheCapacity();
|
||||
testEmittedWordsAreStable();
|
||||
if (failures != 0) {
|
||||
std::fprintf(stderr, "%d check(s) failed\n", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -11,7 +11,9 @@
|
|||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
|
@ -347,6 +349,61 @@ void testWaitFreeSubmissionTheCpuWaitsFor() {
|
|||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
|
||||
void testMultiSubmissions() {
|
||||
alignas(64) static volatile std::uint32_t value = 0, done = 0;
|
||||
check(sceAgcDriverSubmitMultiDcbs(nullptr, nullptr, 0) == 0, "empty multi-DCB submit failed");
|
||||
check(sceAgcDriverAgrSubmitMultiDcbs(nullptr, nullptr, 0) == 0, "empty AGR multi-DCB submit failed");
|
||||
std::array<std::uint32_t, 3> invalid{0xc0017600, 0x20c, 0};
|
||||
std::array<std::uint32_t*, 1> invalidAddresses{invalid.data()};
|
||||
std::array<std::uint32_t, 1> invalidSizes{2};
|
||||
expectFailure([&] { sceAgcDriverSubmitMultiDcbs(nullptr, invalidSizes.data(), 1); });
|
||||
expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(invalidAddresses.data(), nullptr, 1); });
|
||||
expectFailure([&] { sceAgcDriverSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), 1); });
|
||||
expectFailure([&] { sceAgcDriverAgrSubmitMultiDcbs(invalidAddresses.data(), invalidSizes.data(), 1); });
|
||||
for (bool agr : {false, true}) {
|
||||
value = 0;
|
||||
done = 0;
|
||||
auto first = writeData(&value, 1);
|
||||
auto second = writeData(&value, 2);
|
||||
auto third = writeData(&done, 1);
|
||||
std::array<std::uint32_t*, 3> addresses{first.data(), second.data(), third.data()};
|
||||
std::array<std::uint32_t, 3> sizes{static_cast<std::uint32_t>(first.size()), static_cast<std::uint32_t>(second.size()), static_cast<std::uint32_t>(third.size())};
|
||||
const int result = agr ? sceAgcDriverAgrSubmitMultiDcbs(addresses.data(), sizes.data(), 3) : sceAgcDriverSubmitMultiDcbs(addresses.data(), sizes.data(), 3);
|
||||
check(result == 0, "multi-DCB submit failed");
|
||||
waitFor(&done, 1, "the last DCB of a multi-DCB submit never ran");
|
||||
check(value == 2, "multi-DCB submit did not run its DCBs in order");
|
||||
AgcDriverWaitIdle_nid_postfix();
|
||||
}
|
||||
}
|
||||
|
||||
void testShaderHeaderAlignment() {
|
||||
alignas(256) static const std::array<std::uint32_t, 64> code{0xbf810000};
|
||||
alignas(8) static std::array<std::byte, 2 * sizeof(Shader)> storage{};
|
||||
Shader shader{};
|
||||
shader.file_header = 0x34333231;
|
||||
shader.version = 0x18;
|
||||
shader.header_size = sizeof(Shader);
|
||||
shader.shader_size = sizeof(code);
|
||||
shader.code = code.data();
|
||||
const auto at = [](std::size_t offset, const Shader& fields) {
|
||||
std::memcpy(storage.data() + offset, &fields, sizeof(fields));
|
||||
return reinterpret_cast<const Shader*>(storage.data() + offset);
|
||||
};
|
||||
for (const std::size_t offset : {0, 4, 1}) AgcDriverRegisterShader_nid_postfix(at(offset, shader));
|
||||
const auto refused = [](const std::string& message, const char* reason) { check(message.find(reason) != std::string::npos, message.c_str()); };
|
||||
refused(expectFailure([] { AgcDriverRegisterShader_nid_postfix(nullptr); }), "null or misaligned address");
|
||||
refused(expectFailure([] { AgcDriverRegisterShader_nid_postfix(reinterpret_cast<const Shader*>(0x1001)); }), "not readable");
|
||||
Shader misplaced = shader;
|
||||
misplaced.code = code.data() + 1;
|
||||
refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, misplaced)); }), "null or misaligned address");
|
||||
Shader older = shader;
|
||||
older.version = 0x17;
|
||||
refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(1, older)); }), "invalid shader header");
|
||||
Shader truncated = shader;
|
||||
truncated.header_size = sizeof(Shader) - 4;
|
||||
refused(expectFailure([&] { AgcDriverRegisterShader_nid_postfix(at(4, truncated)); }), "smaller than its fixed fields");
|
||||
}
|
||||
|
||||
void testWorkerFailure() {
|
||||
std::array<std::uint32_t, 5> words{0xc0031500, 1, 1, 1, 0x41};
|
||||
Packet packet{words.data(), static_cast<std::uint32_t>(words.size()), 0, {}};
|
||||
|
|
@ -442,6 +499,8 @@ int main() {
|
|||
testWaitFreeSubmissionQueue0WaitsOn();
|
||||
testWaitFreeSubmissionBehindHeldOne();
|
||||
testWaitFreeSubmissionTheCpuWaitsFor();
|
||||
testMultiSubmissions();
|
||||
testShaderHeaderAlignment();
|
||||
testWorkerFailure();
|
||||
check(expectFailure([] { LibcRunShutdown_nid_postfix(); }).find("required shader register") != std::string::npos, "shutdown lost worker failure");
|
||||
std::puts("AGC driver submit tests passed");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
#include "GraphicsTests.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp"
|
||||
#include "RdnaDecoder/RdnaDescriptorFormat.hpp"
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
|
|
@ -7,6 +12,38 @@ namespace {
|
|||
|
||||
using namespace AgcDriver::Graphics;
|
||||
|
||||
VkFormat unsampledFormats[2]{};
|
||||
|
||||
void unsampledFormatProperties(VkPhysicalDevice, VkFormat format, VkFormatProperties* properties) {
|
||||
*properties = {};
|
||||
if (format != unsampledFormats[0] && format != unsampledFormats[1]) properties->optimalTilingFeatures = VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT;
|
||||
}
|
||||
|
||||
std::uint32_t decodedWithout(VkFormat first = VK_FORMAT_UNDEFINED, VkFormat second = VK_FORMAT_UNDEFINED) {
|
||||
unsampledFormats[0] = first;
|
||||
unsampledFormats[1] = second;
|
||||
return SrgbDecodeFormats(unsampledFormatProperties, VK_NULL_HANDLE);
|
||||
}
|
||||
|
||||
void srgbDecodeTests() {
|
||||
constexpr std::uint32_t srgb8 = 1u;
|
||||
constexpr std::uint32_t srgb8_8 = 2u;
|
||||
Require(decodedWithout() == 0u, "a device that samples every sRGB format needs no shader decode");
|
||||
Require(decodedWithout(VK_FORMAT_R8G8_SRGB) == srgb8_8, "a device without sampled R8G8_SRGB must decode 8_8_SRGB in the shader");
|
||||
Require(decodedWithout(VK_FORMAT_R8_SRGB) == srgb8, "a device without sampled R8_SRGB must decode 8_SRGB in the shader");
|
||||
Require(decodedWithout(VK_FORMAT_R8_SRGB, VK_FORMAT_R8G8_SRGB) == (srgb8 | srgb8_8), "a device without either 8-bit sRGB format must decode both in the shader");
|
||||
Require(decodedWithout(VK_FORMAT_R8G8_SRGB, VK_FORMAT_R8G8_UNORM) == 0u, "8_8_SRGB without a sampled UNORM view must not be decoded in the shader");
|
||||
Require(decodedWithout(VK_FORMAT_R8G8B8A8_SRGB, VK_FORMAT_BC1_RGBA_SRGB_BLOCK) == 0u, "only the 8 and 8_8 sRGB formats may be decoded in the shader");
|
||||
Context context{};
|
||||
Require(SampledTextureFormat(context, 129) == VK_FORMAT_R8G8_SRGB && SampledTextureFormat(context, 128) == VK_FORMAT_R8_SRGB, "sRGB textures must keep their sRGB views without shader decode");
|
||||
context.srgbDecodeFormats = srgb8_8;
|
||||
Require(SampledTextureFormat(context, 129) == VK_FORMAT_R8G8_UNORM, "8_8_SRGB decoded in the shader must be viewed as R8G8_UNORM");
|
||||
Require(SampledTextureFormat(context, 128) == VK_FORMAT_R8_SRGB, "8_SRGB must keep its sRGB view when only 8_8_SRGB is decoded in the shader");
|
||||
context.srgbDecodeFormats = srgb8 | srgb8_8;
|
||||
Require(SampledTextureFormat(context, 128) == VK_FORMAT_R8_UNORM, "8_SRGB decoded in the shader must be viewed as R8_UNORM");
|
||||
Require(SampledTextureFormat(context, 130) == VK_FORMAT_R8G8B8A8_SRGB && SampledTextureFormat(context, 14) == VK_FORMAT_R8G8_UNORM && SampledTextureFormat(context, 170) == VK_FORMAT_BC1_RGBA_SRGB_BLOCK, "formats without a shader decode must keep their views");
|
||||
}
|
||||
|
||||
template<typename TAction>
|
||||
void reject(TAction action, std::string_view reason) {
|
||||
try {
|
||||
|
|
@ -18,9 +55,27 @@ void reject(TAction action, std::string_view reason) {
|
|||
throw std::runtime_error(std::string("expected texture format rejection: ") + std::string(reason));
|
||||
}
|
||||
|
||||
void convertedDccClearTests() {
|
||||
alignas(64) std::array<std::uint8_t, 16> keys{};
|
||||
GuestTextureResource resource{};
|
||||
resource.dccAddress = reinterpret_cast<std::uint64_t>(keys.data());
|
||||
const auto read = [&](std::uint32_t format, std::uint8_t key) {
|
||||
keys.fill(key);
|
||||
resource.format = format;
|
||||
return TextureClearKeys(resource, keys.size() * 256u);
|
||||
};
|
||||
for (const std::uint32_t format : {30u, 34u}) {
|
||||
reject([&] { read(format, 0x40); }, "DCC clear code 0001 of converted texture format " + std::to_string(format));
|
||||
reject([&] { read(format, 0x80); }, "DCC clear code 1110 of converted texture format " + std::to_string(format));
|
||||
Require(read(format, 0x00) == DccKeys::Clear0000 && read(format, 0xc0) == DccKeys::Clear1111, "converted texture format " + std::to_string(format) + " must keep its 0000 and 1111 DCC clear codes");
|
||||
}
|
||||
Require(read(20, 0x40) == DccKeys::Clear0001 && read(20, 0x80) == DccKeys::Clear1110, "format 20 must keep its 0001 and 1110 DCC clear codes");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
void RunTextureFormatTests() {
|
||||
srgbDecodeTests();
|
||||
Require(ResolveTextureFormat(1) == VK_FORMAT_R8_UNORM, "format 1 must resolve to R8_UNORM");
|
||||
Require(BytesPerElement(1) == 1u, "format 1 must be one byte wide");
|
||||
Require(!IsBlockCompressed(1), "format 1 must not be block compressed");
|
||||
|
|
@ -29,6 +84,9 @@ void RunTextureFormatTests() {
|
|||
Require(ResolveTextureFormat(56) == VK_FORMAT_R8G8B8A8_UNORM, "format 56 must resolve to R8G8B8A8_UNORM");
|
||||
Require(BytesPerElement(56) == 4u, "format 56 must be four bytes wide");
|
||||
|
||||
Require(ResolveTextureFormat(6) == VK_FORMAT_R8_SINT, "format 6 must resolve to R8_SINT");
|
||||
Require(BytesPerElement(6) == 1u, "format 6 must be one byte wide");
|
||||
|
||||
Require(ResolveTextureFormat(22) == VK_FORMAT_R32_SFLOAT, "format 22 must resolve to R32_SFLOAT");
|
||||
Require(BytesPerElement(22) == 4u, "format 22 must be four bytes wide");
|
||||
|
||||
|
|
@ -45,10 +103,18 @@ void RunTextureFormatTests() {
|
|||
|
||||
Require(ResolveTextureFormat(34) == ResolveTextureFormat(20), "format 34 must remap to format 20");
|
||||
Require(BytesPerElement(34) == BytesPerElement(20), "remapped format 34 must share the width of format 20");
|
||||
Require(ResolveTextureFormat(30) == ResolveTextureFormat(20), "format 30 must remap to format 20");
|
||||
Require(BytesPerElement(30) == 4u, "remapped format 30 must be four bytes wide");
|
||||
for (std::uint32_t format = 0; format < 512u; ++format) {
|
||||
const auto remapped = static_cast<std::uint32_t>(ShaderRecompiler::RemapTextureFormat(static_cast<ShaderRecompiler::IrBufferFormat>(format)));
|
||||
if (remapped == format) continue;
|
||||
Require(ResolveTextureFormat(format) == ResolveTextureFormat(remapped), "guest format " + std::to_string(format) + " must resolve like the format the recompiler remaps it to");
|
||||
}
|
||||
|
||||
reject([] { ResolveTextureFormat(0); }, "unsupported guest texture format");
|
||||
reject([] { ResolveTextureFormat(183); }, "unsupported guest texture format");
|
||||
reject([] { ResolveTextureFormat(9999); }, "unsupported guest texture format");
|
||||
reject([] { BytesPerElement(2); }, "unsupported guest texture format");
|
||||
reject([] { IsBlockCompressed(200); }, "unsupported guest texture format");
|
||||
convertedDccClearTests();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,188 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Inputs = 8;
|
||||
constexpr std::uint32_t Results = 32;
|
||||
constexpr std::uint32_t Loads = 24;
|
||||
constexpr std::uint32_t Stored = 7;
|
||||
constexpr std::uint32_t ScratchWords = 16;
|
||||
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * ScratchWords> Scratch{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 50> Code{
|
||||
0x34040086, 0x34060087, 0x34500085, 0xe0381000, 0x80023228, 0xe0381010, 0x80023628, 0xbf8c3f70,
|
||||
0xe0001000, 0x80020a28, 0xe0041004, 0x80030b28, 0xe0081008, 0x80040d28, 0xe00c100c, 0x80051028,
|
||||
0xe8381002, 0x80021428, 0xe9211004, 0x80021528, 0xe99a1000, 0x80021728, 0xe00c1008, 0x80061a28,
|
||||
0xe00c100c, 0x80071e28, 0xe0101000, 0x80083602, 0xe0141004, 0x80093702, 0xe018100c, 0x800a3602,
|
||||
0xe01c1010, 0x800b3602, 0xe01c1018, 0x80083602, 0xbf8c3f70, 0xe0781000, 0x80010a03, 0xe0781010,
|
||||
0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xe0781040, 0x80011a03, 0xe0781050,
|
||||
0x80011e03, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::uint32_t Rows[32][8] = {
|
||||
{0xff807f00u, 0x80000001u, 0x7fff8001u, 0xffff0000u, 0x3f800000u, 0xbf800000u, 0x3f000000u, 0x80000000u},
|
||||
{0x00000000u, 0x7c0007c0u, 0xffe00000u, 0x7ff00000u, 0x7f800000u, 0xff800000u, 0x7fc00001u, 0x0da24260u},
|
||||
{0x81018101u, 0x00010000u, 0x00008000u, 0x0001ffffu, 0x3b808081u, 0x3b008081u, 0x37800080u, 0xb7800080u},
|
||||
{0x200001ffu, 0x3ff003ffu, 0x07ff07ffu, 0xc0000000u, 0x3f7f0000u, 0x3effffffu, 0xbf7fffffu, 0x40000000u},
|
||||
{0x52e6b438u, 0xf2a74de4u, 0x269e0d37u, 0x6513270eu, 0xbf8e4b0fu, 0xbeb2cb7fu, 0x7f800000u, 0x80000000u},
|
||||
{0x11e20b8fu, 0x3d9c1724u, 0x1738f7d9u, 0x8d116eceu, 0x3f598d83u, 0xbf800000u, 0x3e5cf891u, 0xff800000u},
|
||||
{0xf9ebdaccu, 0x3898d190u, 0x0becd7b0u, 0x8e81973eu, 0xbf0c093bu, 0x80000000u, 0x3e206dd1u, 0xbf841a62u},
|
||||
{0x301850c5u, 0x5f557203u, 0x18f135d2u, 0x8c38fb29u, 0x3e2b5fc8u, 0xbc18bc9du, 0x3f388602u, 0x3f8cebdau},
|
||||
{0x3f98e277u, 0xcb5c7427u, 0x2e05319au, 0xb2f14c94u, 0xbf8b289du, 0xbc500907u, 0xbe08394eu, 0xbf8e09ecu},
|
||||
{0x2a3af4d4u, 0xc1d3fcffu, 0x5790f82eu, 0x26e87555u, 0xbe507866u, 0xbf8c9162u, 0x3f406bccu, 0xbed4d441u},
|
||||
{0x7f26144bu, 0x9474031bu, 0xcc011cddu, 0x74c9df6au, 0x80000000u, 0xbd89ebd2u, 0xbf92358fu, 0x3ec3dba1u},
|
||||
{0xd269a9a5u, 0x72158370u, 0x48db40afu, 0xb774eb52u, 0x3ee082b0u, 0xc0000000u, 0x3e93a7f6u, 0xbf3b8f97u},
|
||||
{0xbd0561e6u, 0x3f63af83u, 0x65dc9f50u, 0x6415479cu, 0xbc14cf1du, 0x40000000u, 0x3f7f2e26u, 0x3f7244a0u},
|
||||
{0x6a50df4du, 0xfc891b4au, 0x5bd86d40u, 0xaec6f024u, 0x3f98553cu, 0x3f800000u, 0x0da24260u, 0xc0000000u},
|
||||
{0x2eae05cfu, 0x43435cc5u, 0x482c9cbcu, 0x010c4759u, 0x7f800000u, 0x3e30a07bu, 0x3efd95cdu, 0x3e9c8a1bu},
|
||||
{0x0dd27a65u, 0x74e69a5du, 0xe647cb8fu, 0xdef88334u, 0x3f7946a6u, 0xbe8f43e4u, 0xbf83f15fu, 0x7fcd5c60u},
|
||||
{0x298cb3a5u, 0x1c2442f9u, 0x570dc195u, 0x99c94309u, 0x00000000u, 0x3dc2fcb8u, 0x3e976834u, 0xbf800000u},
|
||||
{0x2607679du, 0xa268aa87u, 0x4093f6deu, 0xf4998d7cu, 0xbeb4d35cu, 0x3eaaaaabu, 0x3f9f04feu, 0xbefa7653u},
|
||||
{0xbfeaa155u, 0x57b6fb7eu, 0xbd87a865u, 0x43c71b9au, 0x3effaa7du, 0xbf44357au, 0xbeb808feu, 0x3f89d3e5u},
|
||||
{0x4c4f9b06u, 0xfa7f0eabu, 0xa49636a2u, 0xdd02de92u, 0x3eaaaaabu, 0xbf000000u, 0xbec018e6u, 0x7f800000u},
|
||||
{0x5464ecc2u, 0xa2eddbbdu, 0x39194242u, 0x9cfc8652u, 0x3fa16226u, 0x3f4bb9d5u, 0x3f1fa8d4u, 0x7f800000u},
|
||||
{0xbb2313f5u, 0x076b3e36u, 0xfd56a926u, 0x0726e25cu, 0xbd93d0edu, 0xff800000u, 0xbe0c8044u, 0x3fa26b48u},
|
||||
{0x5d58c705u, 0x149e259bu, 0x38703800u, 0x1a26f889u, 0xbf800000u, 0xbd38bc43u, 0x3e92c7eeu, 0x0da24260u},
|
||||
{0xa4a45effu, 0x15b40aebu, 0xd5ab8b4du, 0xa91c2439u, 0x40000000u, 0x3f267e55u, 0xbf55f9d7u, 0xbedef3ffu},
|
||||
{0xf8be8831u, 0xb8c9817au, 0x6555abfeu, 0x7691b06fu, 0x3f94b1adu, 0xbf5ba544u, 0x3f800000u, 0xbdb87bd0u},
|
||||
{0x9c9011efu, 0xd39630d6u, 0x988af3fbu, 0xfaf55496u, 0x3f9196d5u, 0x7f800000u, 0x00000000u, 0x3f16ac02u},
|
||||
{0xbfdefc15u, 0xef02090bu, 0x23a5ef88u, 0x6f0e2289u, 0xbf4b2333u, 0xbf9d156du, 0x7f800000u, 0xff800000u},
|
||||
{0x8b5ab3eeu, 0x6b446806u, 0xd58dcdb4u, 0x218e0b7bu, 0x8da24260u, 0xbddec8cdu, 0x3f868ccbu, 0x3f8b0480u},
|
||||
{0x8825ae56u, 0x26debfdbu, 0x86048719u, 0x82b33599u, 0xc0000000u, 0x3e908208u, 0xbf69176au, 0xff800000u},
|
||||
{0x8e752fdfu, 0x0fcf31cau, 0x537390e5u, 0xaead44b0u, 0x7fdcf073u, 0xbf831638u, 0xbf2766f5u, 0x3e800000u},
|
||||
{0xc28ee907u, 0xe4ddf9b9u, 0xe998d0eeu, 0x1038f0b5u, 0x3e95cc10u, 0x3d0183cbu, 0xbdfdbfc7u, 0xbd69e789u},
|
||||
{0xb2fff17bu, 0x85f1115bu, 0xe064a114u, 0xe040015cu, 0xbf2003efu, 0x3f9384efu, 0xbf7185f8u, 0xc0000000u}
|
||||
};
|
||||
|
||||
constexpr std::uint32_t Expected[32][31] = {
|
||||
{0x00000000u, 0x37800080u, 0x3f000080u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbc010204u, 0xbc010204u, 0x3f7f8100u, 0x35800000u, 0x00000000u, 0xbf004020u, 0x3d787c3eu, 0xbc804020u, 0x3f800000u, 0x00000000u, 0x3effff00u, 0x3f000180u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x000000ffu, 0x80000000u, 0x00000000u, 0x0040817fu, 0x80017fffu, 0x00004000u, 0x008000ffu},
|
||||
{0x00000000u, 0x3cf800f8u, 0x3ef800f8u, 0x00000000u, 0xba800100u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbe010204u, 0x3f800000u, 0x00000000u, 0x7f800000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x3effe100u, 0x00000000u, 0x3f7fe100u, 0x00000000u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x000000ffu, 0x00000000u, 0x00000000u, 0x0000817fu, 0x80017fffu, 0x00000000u, 0x000000ffu},
|
||||
{0x3b808081u, 0x00000000u, 0x37800080u, 0xbf800000u, 0x00000000u, 0xb8000100u, 0xbc010204u, 0xbc010204u, 0x3c010204u, 0x00000000u, 0x3f010181u, 0x00000000u, 0x38000000u, 0x3f00c060u, 0x3e406030u, 0x3d004020u, 0x37800080u, 0x3f800000u, 0x00000000u, 0x3f000080u, 0xbf800000u, 0x3f800000u, 0x00000000u, 0xbf800000u, 0x00000001u, 0x00010081u, 0x00000000u, 0x00000000u, 0x00400080u, 0x00000000u, 0x00000101u},
|
||||
{0x3f800000u, 0x3c7fc100u, 0x3e7fc100u, 0x3d7fe200u, 0x3d7fe200u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xbf010204u, 0x3e000080u, 0x3ffe0000u, 0x44000000u, 0x3f800000u, 0x00000000u, 0xbf800000u, 0x3f4000c0u, 0x00000000u, 0x3cffe100u, 0x3cffe100u, 0x00000000u, 0x3f800000u, 0x00000000u, 0x00000000u, 0x000000feu, 0x00007fffu, 0x00000000u, 0x00813f7fu, 0x3fff7f7fu, 0x7fff8001u, 0xff007ffeu},
|
||||
{0x3e60e0e1u, 0x3e9bc89cu, 0x3f72a7f3u, 0x3dd371a7u, 0x3e9a7935u, 0x3e9c3938u, 0x3de1c387u, 0x3e9d3a75u, 0x3e193265u, 0x3f4b972eu, 0x3ea5cca6u, 0x43c80000u, 0x41d20000u, 0x3de07038u, 0x3f56eb76u, 0x3f174ba6u, 0x3eca26cau, 0x3e1c389cu, 0x3e1a789au, 0x3d5370d3u, 0x421c0000u, 0x3f800000u, 0x00000000u, 0x41600000u, 0x00000000u, 0xffff0000u, 0x00000000u, 0x007fd481u, 0xd34d8001u, 0x00007fffu, 0x00ff0000u},
|
||||
{0x3f0f8f90u, 0x3db920b9u, 0x3e7670f6u, 0xbd827105u, 0x3e39c174u, 0x3f5d9dbbu, 0xbec99326u, 0x3f5dbb77u, 0x3e091224u, 0xbf67cf9fu, 0x3d8f108fu, 0x46480000u, 0x3f040000u, 0xbe627139u, 0x3e824121u, 0x3f0f47a4u, 0x3f0d118du, 0x3edd9cdeu, 0x3db9c0bau, 0x3f77d9f8u, 0x42dc0000u, 0x3f800000u, 0x00000000u, 0xc2480000u, 0x000000d9u, 0x373e0000u, 0x00000000u, 0x001b816cu, 0x80016cc6u, 0x80011b9fu, 0x003700d9u},
|
||||
{0x3f4ccccdu, 0x3f5190d2u, 0x3e6260e2u, 0xbea14143u, 0x3dbec17eu, 0xbf5185a3u, 0x3ef9f3e8u, 0xbf53a74fu, 0xbf800000u, 0xbf65cb97u, 0x3f79ebfau, 0x3b200000u, 0x3e340000u, 0xbf1a4d27u, 0xbf0542a1u, 0xbe446231u, 0x3f0e818fu, 0x3f173e97u, 0x3d3ec0bfu, 0x3f57b0d8u, 0xc2d20000u, 0x3f800000u, 0x00000000u, 0x42780000u, 0x00000000u, 0x281b0000u, 0x00000000u, 0x001400bbu, 0x0000b9fcu, 0x8001140eu, 0x00280000u},
|
||||
{0x3f45c5c6u, 0x3ee406e4u, 0x3ebeaabfu, 0x3ed749afu, 0x3e47898fu, 0xbd1ae136u, 0x3ea54a95u, 0xbd214285u, 0x3ee1c387u, 0xbf69d3a7u, 0x3e4060c0u, 0x3c060000u, 0x3d5c0000u, 0x3ec562b1u, 0xbf767b3eu, 0xbeff7fc0u, 0x3f0c388cu, 0x3f7b29fbu, 0x3dc788c8u, 0x3e5748d7u, 0xc0a00000u, 0x3f800000u, 0x00000000u, 0x42240000u, 0x0000002bu, 0xb8850000u, 0x00000000u, 0x005cff15u, 0xfecf156cu, 0x7fff5c42u, 0xffb8002bu},
|
||||
{0x3eeeeeefu, 0x3ee84ee8u, 0x3f4b5ccbu, 0x3ec6698du, 0x3eb81570u, 0x3f192932u, 0xbf59b367u, 0x3f193265u, 0xbdf1e3c8u, 0xbf1d3a75u, 0x3e7e60feu, 0x404e0000u, 0x3f1c0000u, 0xbf44e271u, 0xbf647239u, 0xbc607038u, 0x3f32f1b3u, 0x3e992899u, 0x3e3814b8u, 0x3e4668c6u, 0x42980000u, 0x3f800000u, 0x00000000u, 0xc2d80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00effe81u, 0xfe608001u, 0x8001eef9u, 0x00000000u},
|
||||
{0x3f54d4d5u, 0x3f7cfffdu, 0x3f41d3c2u, 0xbd7a41f5u, 0x3f2f215eu, 0x3f6aabd5u, 0x3f2b56adu, 0x3f6bd7afu, 0xbe418306u, 0x3e993265u, 0x3e28e8a9u, 0x41fe0000u, 0x3cfe0000u, 0x3ed46a35u, 0xbf21d0e8u, 0xbf2ed76cu, 0x3e1ba09cu, 0x3eeaaaebu, 0x3eaf20afu, 0x3f782ef8u, 0x42ea0000u, 0x3f800000u, 0x00000000u, 0x42aa0000u, 0x00000000u, 0xc06b0000u, 0x00000000u, 0x005f81e6u, 0x8001e5f1u, 0xcacb6035u, 0x00c00000u},
|
||||
{0x3e969697u, 0x3c46c0c7u, 0x3f147494u, 0x3e66e9ceu, 0xbecffda0u, 0xbe825905u, 0x3f55ab57u, 0xbe850a14u, 0xbeddbb77u, 0x3f69d3a7u, 0x3efe4cfeu, 0x3e360000u, 0x45000000u, 0x3e164b26u, 0x3f42e171u, 0xbce07038u, 0x3ee992eau, 0x3f5f6adfu, 0x3f4c01ccu, 0x3de6e8e7u, 0xc2040000u, 0x3f800000u, 0x00000000u, 0x42d40000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0081f700u, 0xf7610000u, 0x30f78001u, 0x62000000u},
|
||||
{0x3f25a5a6u, 0x3f037083u, 0x3ee42ae4u, 0x3f015f03u, 0x3f11b723u, 0xbe25714bu, 0x3f254a95u, 0xbe2952a5u, 0x3f69d3a7u, 0xbf13264du, 0x3f5269d2u, 0x3ee00000u, 0x3d600000u, 0x3f52e975u, 0xbf4b65b3u, 0x3f1349a5u, 0x3f3774b7u, 0x3f6b52ebu, 0x3e91b692u, 0x3e815e81u, 0xc1a80000u, 0x3f800000u, 0x00000000u, 0x42a40000u, 0x00000070u, 0x49d40000u, 0x00000000u, 0x00258138u, 0x80013820u, 0xa23924eau, 0x004a0070u},
|
||||
{0x3f66e6e7u, 0x3f2f83b0u, 0x3e7d8cfeu, 0xbf416183u, 0x3f4bb997u, 0x3f0f391eu, 0xbf499326u, 0x3f0f1e3cu, 0x3e2952a5u, 0x3f499326u, 0x3f3d05bdu, 0x47060000u, 0x40ea0000u, 0x3f7379bdu, 0x3f2c562bu, 0xbdc06030u, 0x3ec82ac8u, 0x3e8f388fu, 0x3ecbb8ccu, 0x3f1f509fu, 0x428e0000u, 0x3f800000u, 0x00000000u, 0xc2c80000u, 0x00000000u, 0xff2dffffu, 0x00000000u, 0x007f7fffu, 0x7ffffed6u, 0x79217f96u, 0xf1feff00u},
|
||||
{0x3e9a9a9bu, 0x3dda50dau, 0x3f7c89fdu, 0x3f5a81b5u, 0x3f37b16fu, 0xbdfdc1fcu, 0x3e912245u, 0xbe010204u, 0xbee9d3a7u, 0xbf254a95u, 0x3ed4a0d5u, 0x3e940000u, 0x3a460000u, 0xbeb359adu, 0x3ddc6e37u, 0xbf2dd6ebu, 0x3f2ec6afu, 0x3f7024f0u, 0x3eb7b0b8u, 0x3eda80dbu, 0xc1800000u, 0x3f800000u, 0x00000000u, 0x42100000u, 0x000000ffu, 0x0000ffffu, 0x00000000u, 0x00007f7fu, 0x7fff7fffu, 0x80010000u, 0x0000ffffu},
|
||||
{0x3f4fcfd0u, 0x3eb98abau, 0x3e868687u, 0xbf46898du, 0x3f105921u, 0x3f0eb31du, 0x3f3366ceu, 0x3f0f1e3cu, 0x3dc18306u, 0x3c010204u, 0x3e3ab8bbu, 0x418a0000u, 0x38d60000u, 0x3f67f3fau, 0xbe7e7f40u, 0xbf0b45a3u, 0x3b860086u, 0x3e8eb28fu, 0x3e905890u, 0x3f1cbc9du, 0x428e0000u, 0x3f800000u, 0x00000000u, 0x42b20000u, 0x000000ffu, 0x7eca2c28u, 0x00000000u, 0x003f167fu, 0x16147fffu, 0x27223f65u, 0x4e7e2cffu},
|
||||
{0x3ecacacbu, 0x3f1a5d9au, 0x3ee9cceau, 0xbed1c5a4u, 0xbe4dc99cu, 0xbf7999f3u, 0x3ed1a347u, 0xbf7bf7f0u, 0xbd810204u, 0xbe891224u, 0x3d5d20ddu, 0x3cba0000u, 0x41a60000u, 0xbf4de6f3u, 0x3e9e4f28u, 0x3edd6eb7u, 0x3f5ef8dfu, 0x3f033483u, 0x3f6647e6u, 0x3f4b8fccu, 0xc2fa0000u, 0x3f800000u, 0x00000000u, 0x42500000u, 0x000000f8u, 0x00000000u, 0x00000000u, 0x0081dc7cu, 0xdc2f7ca2u, 0x00008001u, 0x000000f8u},
|
||||
{0x3f25a5a6u, 0x3e85f286u, 0x3de120e1u, 0xbef9adf3u, 0x3f2e1b5cu, 0x3f06130cu, 0x3d912245u, 0x3f070e1cu, 0xbeddbb77u, 0xbf4f9f3eu, 0x3e2630a6u, 0x3df20000u, 0x41100000u, 0xbe365b2eu, 0xbed46a35u, 0xbf345a2du, 0x3f19c99au, 0x3e861286u, 0x3eae1aaeu, 0x3f4195c2u, 0x42860000u, 0x3f800000u, 0x00000000u, 0x41100000u, 0x00000000u, 0x4bb4185fu, 0x00000000u, 0x00260c00u, 0x0c300000u, 0x800125dau, 0x004b1800u},
|
||||
{0x3f1d9d9eu, 0x3f2a87abu, 0x3f2268a2u, 0xbd922124u, 0x3f012702u, 0xbf6509cau, 0x3f79f3e8u, 0xbf67cf9fu, 0xbf4f9f3eu, 0xbdc18306u, 0x3e181c98u, 0x3d0e0000u, 0x422a0000u, 0xbe466332u, 0x3f6cf67bu, 0xbf506834u, 0x3f7499f5u, 0x3f0d7c8du, 0x3e812681u, 0x3f76def7u, 0xc2e60000u, 0x3f800000u, 0x00000000u, 0x42f80000u, 0x00000000u, 0xffff5555u, 0x00000000u, 0x007f2ad3u, 0x2aaad2ccu, 0xc1637fffu, 0x00ff5500u},
|
||||
{0x3eaaaaabu, 0x3f7b7efbu, 0x3eaf6cafu, 0xbf2f375eu, 0xbf04f30au, 0x3e5cd1bau, 0xbf4d9b36u, 0x3e59b367u, 0xbee5cb97u, 0x3f070e1cu, 0x3f3feac0u, 0x3efc0000u, 0x45be0000u, 0x3f2ad56bu, 0xbf2c562bu, 0xbb804020u, 0x3e878e88u, 0x3ddcd0ddu, 0x3f3d87beu, 0x3f2865a8u, 0x41d80000u, 0x3f800000u, 0x00000000u, 0xc2cc0000u, 0x0000007fu, 0x00000000u, 0x00000000u, 0x00d29f3fu, 0x9de63feau, 0x7fffd1feu, 0xff00007fu},
|
||||
{0x3cc0c0c1u, 0x3d6ab0ebu, 0x3f7a7ffau, 0x3eda89b5u, 0xbf36d56eu, 0xbe85b90bu, 0xbf5dbb77u, 0xbe891224u, 0x3c810204u, 0xbe8d1a34u, 0x3e989e99u, 0x45560000u, 0x7fc20000u, 0xbefa7d3fu, 0xbd506834u, 0x3ec46231u, 0x3f5d02ddu, 0x3f5e92dfu, 0x3f2496a5u, 0x3e5a88dbu, 0xc2080000u, 0x3f800000u, 0x00000000u, 0xc2dc0000u, 0x00000055u, 0x00000000u, 0x00000000u, 0x00d0c02au, 0xc0002aaau, 0x7fffcffau, 0xff000055u},
|
||||
{0x3f42c2c3u, 0x3f5bbddcu, 0x3f22eda3u, 0x3f048509u, 0x3ee465c9u, 0xbf735de7u, 0x3f254a95u, 0xbf75ebd8u, 0xbd010204u, 0xbf499326u, 0x3ea8c8a9u, 0x3f7a0000u, 0x43760000u, 0x3ec26131u, 0x3f1dcee7u, 0x3f2351a9u, 0x3f1cfc9du, 0x3f065286u, 0x3e6464e4u, 0x3e848485u, 0xc2f40000u, 0x3f800000u, 0x00000000u, 0x42a40000u, 0x000000ffu, 0x9fa8cbb9u, 0x00000000u, 0x004f657fu, 0x65dc7fffu, 0x7fff4fd4u, 0xff9fcbffu},
|
||||
{0x3f75f5f6u, 0x3e78d8f9u, 0x3ced60edu, 0xbf2db55bu, 0xbcaa8155u, 0xbe6d21dau, 0x3f3972e6u, 0xbe71e3c8u, 0x3e993265u, 0x3d61c387u, 0x3f3b23bbu, 0x446c0000u, 0x42ce0000u, 0xbcb0582cu, 0x3ec46231u, 0xbe1c4e27u, 0x3ce4c0e5u, 0x3f625ce2u, 0x3f7d56fdu, 0x3f2926a9u, 0xc1f00000u, 0x3f800000u, 0x00000000u, 0x42b80000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00ef81f7u, 0x8001f6c3u, 0x7fffee70u, 0xff000000u},
|
||||
{0x3ca0a0a1u, 0x3e166c96u, 0x3da4f0a5u, 0x3ee001c0u, 0x3ee1c1c4u, 0xbd6ee1deu, 0xbf6fdfbfu, 0xbd810204u, 0x3e993265u, 0x3e51a347u, 0x3ebab0bbu, 0x43360000u, 0x3f880000u, 0xbefb7dbfu, 0xbf67f3fau, 0x3f6af57bu, 0x3dd130d1u, 0x3f7889f9u, 0x3e61c0e2u, 0x3e6000e0u, 0xc1000000u, 0x3f800000u, 0x00000000u, 0xc2ee0000u, 0x00000000u, 0x49640000u, 0x00000000u, 0x0024fa81u, 0xfa3a8001u, 0x000024b2u, 0x00490000u},
|
||||
{0x3f800000u, 0x3d2eb0afu, 0x3dada0aeu, 0xbf6967d3u, 0xbea95553u, 0x3e90e522u, 0x3ee5cb97u, 0x3e912245u, 0x3e61c387u, 0xbf2f5ebdu, 0x3f24a4a5u, 0x3dd60000u, 0x45020000u, 0xbf00c060u, 0x3f0bc5e3u, 0xbf5b6db7u, 0x3f291ca9u, 0x3e10e491u, 0x3f55abd6u, 0x3f0b4d8bu, 0x42100000u, 0x3f800000u, 0x00000000u, 0x42640000u, 0x000000ffu, 0x0000a67eu, 0x00000000u, 0x0096537fu, 0x533f7fffu, 0xc8439504u, 0x0000a6ffu},
|
||||
{0x3e44c4c5u, 0x3f017a81u, 0x3f38c9b9u, 0xbf280550u, 0x3f4aab95u, 0xbf1f233eu, 0x3f5fbf7fu, 0xbf214285u, 0xbf5fbf7fu, 0x3f6ddbb7u, 0x3f78bef9u, 0x3af40000u, 0x3a600000u, 0x3dc46231u, 0xbe3c5e2fu, 0xbe6a753bu, 0x3eed22edu, 0x3f306fb0u, 0x3ecaaacbu, 0x3f2bfeacu, 0xc2a00000u, 0x3f800000u, 0x00000000u, 0x42de0000u, 0x000000ffu, 0xffff0000u, 0x00000000u, 0x007f937fu, 0x922e7fffu, 0xf4787fffu, 0x00ff00ffu},
|
||||
{0x3f6feff0u, 0x3e4358c3u, 0x3f5396d4u, 0xbdc05181u, 0xbf4eed9eu, 0x3f292d52u, 0xbf55ab57u, 0x3f2952a5u, 0xbdb162c6u, 0xbd418306u, 0x3f1c909du, 0x39ac0000u, 0x3d8c0000u, 0x3f77fbfeu, 0x3c004020u, 0x3f64f279u, 0x3f7af5fbu, 0x3ea92ca9u, 0x3f188a99u, 0x3f73fbf4u, 0x42a80000u, 0x3f800000u, 0x00000000u, 0xc2d40000u, 0x000000ffu, 0x0000ffffu, 0x00000000u, 0x00007f7fu, 0x7fff7fffu, 0x4b550000u, 0x9600ffffu},
|
||||
{0x3da8a8a9u, 0x3d10b091u, 0x3f6f02efu, 0xbe03c108u, 0x3e8e951du, 0x3e8a2514u, 0xbf6fdfbfu, 0x3e891224u, 0x3de1c387u, 0x3f5fbf7fu, 0x3f3fdec0u, 0x3a160000u, 0x38820000u, 0x3d28542au, 0xbe024121u, 0xbbc06030u, 0x3ede1cdeu, 0x3e0a248au, 0x3e0e948fu, 0x3f6f88f0u, 0x42080000u, 0x3f800000u, 0x00000000u, 0xc2ee0000u, 0x00000000u, 0xffff0000u, 0x00000000u, 0x007f819bu, 0x80019a6fu, 0x80017fffu, 0x00ff0000u},
|
||||
{0x3f6eeeefu, 0x3ed00cd0u, 0x3ed688d7u, 0xbec93192u, 0xbea9cd54u, 0x3db7b16fu, 0x3f77efe0u, 0x3db162c6u, 0xbf65cb97u, 0x3e850a14u, 0x3f0b5a8bu, 0x36c00000u, 0x391a0000u, 0xbd104824u, 0xbf2a552bu, 0x3eb55aadu, 0x3e063886u, 0x3d37b0b8u, 0x3f558dd6u, 0x3f4db4ceu, 0x41300000u, 0x3f800000u, 0x00000000u, 0x42f60000u, 0x00000000u, 0xffff0000u, 0x00000000u, 0x007ff200u, 0xf2140000u, 0x7fff7fffu, 0xffff0000u},
|
||||
{0x3eacacadu, 0x3f3fdbc0u, 0x3e1b789bu, 0xbf71cfe4u, 0xbf73f9e8u, 0x3ed665adu, 0xbf4f9f3eu, 0x3ed5ab57u, 0xbf1b366du, 0xbf7dfbf8u, 0x3f082588u, 0x7fb60000u, 0x3fae0000u, 0xbf556ab5u, 0x3f35daedu, 0x3e824121u, 0x3f02b383u, 0x3e5664d6u, 0x3f060486u, 0x3f071987u, 0x42540000u, 0x3f800000u, 0x00000000u, 0xc2ce0000u, 0x00000000u, 0x00004841u, 0x00000000u, 0x008c2481u, 0x24208001u, 0x80018b75u, 0x00004800u},
|
||||
{0x3f5fdfe0u, 0x3e4728c7u, 0x3d7cf0fdu, 0xbf5e37bcu, 0x3f26e74eu, 0x3f096113u, 0xbf214285u, 0x3f091224u, 0xbf274e9du, 0xbf254a95u, 0x3f0e758eu, 0x3b940000u, 0x3bcc0000u, 0xbd844221u, 0x3f25d2e9u, 0x3ee773bau, 0x3f2eadafu, 0x3e896089u, 0x3ea6e6a7u, 0x3f10e591u, 0x42880000u, 0x3f800000u, 0x00000000u, 0xc2a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00ad8100u, 0x80010000u, 0x2000ac4du, 0x40000000u},
|
||||
{0x3ce0e0e1u, 0x3f79b9fau, 0x3f64dde5u, 0xbebc4979u, 0xbe334167u, 0xbdf4b1e9u, 0xbf172e5du, 0xbe010204u, 0x3ee1c387u, 0x3e010204u, 0x3f428ec3u, 0x3b720000u, 0x3f7e0000u, 0x3f03c1e1u, 0xbe0c4623u, 0x3da05028u, 0x3d81c082u, 0x3f70b5f1u, 0x3f6998eau, 0x3f50eed1u, 0xc1800000u, 0x3f800000u, 0x00000000u, 0xc2960000u, 0x0000004bu, 0x00000818u, 0x00000000u, 0x00f00425u, 0x040c2573u, 0xf8b1f024u, 0x0000084bu},
|
||||
{0x3ef6f6f7u, 0x3d8ad88bu, 0x3f05f186u, 0xbf3dd97cu, 0xbe7ce1fau, 0x3c2e015cu, 0x3f3972e6u, 0x3c010204u, 0x3f010204u, 0xbe810204u, 0x3f32ffb3u, 0x3ab60000u, 0x44440000u, 0x3f3ddeefu, 0xbc004020u, 0xbed168b4u, 0x3f6040e0u, 0x3bae00aeu, 0x3f6064e0u, 0x3f2114a1u, 0x3f800000u, 0x3f800000u, 0x00000000u, 0x42b80000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00887fb1u, 0x7fffafffu, 0x8001873eu, 0x0000ff00u}
|
||||
};
|
||||
|
||||
constexpr const char* Names[Loads + Stored] = {
|
||||
"buffer_load_format_x 8_8_8_8_UNORM x", "buffer_load_format_xy 16_16_16_16_UNORM x", "buffer_load_format_xy 16_16_16_16_UNORM y",
|
||||
"buffer_load_format_xyz 16_16_16_16_SNORM x", "buffer_load_format_xyz 16_16_16_16_SNORM y", "buffer_load_format_xyz 16_16_16_16_SNORM z",
|
||||
"buffer_load_format_xyzw 8_8_8_8_SNORM x", "buffer_load_format_xyzw 8_8_8_8_SNORM y", "buffer_load_format_xyzw 8_8_8_8_SNORM z",
|
||||
"buffer_load_format_xyzw 8_8_8_8_SNORM w", "tbuffer_load_format_x 16_UNORM x", "tbuffer_load_format_xy 10_11_11_FLOAT x",
|
||||
"tbuffer_load_format_xy 10_11_11_FLOAT y", "tbuffer_load_format_xyz 2_10_10_10_SNORM x", "tbuffer_load_format_xyz 2_10_10_10_SNORM y",
|
||||
"tbuffer_load_format_xyz 2_10_10_10_SNORM z", "buffer_load_format_xyzw 16_16_16_16_UNORM x", "buffer_load_format_xyzw 16_16_16_16_UNORM y",
|
||||
"buffer_load_format_xyzw 16_16_16_16_UNORM z", "buffer_load_format_xyzw 16_16_16_16_UNORM w", "buffer_load_format_xyzw 8_8_8_8_SSCALED x",
|
||||
"buffer_load_format_xyzw 8_8_8_8_SSCALED y", "buffer_load_format_xyzw 8_8_8_8_SSCALED z", "buffer_load_format_xyzw 8_8_8_8_SSCALED w",
|
||||
"scratch dword 0 (store_format_x 8_8_8_8_UNORM)", "scratch dword 1 (store_format_xy 16_16_16_16_UNORM low)", "scratch dword 2 (store_format_xy 16_16_16_16_UNORM high)",
|
||||
"scratch dword 3 (store_format_xyz 8_8_8_8_SNORM)", "scratch dword 4 (store_format_xyzw 16_16_16_16_SNORM low)", "scratch dword 5 (store_format_xyzw 16_16_16_16_SNORM high)",
|
||||
"scratch dword 6 (store_format_xyzw 8_8_8_8_UNORM)",
|
||||
};
|
||||
|
||||
constexpr std::array<std::uint32_t, 10> FormattedWord3{0x31038facu, 0x31041facu, 0x31042facu, 0x31039facu, 0x31041977u, 0x3103b80du, 0x31038facu, 0x31041facu, 0x31039facu, 0x31042facu};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes, std::uint32_t word3) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, word3};
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) {
|
||||
Require(actual == expected, std::string("buffer format: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected));
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), &Input[tid * Inputs]);
|
||||
Output.fill(0xdeadbeefu);
|
||||
Scratch.fill(0xaaaaaaaau);
|
||||
std::vector<std::uint32_t> userData(8u + 4u * FormattedWord3.size(), 0u);
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u), 0x31016facu);
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
for (std::size_t index = 0; index < FormattedWord3.size(); ++index) {
|
||||
const bool store = index >= 6u;
|
||||
const auto descriptor = store ? BufferDescriptor(Scratch.data(), static_cast<std::uint32_t>(Scratch.size() * 4u), FormattedWord3[index])
|
||||
: BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u), FormattedWord3[index]);
|
||||
std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 8 + 4 * index);
|
||||
}
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Check() {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
for (std::uint32_t index = 0; index < Loads; ++index) Expect(tid, Output[tid * Results + index], Expected[tid][index], Names[index]);
|
||||
for (std::uint32_t index = 0; index < Stored; ++index) Expect(tid, Scratch[tid * ScratchWords + index], Expected[tid][Loads + index], Names[Loads + index]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
Run(*device);
|
||||
Check();
|
||||
std::puts("buffer format tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <cctype>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
void SetRequest(const std::string& value) {
|
||||
#ifdef _WIN32
|
||||
if (_putenv_s("ANYPS5_GPU", value.c_str()) != 0) throw std::runtime_error("cannot set ANYPS5_GPU");
|
||||
#else
|
||||
if (setenv("ANYPS5_GPU", value.c_str(), 1) != 0) throw std::runtime_error("cannot set ANYPS5_GPU");
|
||||
#endif
|
||||
}
|
||||
|
||||
void Require(const bool condition, const std::string& message) {
|
||||
if (!condition) throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
SetRequest("");
|
||||
const auto defaultDevice = OpenVulkanTestDevice();
|
||||
if (!defaultDevice) return VulkanTestSkipped;
|
||||
const std::string name = defaultDevice->DeviceName();
|
||||
Require(!name.empty(), "the default device has no name");
|
||||
|
||||
std::string request = name;
|
||||
for (auto& character : request) character = static_cast<char>(std::toupper(static_cast<unsigned char>(character)));
|
||||
SetRequest(request);
|
||||
const AgcDriver::VulkanDevice requested;
|
||||
Require(requested.DeviceName() == name, "ANYPS5_GPU=" + request + " selected " + requested.DeviceName() + " instead of " + name);
|
||||
|
||||
SetRequest("no such device 1d3d154f");
|
||||
try {
|
||||
const AgcDriver::VulkanDevice missing;
|
||||
throw std::runtime_error("ANYPS5_GPU naming no device selected " + missing.DeviceName());
|
||||
} catch (const std::runtime_error& error) {
|
||||
const std::string message = error.what();
|
||||
Require(message.find("ANYPS5_GPU=\"no such device 1d3d154f\"") != std::string::npos && message.find(name) != std::string::npos, "unexpected error: " + message);
|
||||
}
|
||||
std::puts("device selection tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Results = 16;
|
||||
constexpr std::uint32_t ActiveLanes = 16;
|
||||
constexpr std::uint32_t LdsDwords = 256;
|
||||
constexpr std::uint32_t AppendStart = 1000;
|
||||
constexpr std::uint32_t ConsumeStart = 2000;
|
||||
constexpr std::uint32_t MaskedAppendStart = 3000;
|
||||
constexpr std::uint32_t MaskedConsumeStart = 4000;
|
||||
constexpr std::uint32_t BaseShiftedStart = 5000;
|
||||
constexpr std::uint32_t UntouchedAppend = 0x5eed0001u;
|
||||
constexpr std::uint32_t UntouchedConsume = 0x5eed0002u;
|
||||
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
|
||||
|
||||
alignas(256) constexpr auto Code = std::to_array<std::uint32_t>({
|
||||
0x34060084, 0x7e100280,
|
||||
0x7e0802ff, AppendStart, 0x7e0a02ff, ConsumeStart, 0x7e0c02ff, MaskedAppendStart, 0x7e0e02ff, MaskedConsumeStart,
|
||||
0x7e1c02ff, BaseShiftedStart, 0x7e1e02ff, BaseShiftedStart + 1u, 0x7e2002ff, BaseShiftedStart + 2u, 0x7e2202ff, BaseShiftedStart + 3u,
|
||||
0x7e1802ff, UntouchedAppend, 0x7e1a02ff, UntouchedConsume,
|
||||
0x7da40080,
|
||||
0xd8340010, 0x00000408, 0xd8340014, 0x00000508, 0xd8340018, 0x00000608, 0xd834001c, 0x00000708,
|
||||
0xd8340050, 0x00000e08, 0xd8340054, 0x00000f08, 0xd8340058, 0x00001008, 0xd834005c, 0x00001108,
|
||||
0xbefe03c1,
|
||||
0xbf8cc07f,
|
||||
0xbefc03ff, 0x0040ffff,
|
||||
0xd8f80010, 0x0a000000,
|
||||
0xd8f40014, 0x0b000000,
|
||||
0xbefc03ff, 0x00400000,
|
||||
0x7da80090,
|
||||
0xd8f80018, 0x0c000000,
|
||||
0xd8f4001c, 0x0d000000,
|
||||
0xbefe03c1,
|
||||
0xbf8cc07f,
|
||||
0xd8d80010, 0x14000008, 0xd8d80014, 0x15000008, 0xd8d80018, 0x16000008, 0xd8d8001c, 0x17000008,
|
||||
0xd8d80050, 0x18000008, 0xd8d80054, 0x19000008, 0xd8d80058, 0x1a000008, 0xd8d8005c, 0x1b000008,
|
||||
0xbf8cc07f,
|
||||
0xe0702000, 0x80000a03, 0xe0702004, 0x80000b03, 0xe0702008, 0x80000c03, 0xe070200c, 0x80000d03,
|
||||
0xe0702010, 0x80001403, 0xe0702014, 0x80001503, 0xe0702018, 0x80001603, 0xe070201c, 0x80001703,
|
||||
0xe0702020, 0x80001803, 0xe0702024, 0x80001903, 0xe0702028, 0x80001a03, 0xe070202c, 0x80001b03,
|
||||
0xbf810000,
|
||||
});
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(4, 0u);
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size()));
|
||||
std::copy(output.begin(), output.end(), userData.begin());
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, LdsDwords, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Check() {
|
||||
constexpr std::array<const char*, 12> names{
|
||||
"ds_append result", "ds_consume result", "masked ds_append result", "masked ds_consume result",
|
||||
"ds_append counter", "ds_consume counter", "masked ds_append counter", "masked ds_consume counter",
|
||||
"dword at M0.base + ds_append offset", "dword at M0.base + ds_consume offset",
|
||||
"dword at M0.base + masked ds_append offset", "dword at M0.base + masked ds_consume offset",
|
||||
};
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const bool active = tid < ActiveLanes;
|
||||
const std::array<std::uint32_t, 12> expected{
|
||||
AppendStart, ConsumeStart, active ? MaskedAppendStart : UntouchedAppend, active ? MaskedConsumeStart : UntouchedConsume,
|
||||
AppendStart + Threads, ConsumeStart - Threads, MaskedAppendStart + ActiveLanes, MaskedConsumeStart - ActiveLanes,
|
||||
BaseShiftedStart, BaseShiftedStart + 1u, BaseShiftedStart + 2u, BaseShiftedStart + 3u,
|
||||
};
|
||||
for (std::uint32_t j = 0; j < expected.size(); ++j) {
|
||||
const auto actual = Output[tid * Results + j];
|
||||
Require(actual == expected[j], std::string("ds append consume: lane ") + std::to_string(tid) + " " + names[j] + " is " + std::to_string(actual) + ", expected " + std::to_string(expected[j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (device->Target().subgroupSize < Threads) {
|
||||
std::printf("skipped, the device's subgroups are narrower than a wave (%u lanes)\n", device->Target().subgroupSize);
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
Run(*device);
|
||||
Check();
|
||||
std::puts("ds append consume tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Inputs = 4;
|
||||
constexpr std::uint32_t Results = 16;
|
||||
constexpr std::uint32_t ActiveLanes = 16;
|
||||
constexpr std::uint32_t LdsDwords = 256;
|
||||
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
|
||||
|
||||
alignas(256) constexpr auto Code = std::to_array<std::uint32_t>({
|
||||
0x34020082, 0x34060084, 0x343c0082,
|
||||
0xe0302000, 0x80000401, 0xe0302004, 0x80000501, 0xe0302008, 0x80000601, 0xe030200c, 0x80000701,
|
||||
0xbf8c3f70,
|
||||
0xd8340000, 0x0000041e, 0xd8340080, 0x0000051e, 0xd8340100, 0x0000051e, 0xd8340300, 0x0000041e,
|
||||
0xd8340180, 0x0000051e, 0xd8340280, 0x0000041e, 0xd8340380, 0x0000041e, 0xd8340200, 0x0000051e,
|
||||
0xbf8cc07f,
|
||||
0x7e1c0306, 0x7e1e0307, 0x7e200306, 0x7e220307,
|
||||
0xd8b82000, 0x0a07061e,
|
||||
0xd8bc0103, 0x0c06071e,
|
||||
0xd8b8a060, 0x0e0f0e1e,
|
||||
0x7da80090,
|
||||
0xd8b880e0, 0x1007061e,
|
||||
0xbefe03c1,
|
||||
0xbf8cc07f,
|
||||
0xd8d80000, 0x1400001e, 0xd8d80080, 0x1500001e, 0xd8d80300, 0x1600001e, 0xd8d80100, 0x1700001e,
|
||||
0xd8d80180, 0x1800001e, 0xd8d80280, 0x1900001e, 0xd8d80380, 0x1a00001e, 0xd8d80200, 0x1b00001e,
|
||||
0xbf8cc07f,
|
||||
0xe0702000, 0x80010a03, 0xe0702004, 0x80010b03, 0xe0702008, 0x80010c03, 0xe070200c, 0x80010d03,
|
||||
0xe0702010, 0x80010e03, 0xe0702014, 0x80010f03, 0xe0702018, 0x80011003, 0xe070201c, 0x80011103,
|
||||
0xe0702020, 0x80011403, 0xe0702024, 0x80011503, 0xe0702028, 0x80011603, 0xe070202c, 0x80011703,
|
||||
0xe0702030, 0x80011803, 0xe0702034, 0x80011903, 0xe0702038, 0x80011a03, 0xe070203c, 0x80011b03,
|
||||
0xbf810000,
|
||||
});
|
||||
|
||||
alignas(256) constexpr auto CoincidingOffsets = std::to_array<std::uint32_t>({0xd8b80505, 0x0a07061e, 0xbf810000});
|
||||
alignas(256) constexpr auto CoincidingOffsetsSt64 = std::to_array<std::uint32_t>({0xd8bc0000, 0x0a07061e, 0xbf810000});
|
||||
alignas(256) constexpr auto DestinationOverflow = std::to_array<std::uint32_t>({0xd8b82000, 0xff07061e, 0xbf810000});
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t count) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x01016facu};
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
template<typename TUse>
|
||||
void Translate(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, TUse&& use) {
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size()));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size()));
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, LdsDwords, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
use(ShaderRecompiler::Recompile(request));
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
for (std::uint32_t word = 0; word < Inputs; ++word) {
|
||||
Input[tid * Inputs + word] = ((word + 1u) << 28u) | (tid * 0x00010203u + word);
|
||||
}
|
||||
}
|
||||
Output.fill(0xdeadbeefu);
|
||||
Translate(device, Code, [&](const auto& result) {
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
|
||||
device.WaitIdle();
|
||||
});
|
||||
}
|
||||
|
||||
void Reject(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::string& reason) {
|
||||
std::string error;
|
||||
try {
|
||||
Translate(device, code, [](const auto&) {});
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
}
|
||||
Require(error.find(reason) != std::string::npos, "ds wrxchg2: expected a rejection for \"" + reason + "\", got \"" + error + "\"");
|
||||
}
|
||||
|
||||
void CheckRejections(AgcDriver::VulkanDevice& device) {
|
||||
Reject(device, CoincidingOffsets, "DS write exchange of one location through both offsets is not supported");
|
||||
Reject(device, CoincidingOffsetsSt64, "DS write exchange of one location through both offsets is not supported");
|
||||
Reject(device, DestinationOverflow, "DS write exchange destination register range overflow");
|
||||
}
|
||||
|
||||
void Check() {
|
||||
constexpr std::array<const char*, Results> names{
|
||||
"ds_wrxchg2_rtn_b32 offset0 result", "ds_wrxchg2_rtn_b32 offset1 result",
|
||||
"ds_wrxchg2st64_rtn_b32 offset0 result", "ds_wrxchg2st64_rtn_b32 offset1 result",
|
||||
"aliased ds_wrxchg2_rtn_b32 offset0 result", "aliased ds_wrxchg2_rtn_b32 offset1 result",
|
||||
"masked ds_wrxchg2_rtn_b32 offset0 result", "masked ds_wrxchg2_rtn_b32 offset1 result",
|
||||
"ds_wrxchg2_rtn_b32 offset0 memory", "ds_wrxchg2_rtn_b32 offset1 memory",
|
||||
"ds_wrxchg2st64_rtn_b32 offset0 memory", "ds_wrxchg2st64_rtn_b32 offset1 memory",
|
||||
"aliased ds_wrxchg2_rtn_b32 offset0 memory", "aliased ds_wrxchg2_rtn_b32 offset1 memory",
|
||||
"masked ds_wrxchg2_rtn_b32 offset0 memory", "masked ds_wrxchg2_rtn_b32 offset1 memory",
|
||||
};
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const std::uint32_t a = Input[tid * Inputs];
|
||||
const std::uint32_t b = Input[tid * Inputs + 1];
|
||||
const std::uint32_t c = Input[tid * Inputs + 2];
|
||||
const std::uint32_t d = Input[tid * Inputs + 3];
|
||||
const bool active = tid < ActiveLanes;
|
||||
const std::array<std::uint32_t, Results> expected{
|
||||
a, b, a, b, b, a, active ? a : c, active ? b : d,
|
||||
c, d, d, c, c, d, active ? c : a, active ? d : b,
|
||||
};
|
||||
for (std::uint32_t j = 0; j < Results; ++j) {
|
||||
const auto actual = Output[tid * Results + j];
|
||||
Require(actual == expected[j], std::string("ds wrxchg2: lane ") + std::to_string(tid) + " " + names[j] + " is " + Hex(actual) + ", expected " + Hex(expected[j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
Run(*device);
|
||||
Check();
|
||||
CheckRejections(*device);
|
||||
std::puts("ds wrxchg2 tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Inputs = 8;
|
||||
constexpr std::uint32_t Results = 16;
|
||||
constexpr std::uint32_t LdsDwords = 3072;
|
||||
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
|
||||
|
||||
alignas(256) constexpr auto Code = std::to_array<std::uint32_t>({
|
||||
0x34020085, 0xe0381000, 0x80000201, 0xe0381010, 0x80000601,
|
||||
0x345c0085, 0x4a5c5cff, 0x00000480, 0x345e0083, 0x4a5e5eff, 0x00000880,
|
||||
0xbf8c3f70,
|
||||
0xd9380301, 0x0004022e, 0xd9b80301, 0x1208062e, 0xd9dc0301, 0x1600002e,
|
||||
0xd93c0100, 0x0004022f, 0xd9bc0100, 0x1a08062f, 0xd9e00100, 0x1e00002f,
|
||||
0x34020086,
|
||||
0xbf8cc07f,
|
||||
0xe0781000, 0x80011201, 0xe0781010, 0x80011601, 0xe0781020, 0x80011a01, 0xe0781030, 0x80011e01,
|
||||
0xbf810000,
|
||||
});
|
||||
|
||||
alignas(256) constexpr auto CoincidingOffsets = std::to_array<std::uint32_t>({0xd9b80101, 0x1208062e, 0xbf810000});
|
||||
alignas(256) constexpr auto CoincidingOffsetsSt64 = std::to_array<std::uint32_t>({0xd9bc0000, 0x1a08062f, 0xbf810000});
|
||||
alignas(256) constexpr auto DestinationOverflow = std::to_array<std::uint32_t>({0xd9b80301, 0xfd08062e, 0xbf810000});
|
||||
|
||||
void Fill(std::uint32_t tid, std::uint32_t* words) {
|
||||
std::uint64_t seed = (tid + 1u) * 0x9e3779b97f4a7c15ull;
|
||||
for (std::uint32_t i = 0; i < Inputs; ++i) {
|
||||
seed = seed * 0xbf58476d1ce4e5b9ull + 0x94d049bb133111ebull;
|
||||
words[i] = static_cast<std::uint32_t>(seed >> 32u);
|
||||
}
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x01016facu};
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
template<typename TUse>
|
||||
void Translate(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, TUse&& use) {
|
||||
std::vector<std::uint32_t> userData(8, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, LdsDwords, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
use(ShaderRecompiler::Recompile(request));
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]);
|
||||
Output.fill(0xdeadbeefu);
|
||||
Translate(device, Code, [&](const auto& result) {
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
|
||||
device.WaitIdle();
|
||||
});
|
||||
}
|
||||
|
||||
void Reject(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::string& reason) {
|
||||
std::string error;
|
||||
try {
|
||||
Translate(device, code, [](const auto&) {});
|
||||
} catch (const std::exception& exception) {
|
||||
error = exception.what();
|
||||
}
|
||||
Require(error.find(reason) != std::string::npos, "ds wrxchg2 b64: expected a rejection for \"" + reason + "\", got \"" + error + "\"");
|
||||
}
|
||||
|
||||
void CheckRejections(AgcDriver::VulkanDevice& device) {
|
||||
Reject(device, CoincidingOffsets, "DS write exchange of one location through both offsets is not supported");
|
||||
Reject(device, CoincidingOffsetsSt64, "DS write exchange of one location through both offsets is not supported");
|
||||
Reject(device, DestinationOverflow, "DS write exchange destination register range overflow");
|
||||
}
|
||||
|
||||
void Check() {
|
||||
constexpr std::array<const char*, 2> names{"ds_wrxchg2_rtn_b64", "ds_wrxchg2st64_rtn_b64"};
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const std::uint32_t* in = &Input[tid * Inputs];
|
||||
const std::uint32_t* out = &Output[tid * Results];
|
||||
for (std::uint32_t op = 0; op < 2u; ++op) {
|
||||
for (std::uint32_t i = 0; i < 4u; ++i) {
|
||||
const auto returned = out[op * 8u + i];
|
||||
const auto stored = out[op * 8u + 4u + i];
|
||||
Require(returned == in[i], std::string("ds wrxchg2 b64: lane ") + std::to_string(tid) + " " + names[op] + " returned dword " + std::to_string(i) + " is " + Hex(returned) + ", expected " + Hex(in[i]));
|
||||
Require(stored == in[4u + i], std::string("ds wrxchg2 b64: lane ") + std::to_string(tid) + " " + names[op] + " stored dword " + std::to_string(i) + " is " + Hex(stored) + ", expected " + Hex(in[4u + i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
Run(*device);
|
||||
Check();
|
||||
CheckRejections(*device);
|
||||
std::puts("ds wrxchg2 b64 tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
|
@ -38,10 +39,12 @@ constexpr std::uint32_t BorderTable = 3;
|
|||
constexpr std::uint32_t FilterPoint = 0;
|
||||
constexpr std::uint32_t FilterBilinear = 1;
|
||||
constexpr std::uint32_t FilterAnisoBilinear = 3;
|
||||
constexpr std::uint32_t ReductionMin = 1;
|
||||
constexpr std::array<std::uint8_t, 4> Red{0, 64, 128, 255};
|
||||
|
||||
alignas(256) std::array<float, Threads * 3> Input{};
|
||||
alignas(256) std::array<float, Threads> Output{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * 3> Extra{};
|
||||
alignas(4096) std::array<std::uint8_t, 4096> Texels{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> Code{
|
||||
|
|
@ -49,10 +52,17 @@ alignas(256) constexpr std::array<std::uint32_t, 13> Code{
|
|||
0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 16> OffsetCode{
|
||||
0x1614008c, 0xe03c1000, 0x8000030a, 0xe03c1000, 0x8005060a, 0xbf8c3f70, 0x7e020306, 0x7e040307,
|
||||
0x7e0c0308, 0xf0f80108, 0x00820901, 0xbf8c3f70, 0x34160082, 0xe0701000, 0x8001090b, 0xbf810000,
|
||||
};
|
||||
|
||||
struct Sampler {
|
||||
std::uint32_t clamp;
|
||||
std::uint32_t filter;
|
||||
std::uint32_t border = BorderBlack;
|
||||
std::uint32_t reduction = 0;
|
||||
std::uint32_t lodBias = 0;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
|
|
@ -73,9 +83,9 @@ std::array<std::uint32_t, 8> TextureDescriptor(std::uint32_t format) {
|
|||
|
||||
std::array<std::uint32_t, 4> SamplerDescriptor(const Sampler& sampler) {
|
||||
return {
|
||||
sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u),
|
||||
sampler.clamp | (sampler.clamp << 3u) | (ClampEdge << 6u) | (LessEqual << 12u) | (sampler.reduction << 29u),
|
||||
0u,
|
||||
(sampler.filter << 20u) | (sampler.filter << 22u),
|
||||
sampler.lodBias | (sampler.filter << 20u) | (sampler.filter << 22u),
|
||||
sampler.border << 30u,
|
||||
};
|
||||
}
|
||||
|
|
@ -107,6 +117,19 @@ void FillInput() {
|
|||
Input[6] = 0.6f; Input[7] = -0.1f; Input[8] = 0.5f;
|
||||
Input[9] = 0.3f; Input[10] = 0.5f; Input[11] = 1.1f;
|
||||
Input[12] = 0.9f; Input[13] = 1.05f; Input[14] = -0.05f;
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto x = static_cast<std::int32_t>(tid % 7u) - 3;
|
||||
const auto y = static_cast<std::int32_t>((tid / 7u) % 5u) - 2;
|
||||
Extra[tid * 3u + 0u] = (static_cast<std::uint32_t>(x) & 0x3fu) | ((static_cast<std::uint32_t>(y) & 0x3fu) << 8u) | (tid % 3u == 0u ? 0x3f0000u : 0u);
|
||||
Extra[tid * 3u + 1u] = std::bit_cast<std::uint32_t>(-2.5f + 0.25f * static_cast<float>(tid % 21u));
|
||||
Extra[tid * 3u + 2u] = std::bit_cast<std::uint32_t>(0.5f * static_cast<float>(tid % 5u));
|
||||
}
|
||||
Extra[0] = 0x00001f20u;
|
||||
}
|
||||
|
||||
std::int32_t OffsetComponent(std::uint32_t tid, std::uint32_t component) {
|
||||
const auto field = (Extra[tid * 3u] >> (component * 8u)) & 0x3fu;
|
||||
return static_cast<std::int32_t>(field ^ 0x20u) - 0x20;
|
||||
}
|
||||
|
||||
float ReferenceTexel(int x, int y, float reference, const Sampler& sampler) {
|
||||
|
|
@ -125,15 +148,17 @@ float ReferenceTexel(int x, int y, float reference, const Sampler& sampler) {
|
|||
return reference <= red ? 1.0f : 0.0f;
|
||||
}
|
||||
|
||||
float Expected(std::uint32_t tid, const Sampler& sampler) {
|
||||
float Expected(std::uint32_t tid, const Sampler& sampler, bool offsets) {
|
||||
const float u = Input[tid * 3u + 1u] * static_cast<float>(Side);
|
||||
const float v = Input[tid * 3u + 2u] * static_cast<float>(Side);
|
||||
const float reference = std::clamp(Input[tid * 3u + 0u], 0.0f, 1.0f);
|
||||
if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast<int>(std::floor(u)), static_cast<int>(std::floor(v)), reference, sampler);
|
||||
const int offsetX = offsets ? OffsetComponent(tid, 0u) : 0;
|
||||
const int offsetY = offsets ? OffsetComponent(tid, 1u) : 0;
|
||||
if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast<int>(std::floor(u)) + offsetX, static_cast<int>(std::floor(v)) + offsetY, reference, sampler);
|
||||
const float cu = u - 0.5f;
|
||||
const float cv = v - 0.5f;
|
||||
const int x = static_cast<int>(std::floor(cu));
|
||||
const int y = static_cast<int>(std::floor(cv));
|
||||
const int x = static_cast<int>(std::floor(cu)) + offsetX;
|
||||
const int y = static_cast<int>(std::floor(cv)) + offsetY;
|
||||
const float a = cu - std::floor(cu);
|
||||
const float b = cv - std::floor(cv);
|
||||
const float top = ReferenceTexel(x, y, reference, sampler) * (1.0f - a) + ReferenceTexel(x + 1, y, reference, sampler) * a;
|
||||
|
|
@ -141,8 +166,8 @@ float Expected(std::uint32_t tid, const Sampler& sampler) {
|
|||
return top * (1.0f - b) + bottom * b;
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler) {
|
||||
std::vector<std::uint32_t> userData(20, 0u);
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span<const std::uint32_t> code = Code) {
|
||||
std::vector<std::uint32_t> userData(24, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output)));
|
||||
const auto texture = TextureDescriptor(format);
|
||||
|
|
@ -151,7 +176,8 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::
|
|||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
|
||||
std::copy(samplerWords.begin(), samplerWords.end(), userData.begin() + 16);
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const auto extra = BufferDescriptor(Extra.data(), static_cast<std::uint32_t>(sizeof(Extra)));
|
||||
std::copy(extra.begin(), extra.end(), userData.begin() + 20);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
|
|
@ -164,13 +190,14 @@ ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::
|
|||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) {
|
||||
void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name, bool offsets = false) {
|
||||
Output.fill(-1.0f);
|
||||
const auto result = Compile(device, Format8888UNorm, sampler);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
|
||||
const std::span<const std::uint32_t> code = offsets ? std::span<const std::uint32_t>(OffsetCode) : std::span<const std::uint32_t>(Code);
|
||||
const auto result = Compile(device, Format8888UNorm, sampler, code);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const float expected = Expected(tid, sampler);
|
||||
const float expected = Expected(tid, sampler, offsets);
|
||||
const float tolerance = sampler.filter == FilterPoint ? 0.0f : 1e-4f;
|
||||
Require(std::fabs(Output[tid] - expected) <= tolerance, std::string(name) + ": thread " + std::to_string(tid) + " compared to " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected));
|
||||
}
|
||||
|
|
@ -209,11 +236,16 @@ int main() {
|
|||
Run(*device, {ClampBorder, FilterPoint, BorderBlack}, "point, black border");
|
||||
Run(*device, {ClampBorder, FilterBilinear, BorderWhite}, "bilinear, white border");
|
||||
Run(*device, {ClampBorder, FilterBilinear, BorderBlack}, "bilinear, black border");
|
||||
Run(*device, {ClampEdge, FilterPoint, BorderBlack, 0u, 0x0140u}, "point, clamp to edge, sampler LOD bias");
|
||||
Run(*device, {ClampEdge, FilterPoint}, "point, clamp to edge, offsets, bias and LOD clamp", true);
|
||||
Run(*device, {ClampWrap, FilterBilinear}, "bilinear, wrap, offsets, bias and LOD clamp", true);
|
||||
Run(*device, {ClampBorder, FilterBilinear, BorderWhite, 0u, 0x3f00u}, "bilinear, white border, offsets, bias and LOD clamp", true);
|
||||
Reject(*device, Format8888UInt, {ClampEdge, FilterPoint}, "unsupported format");
|
||||
Reject(*device, Format8888UNorm, {ClampMirror, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border");
|
||||
Reject(*device, Format8888UNorm, {ClampHalfBorder, FilterPoint}, "wrap, clamp-to-edge or clamp-to-border");
|
||||
Reject(*device, Format8888UNorm, {ClampBorder, FilterPoint, BorderTable}, "border color table");
|
||||
Reject(*device, Format8888UNorm, {ClampEdge, FilterAnisoBilinear}, "point or bilinear");
|
||||
Reject(*device, Format8888UNorm, {ClampEdge, FilterBilinear, BorderBlack, ReductionMin}, "min or max reduction");
|
||||
std::puts("emulated color compare tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,189 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Inputs = 8;
|
||||
constexpr std::uint32_t Results = 64;
|
||||
constexpr std::uint32_t Operations = 11;
|
||||
constexpr std::uint32_t Format32_32UInt = 62;
|
||||
constexpr std::uint32_t Format32_32SInt = 63;
|
||||
constexpr std::uint32_t Format32_32Float = 64;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
alignas(256) std::array<std::uint32_t, Threads * Inputs> Input{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * Results> Output{};
|
||||
alignas(4096) std::array<std::uint32_t, 4096> Texels{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 219> Code{
|
||||
0x34020085, 0x34060088, 0xe0381000, 0x80000401, 0xe0341010, 0x80001001, 0xbf8c3f70, 0x7e100300,
|
||||
0x7e120280, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf03c2308,
|
||||
0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741000, 0x80010c03,
|
||||
0xe0741008, 0x80011203, 0x7e100300, 0x7e120281, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08,
|
||||
0x7e180306, 0x7e1a0307, 0xf0442308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208,
|
||||
0xbf8c3f70, 0xe0741010, 0x80010c03, 0xe0741018, 0x80011203, 0x7e100300, 0x7e120282, 0x7e140304,
|
||||
0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0482308, 0x00020c08, 0x7e240280,
|
||||
0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741020, 0x80010c03, 0xe0741028, 0x80011203,
|
||||
0x7e100300, 0x7e120283, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307,
|
||||
0xf0502308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741030,
|
||||
0x80010c03, 0xe0741038, 0x80011203, 0x7e100300, 0x7e120284, 0x7e140304, 0x7e160305, 0xf03c0308,
|
||||
0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0542308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308,
|
||||
0x00021208, 0xbf8c3f70, 0xe0741040, 0x80010c03, 0xe0741048, 0x80011203, 0x7e100300, 0x7e120285,
|
||||
0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0582308, 0x00020c08,
|
||||
0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741050, 0x80010c03, 0xe0741058,
|
||||
0x80011203, 0x7e100300, 0x7e120286, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306,
|
||||
0x7e1a0307, 0xf05c2308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70,
|
||||
0xe0741060, 0x80010c03, 0xe0741068, 0x80011203, 0x7e100300, 0x7e120287, 0x7e140304, 0x7e160305,
|
||||
0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0602308, 0x00020c08, 0x7e240280, 0x7e260280,
|
||||
0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741070, 0x80010c03, 0xe0741078, 0x80011203, 0x7e100300,
|
||||
0x7e120288, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0xf0642308,
|
||||
0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe0741080, 0x80010c03,
|
||||
0xe0741088, 0x80011203, 0x7e100300, 0x7e120289, 0x7e140304, 0x7e160305, 0xf03c0308, 0x00020a08,
|
||||
0x7e180306, 0x7e1a0307, 0xf0682308, 0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208,
|
||||
0xbf8c3f70, 0xe0741090, 0x80010c03, 0xe0741098, 0x80011203, 0x7e100300, 0x7e12028a, 0x7e140304,
|
||||
0x7e160305, 0xf03c0308, 0x00020a08, 0x7e180306, 0x7e1a0307, 0x7e1c0310, 0x7e1e0311, 0xf0402f08,
|
||||
0x00020c08, 0x7e240280, 0x7e260280, 0xf0442308, 0x00021208, 0xbf8c3f70, 0xe07410a0, 0x80010c03,
|
||||
0xe07410a8, 0x80011203, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<std::array<std::uint64_t, 2>, 12> Edges{{
|
||||
{0u, 0u}, {0u, 5u}, {5u, 0u}, {0x00000000ffffffffull, 1u},
|
||||
{0x0000000100000000ull, 1u}, {0x0000000100000000ull, 0x00000000ffffffffull}, {0x8000000000000000ull, 0x7fffffffffffffffull}, {0x7fffffffffffffffull, 0x8000000000000000ull},
|
||||
{0xffffffffffffffffull, 1u}, {1u, 0xffffffffffffffffull}, {0xffffffff00000000ull, 0x00000000ffffffffull}, {0x7fffffff00000000ull, 0x80000000ffffffffull},
|
||||
}};
|
||||
|
||||
std::uint64_t Word(const std::uint32_t* words) {
|
||||
return words[0] | static_cast<std::uint64_t>(words[1]) << 32u;
|
||||
}
|
||||
|
||||
void SetWord(std::uint32_t* words, std::uint64_t value) {
|
||||
words[0] = static_cast<std::uint32_t>(value);
|
||||
words[1] = static_cast<std::uint32_t>(value >> 32u);
|
||||
}
|
||||
|
||||
void FillInput() {
|
||||
std::uint64_t state = 0x9e3779b97f4a7c15ull;
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
auto* words = &Input[tid * Inputs];
|
||||
for (std::uint32_t j = 0; j < 3u; ++j) {
|
||||
state = state * 6364136223846793005ull + 1442695040888963407ull;
|
||||
SetWord(words + 2u * j, state ^ (state << 29u));
|
||||
}
|
||||
if (tid < Edges.size()) {
|
||||
SetWord(words, Edges[tid][0]);
|
||||
SetWord(words + 2, Edges[tid][1]);
|
||||
}
|
||||
if ((tid & 1u) != 0u) {
|
||||
SetWord(words + 4, Word(words));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const void* data, std::uint32_t width, std::uint32_t height, std::uint32_t format) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((width - 1u) & 3u) << 30u),
|
||||
((width - 1u) >> 2u) | ((height - 1u) << 14u),
|
||||
0xfacu | (Type2D << 28u),
|
||||
0u, 0u, 0u, 0u,
|
||||
};
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::uint32_t format) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
Texels.fill(0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
const auto texture = TextureDescriptor(Texels.data(), Threads, Operations, format);
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
std::string Hex(std::uint64_t value) {
|
||||
char text[24];
|
||||
std::snprintf(text, sizeof(text), "0x%016llx", static_cast<unsigned long long>(value));
|
||||
return text;
|
||||
}
|
||||
|
||||
void Check(std::uint32_t format) {
|
||||
constexpr std::array<const char*, Operations> names{"image_atomic_swap", "image_atomic_add", "image_atomic_sub", "image_atomic_smin", "image_atomic_umin", "image_atomic_smax", "image_atomic_umax", "image_atomic_and", "image_atomic_or", "image_atomic_xor", "image_atomic_cmpswap"};
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto* in = &Input[tid * Inputs];
|
||||
const std::uint64_t a = Word(in);
|
||||
const std::uint64_t b = Word(in + 2);
|
||||
const std::uint64_t c = Word(in + 4);
|
||||
const auto sa = static_cast<std::int64_t>(a);
|
||||
const auto sb = static_cast<std::int64_t>(b);
|
||||
const std::array<std::uint64_t, Operations> expected{
|
||||
b,
|
||||
a + b,
|
||||
a - b,
|
||||
static_cast<std::uint64_t>(std::min(sa, sb)),
|
||||
std::min(a, b),
|
||||
static_cast<std::uint64_t>(std::max(sa, sb)),
|
||||
std::max(a, b),
|
||||
a & b,
|
||||
a | b,
|
||||
a ^ b,
|
||||
a == c ? b : a,
|
||||
};
|
||||
for (std::uint32_t j = 0; j < Operations; ++j) {
|
||||
const std::uint64_t returned = Word(&Output[tid * Results + 4u * j]);
|
||||
const std::uint64_t stored = Word(&Output[tid * Results + 4u * j + 2u]);
|
||||
Require(returned == a, std::string(names[j]) + ", format " + std::to_string(format) + ": thread " + std::to_string(tid) + " returned " + Hex(returned) + ", expected " + Hex(a));
|
||||
Require(stored == expected[j], std::string(names[j]) + ", format " + std::to_string(format) + ": thread " + std::to_string(tid) + " stored " + Hex(stored) + ", expected " + Hex(expected[j]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
FillInput();
|
||||
for (const auto format : {Format32_32UInt, Format32_32SInt, Format32_32Float}) {
|
||||
Run(*device, format);
|
||||
Check(format);
|
||||
}
|
||||
std::puts("64-bit image atomics tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,295 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t LoadWidth = 2048;
|
||||
constexpr std::uint32_t LoadGroups = LoadWidth / Threads;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint32_t UnormFormat = 30;
|
||||
constexpr std::uint32_t UintFormat = 34;
|
||||
constexpr std::uint32_t SwizzleXYZ1 = 0x3acu;
|
||||
constexpr std::uint32_t SwizzleXZY1 = 0x374u;
|
||||
constexpr std::uint32_t SwizzleZYX1 = 0x32eu;
|
||||
constexpr std::uint32_t SwizzleX011 = 0x244u;
|
||||
constexpr std::uint32_t SwizzleYXZ1 = 0x3a5u;
|
||||
constexpr std::uint32_t SwizzleXYZW = 0xfacu;
|
||||
constexpr std::uint32_t D16 = 0x80000000u;
|
||||
constexpr std::uint32_t DivisionUlps = 3;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
constexpr std::size_t RoundTripOffset = 16384;
|
||||
constexpr std::size_t StoreOffset = 32768;
|
||||
constexpr std::array<std::uint32_t, 4> PointSampler{0x00000092u, 0x00fff000u, 0u, 0u};
|
||||
|
||||
alignas(256) std::array<std::uint32_t, LoadWidth * 4> Output{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
|
||||
|
||||
constexpr std::array<std::uint32_t, 15> LoadCode(std::uint32_t dmask, std::uint32_t d16) {
|
||||
return {
|
||||
0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x7e3e0280u, 0x7e140280u, 0x7e160280u, 0x7e180280u,
|
||||
0x7e1a0280u, 0xf0001008u | (dmask << 8u), 0x00010a1eu | d16, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u,
|
||||
};
|
||||
}
|
||||
|
||||
constexpr std::array<std::uint32_t, 9> StoreCode(std::uint32_t dmask, std::uint32_t d16) {
|
||||
return {
|
||||
0x34060084u, 0xe0381000u, 0x80030a03u, 0x7e3c0300u, 0x7e3e0280u, 0xbf8c3f70u, 0xf0201008u | (dmask << 8u), 0x00010a1eu | d16,
|
||||
0xbf810000u,
|
||||
};
|
||||
}
|
||||
|
||||
constexpr std::array<std::uint32_t, 9> SamplerCode(std::uint32_t word0) {
|
||||
return {
|
||||
0x34060084u, 0x7e280280u, 0x7e2a0280u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u,
|
||||
0xbf810000u,
|
||||
};
|
||||
}
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> RoundTrip{
|
||||
0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x7e3e0280u, 0xf0001708u, 0x00010a1eu, 0xbf8c3f70u, 0xf0201708u,
|
||||
0x00040a1eu, 0xbf810000u,
|
||||
};
|
||||
|
||||
alignas(256) constexpr auto LoadXyzw = LoadCode(0xfu, 0u);
|
||||
alignas(256) constexpr auto LoadXyz = LoadCode(0x7u, 0u);
|
||||
alignas(256) constexpr auto LoadXy = LoadCode(0x3u, 0u);
|
||||
alignas(256) constexpr auto LoadD16 = LoadCode(0xfu, D16);
|
||||
alignas(256) constexpr auto StoreXyz = StoreCode(0x7u, 0u);
|
||||
alignas(256) constexpr auto StoreXz = StoreCode(0x5u, 0u);
|
||||
alignas(256) constexpr auto StoreD16 = StoreCode(0x7u, D16);
|
||||
alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u);
|
||||
alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u);
|
||||
alignas(256) constexpr auto GetLod = SamplerCode(0xf1800308u);
|
||||
|
||||
constexpr std::array<std::uint32_t, 11> StoreValues{
|
||||
0x00000000u, 0x3f800000u, 0x3f000000u, 0x3eaaaaabu, 0x80000000u, 0xbf800000u,
|
||||
0x40000000u, 0x7f800000u, 0x7fc00000u, 0x000116c2u, 0x3f7fbe77u,
|
||||
};
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "image converted unorm: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint8_t* Data() { return block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle, std::uint32_t width) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((width - 1u) & 3u) << 30u),
|
||||
(width - 1u) >> 2u,
|
||||
swizzle | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
std::uint32_t LoadTexel(std::uint32_t index) {
|
||||
return index | ((2047u - index) << 11u) | ((index & 1023u) << 22u);
|
||||
}
|
||||
|
||||
std::uint32_t Field(std::uint32_t texel, std::uint32_t component) {
|
||||
return component == 2u ? texel >> 22u : (texel >> (component * 11u)) & 2047u;
|
||||
}
|
||||
|
||||
struct Expected {
|
||||
std::uint32_t bits;
|
||||
std::uint32_t ulps;
|
||||
};
|
||||
|
||||
Expected Selected(std::uint32_t texel, std::uint32_t swizzle, std::uint32_t component, bool unorm) {
|
||||
const auto selector = (swizzle >> (component * 3u)) & 7u;
|
||||
if (selector == 0u) return {0u, 0u};
|
||||
if (selector == 1u) return {unorm ? 0x3f800000u : 1u, 0u};
|
||||
const auto field = Field(texel, selector - 4u);
|
||||
const auto maximum = selector == 6u ? 1023u : 2047u;
|
||||
if (!unorm) return {field, 0u};
|
||||
return {std::bit_cast<std::uint32_t>(static_cast<float>(static_cast<double>(field) / maximum)), field == 0u || field == maximum ? 0u : DivisionUlps};
|
||||
}
|
||||
|
||||
std::uint32_t StoreField(std::uint32_t bits, std::uint32_t maximum) {
|
||||
const auto value = std::bit_cast<float>(bits);
|
||||
if (!(value > 0.0f)) return 0u;
|
||||
if (value >= 1.0f) return maximum;
|
||||
return static_cast<std::uint32_t>(std::nearbyint(static_cast<double>(value) * maximum));
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::array<std::uint32_t, 8>& texture, const std::array<std::uint32_t, 4>& extra, std::uint32_t groups, const std::array<std::uint32_t, 8>& target = {}) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(24, 0u);
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
std::copy(output.begin(), output.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
std::copy(extra.begin(), extra.end(), userData.begin() + 12);
|
||||
std::copy(target.begin(), target.end(), userData.begin() + 16);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void CheckLoad(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, std::span<const std::uint32_t> code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) {
|
||||
Run(device, code, TextureDescriptor(texels, format, swizzle, LoadWidth), {}, LoadGroups);
|
||||
for (std::uint32_t index = 0; index < LoadWidth; ++index) {
|
||||
std::uint32_t slot = 0;
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
if (((dmask >> component) & 1u) == 0u) continue;
|
||||
const auto expected = Selected(LoadTexel(index), swizzle, component, format == UnormFormat);
|
||||
const auto actual = Output[index * 4u + slot++];
|
||||
const auto distance = actual > expected.bits ? actual - expected.bits : expected.bits - actual;
|
||||
Require(distance <= expected.ulps, what + ": texel " + std::to_string(index) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected.bits) + " within " + std::to_string(expected.ulps) + " ULP");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckRoundTrip(AgcDriver::VulkanDevice& device, std::uint8_t* texels) {
|
||||
auto* target = texels + RoundTripOffset;
|
||||
std::fill(target, target + LoadWidth * 4u, static_cast<std::uint8_t>(0xa5u));
|
||||
Run(device, RoundTrip, TextureDescriptor(texels, UnormFormat, SwizzleXYZ1, LoadWidth), {}, LoadGroups, TextureDescriptor(target, UnormFormat, SwizzleXYZ1, LoadWidth));
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(target), LoadWidth * 4u, nullptr, "test");
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t index = 0; index < LoadWidth; ++index) {
|
||||
std::uint32_t actual = 0;
|
||||
std::memcpy(&actual, target + index * 4u, 4u);
|
||||
Require(actual == LoadTexel(index), "image_load then image_store: texel " + std::to_string(index) + " is " + Hex(actual) + ", expected " + Hex(LoadTexel(index)));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckStore(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span<const std::uint32_t> code, std::uint32_t dmask, const std::string& what) {
|
||||
std::fill(texels, texels + Threads * 4u, static_cast<std::uint8_t>(0xa5u));
|
||||
Input.fill(0u);
|
||||
std::array<std::uint32_t, Threads> expected{};
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const std::array<std::uint32_t, 3> values{StoreValues[tid % 11u], StoreValues[(tid + 4u) % 11u], StoreValues[(tid + 8u) % 11u]};
|
||||
std::uint32_t slot = 0;
|
||||
for (std::uint32_t component = 0; component < 3u; ++component) {
|
||||
if (((dmask >> component) & 1u) == 0u) continue;
|
||||
Input[tid * 4u + slot++] = values[component];
|
||||
expected[tid] |= StoreField(values[component], component == 2u ? 1023u : 2047u) << (component * 11u);
|
||||
}
|
||||
}
|
||||
Run(device, code, TextureDescriptor(texels, UnormFormat, SwizzleXYZ1, Threads), BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)), 1);
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(texels), Threads * 4u, nullptr, "test");
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
std::uint32_t actual = 0;
|
||||
std::memcpy(&actual, texels + tid * 4u, 4u);
|
||||
Require(actual == expected[tid], what + ": texel " + std::to_string(tid) + " is " + Hex(actual) + ", expected " + Hex(expected[tid]));
|
||||
}
|
||||
}
|
||||
|
||||
void RequireRefused(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array<std::uint32_t, 4>& extra, const std::string& reason, const std::string& what) {
|
||||
std::string refusal;
|
||||
try {
|
||||
Run(device, code, TextureDescriptor(texels, UnormFormat, swizzle, LoadWidth), extra, 1);
|
||||
} catch (const std::exception& error) {
|
||||
refusal = error.what();
|
||||
}
|
||||
Require(refusal.find(reason) != std::string::npos, what + " of a 10_11_11_UNORM image was not refused: " + refusal);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
GuestBlock block;
|
||||
auto* texels = block.Data();
|
||||
for (std::uint32_t index = 0; index < LoadWidth; ++index) {
|
||||
const auto texel = LoadTexel(index);
|
||||
std::memcpy(texels + index * 4u, &texel, 4u);
|
||||
}
|
||||
CheckLoad(*device, texels, LoadXyzw, 0xfu, UnormFormat, SwizzleXYZ1, "image_load dmask:0xf X Y Z 1");
|
||||
CheckLoad(*device, texels, LoadXy, 0x3u, UnormFormat, SwizzleXZY1, "image_load dmask:0x3 X Z Y 1");
|
||||
CheckLoad(*device, texels, LoadXyzw, 0xfu, UnormFormat, SwizzleZYX1, "image_load dmask:0xf Z Y X 1");
|
||||
CheckLoad(*device, texels, LoadXyzw, 0xfu, UnormFormat, SwizzleX011, "image_load dmask:0xf X 0 1 1");
|
||||
CheckLoad(*device, texels, LoadXyz, 0x7u, UintFormat, SwizzleYXZ1, "image_load of 10_11_11_UINT dmask:0x7 Y X Z 1");
|
||||
CheckRoundTrip(*device, texels);
|
||||
CheckStore(*device, texels + StoreOffset, StoreXyz, 0x7u, "image_store dmask:0x7");
|
||||
CheckStore(*device, texels + StoreOffset, StoreXz, 0x5u, "image_store dmask:0x5");
|
||||
RequireRefused(*device, SampleLz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_sample_lz");
|
||||
RequireRefused(*device, Gather4Lz, texels, SwizzleXYZ1, PointSampler, "samples or gathers a converted unorm image", "image_gather4_lz");
|
||||
RequireRefused(*device, GetLod, texels, SwizzleXYZ1, PointSampler, "queries the level of detail of a converted unorm image", "image_get_lod");
|
||||
RequireRefused(*device, LoadD16, texels, SwizzleXYZ1, {}, "converted unorm image with 16-bit data", "image_load d16");
|
||||
RequireRefused(*device, StoreD16, texels + StoreOffset, SwizzleXYZ1, BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)), "converted unorm image with 16-bit data", "image_store d16");
|
||||
RequireRefused(*device, LoadXyzw, texels, SwizzleXYZW, {}, "selects a channel the converted image format does not have", "image_load with DST_SEL X Y Z W");
|
||||
RequireRefused(*device, StoreXyz, texels + StoreOffset, SwizzleXYZW, BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)), "selects a channel the converted image format does not have", "image_store with DST_SEL X Y Z W");
|
||||
std::puts("image converted unorm tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Width = 8;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
constexpr std::uint32_t Type1D = 8;
|
||||
constexpr std::uint32_t ClampWrap = 0;
|
||||
constexpr std::uint32_t ClampEdge = 2;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
|
||||
alignas(256) std::array<float, Threads * 4> Output{};
|
||||
alignas(4096) std::array<std::uint8_t, 8192> Texels{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> Gather1D{
|
||||
0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0100, 0x00820603, 0xbf8c3f70, 0xe0781000,
|
||||
0x80010601, 0xbf810000,
|
||||
};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 10> Gather1DOffset{
|
||||
0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf15c0100, 0x00820602, 0xbf8c3f70, 0xe0781000,
|
||||
0x80010601, 0xbf810000,
|
||||
};
|
||||
|
||||
struct Variant {
|
||||
std::span<const std::uint32_t> code;
|
||||
bool offset;
|
||||
std::uint32_t clamp;
|
||||
const char* name;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor() {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Texels.data()));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u),
|
||||
(Width - 1u) >> 2u,
|
||||
0xfacu | (Type1D << 28u),
|
||||
0u, 0u, 0u, 0u,
|
||||
};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> SamplerDescriptor(std::uint32_t clamp) {
|
||||
return {clamp | (ClampEdge << 3u) | (ClampEdge << 6u), 0u, 0u, 0u};
|
||||
}
|
||||
|
||||
std::uint8_t Red(std::uint32_t x) {
|
||||
return static_cast<std::uint8_t>(x * 31u + 5u);
|
||||
}
|
||||
|
||||
void FillTexels() {
|
||||
Texels.fill(0);
|
||||
const auto descriptor = TextureDescriptor();
|
||||
const auto surface = AgcDriver::Graphics::DescribeSurface(AgcDriver::Graphics::DecodeTextureResource(descriptor));
|
||||
const auto& mip = surface.mips.at(0);
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
auto* texel = Texels.data() + mip.tiledOffset + x * 4u;
|
||||
texel[0] = Red(x);
|
||||
texel[1] = 0x11;
|
||||
texel[2] = 0x22;
|
||||
texel[3] = 0xff;
|
||||
}
|
||||
}
|
||||
|
||||
void FillInput() {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto offset = static_cast<std::int32_t>(tid % 11u) - 5;
|
||||
Input[tid * 4u + 0u] = (static_cast<std::uint32_t>(offset) & 0x3fu) | (tid % 3u == 0u ? 0x3f00u : 0u);
|
||||
Input[tid * 4u + 1u] = std::bit_cast<std::uint32_t>(-0.3f + 0.05f * static_cast<float>(tid));
|
||||
Input[tid * 4u + 2u] = 0u;
|
||||
Input[tid * 4u + 3u] = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t Texel(int x, std::uint32_t clamp) {
|
||||
const int size = static_cast<int>(Width);
|
||||
const int address = clamp == ClampEdge ? std::clamp(x, 0, size - 1) : ((x % size) + size) % size;
|
||||
return Red(static_cast<std::uint32_t>(address));
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> Expected(std::uint32_t tid, const Variant& variant) {
|
||||
const float u = std::bit_cast<float>(Input[tid * 4u + 1u]);
|
||||
int left = static_cast<int>(std::floor(u * static_cast<float>(Width) - 0.5f));
|
||||
if (variant.offset) {
|
||||
const auto field = Input[tid * 4u] & 0x3fu;
|
||||
left += static_cast<int>(field ^ 0x20u) - 0x20;
|
||||
}
|
||||
const auto first = Texel(left, variant.clamp);
|
||||
const auto second = Texel(left + 1, variant.clamp);
|
||||
return {first, second, second, first};
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, const Variant& variant) {
|
||||
std::vector<std::uint32_t> userData(20, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output)));
|
||||
const auto texture = TextureDescriptor();
|
||||
const auto sampler = SamplerDescriptor(variant.clamp);
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
|
||||
std::copy(sampler.begin(), sampler.end(), userData.begin() + 16);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(variant.code.data()), std::as_bytes(variant.code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(variant.code.data()), variant.code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, const Variant& variant) {
|
||||
Output.fill(-1.0f);
|
||||
const auto result = Compile(device, variant);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(variant.code.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto expected = Expected(tid, variant);
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
const float actual = Output[tid * 4u + component];
|
||||
const float scaled = actual * 255.0f;
|
||||
Require(std::fabs(scaled - std::round(scaled)) < 1e-3f && static_cast<std::uint32_t>(std::lround(scaled)) == expected[component], std::string(variant.name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + std::to_string(scaled) + "/255, expected " + std::to_string(expected[component]) + "/255");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
FillTexels();
|
||||
FillInput();
|
||||
Run(*device, {Gather1D, false, ClampEdge, "1D gather, clamp to edge"});
|
||||
Run(*device, {Gather1D, false, ClampWrap, "1D gather, wrap"});
|
||||
Run(*device, {Gather1DOffset, true, ClampEdge, "1D gather with offsets, clamp to edge"});
|
||||
Run(*device, {Gather1DOffset, true, ClampWrap, "1D gather with offsets, wrap"});
|
||||
std::puts("image 1D gather tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -329,10 +329,12 @@ int main() {
|
|||
if (device->Target().subgroupSize < Threads) {
|
||||
std::printf("EXEC-masked cases skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize);
|
||||
}
|
||||
const bool computedOffsets = device->Target().nonConstantImageOffsets;
|
||||
if (!computedOffsets) std::puts("2D cases skipped, their computed texel offsets need VK_KHR_maintenance8");
|
||||
std::vector<std::uint32_t> waves{Threads};
|
||||
if (device->Target().subgroupSize >= Threads) waves.push_back(Wave64Threads);
|
||||
for (const auto threads : waves) {
|
||||
Run(*device, Code2D, threads, 9u, Height, 1u, Names2D, Masked2D, Expected2D);
|
||||
if (computedOffsets) Run(*device, Code2D, threads, 9u, Height, 1u, Names2D, Masked2D, Expected2D);
|
||||
Run(*device, Code3D, threads, 10u, Height, Depth, Names3D, Masked3D, Expected3D);
|
||||
Run(*device, Code1D, threads, 8u, 1u, 1u, Names1D, Masked1D, Expected1D);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,319 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Size = 4;
|
||||
constexpr std::uint32_t Levels = 2;
|
||||
constexpr std::uint32_t Format32SInt = 21;
|
||||
constexpr std::uint32_t Format32Float = 22;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint32_t ReductionMin = 1;
|
||||
constexpr std::uint32_t ReductionMax = 2;
|
||||
constexpr std::uint32_t FilterPoint = 0;
|
||||
constexpr std::uint32_t FilterBilinear = 1;
|
||||
constexpr std::uint32_t MipNone = 0;
|
||||
constexpr std::uint32_t MipPoint = 1;
|
||||
constexpr std::uint32_t MipLinear = 2;
|
||||
constexpr float Margin = 0.05f;
|
||||
constexpr std::array<float, Size * Size> Level0{17, 11, 23, 14, 20, 25, 12, 19, 13, 22, 16, 24, 21, 10, 18, 15};
|
||||
constexpr std::array<float, (Size / 2) * (Size / 2)> Level1{5, 30, 16.5f, 9};
|
||||
|
||||
alignas(256) std::array<float, Threads * 3> Input{};
|
||||
alignas(256) std::array<float, Threads * 4> Output{};
|
||||
alignas(4096) std::array<std::uint8_t, 4096> Texels{};
|
||||
alignas(4096) std::array<std::uint8_t, 4096> Colors{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> Code{
|
||||
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70,
|
||||
0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 13> GatherCode{
|
||||
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70,
|
||||
0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 19> LoadedCode{
|
||||
0x1614008c, 0xe03c1000, 0x8000010a, 0x36180083, 0x7e1a0280, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xf0000108, 0x0005050c,
|
||||
0xbf8c3f70, 0x34160082, 0xe0701000, 0x8001040b, 0xe0701080, 0x8001050b, 0xbf810000, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 17> SampledCode{
|
||||
0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xf0900108, 0x00850501, 0xbf8c3f70,
|
||||
0x34160082, 0xe0701000, 0x8001040b, 0xe0701080, 0x8001050b, 0xbf810000, 0xbf810000, 0xbf810000,
|
||||
};
|
||||
|
||||
struct Sampler {
|
||||
std::uint32_t reduction;
|
||||
std::uint32_t filter;
|
||||
std::uint32_t mip;
|
||||
};
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(std::uint32_t format, const void* texels = Texels.data(), std::uint32_t levels = Levels) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((Size - 1u) & 3u) << 30u),
|
||||
((Size - 1u) >> 2u) | ((Size - 1u) << 14u),
|
||||
0xfacu | ((levels - 1u) << 16u) | (Type2D << 28u),
|
||||
0u,
|
||||
(levels - 1u) << 4u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> SamplerDescriptor(const Sampler& sampler) {
|
||||
return {0x92u | (sampler.reduction << 29u), 0x00fff000u, (sampler.filter << 20u) | (sampler.filter << 22u) | (1u << 24u) | (sampler.mip << 26u), 0u};
|
||||
}
|
||||
|
||||
void FillTexels() {
|
||||
const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format32Float, Size, Size, Levels);
|
||||
Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texels.size(), "image sample reduction: the mip chain does not fit the texel storage");
|
||||
Texels.fill(0);
|
||||
for (std::uint32_t level = 0; level < Levels; ++level) {
|
||||
const auto& mip = mips[level];
|
||||
const auto& values = level == 0 ? std::span<const float>(Level0) : std::span<const float>(Level1);
|
||||
for (std::uint32_t y = 0; y < mip.height; ++y) {
|
||||
for (std::uint32_t x = 0; x < mip.width; ++x) {
|
||||
const auto bits = std::bit_cast<std::uint32_t>(values[y * mip.width + x]);
|
||||
auto* texel = &Texels[mip.tiledOffset + static_cast<std::uint64_t>(y) * mip.pitchBytes + x * 4u];
|
||||
for (std::uint32_t byte = 0; byte < 4u; ++byte) texel[byte] = static_cast<std::uint8_t>(bits >> (byte * 8u));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::uint8_t Red(int x, int y) {
|
||||
const auto cx = static_cast<std::uint32_t>(std::clamp(x, 0, static_cast<int>(Size) - 1));
|
||||
const auto cy = static_cast<std::uint32_t>(std::clamp(y, 0, static_cast<int>(Size) - 1));
|
||||
return static_cast<std::uint8_t>(17u * ((cy * 7u + cx * 5u) % 16u));
|
||||
}
|
||||
|
||||
void FillColors() {
|
||||
const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888UNorm, Size, Size, 1);
|
||||
Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Colors.size(), "image sample reduction: the 8_8_8_8 texture does not fit its storage");
|
||||
Colors.fill(0);
|
||||
for (std::uint32_t y = 0; y < Size; ++y) {
|
||||
for (std::uint32_t x = 0; x < Size; ++x) {
|
||||
auto* texel = &Colors[mips[0].tiledOffset + static_cast<std::uint64_t>(y) * mips[0].pitchBytes + x * 4u];
|
||||
texel[0] = Red(static_cast<int>(x), static_cast<int>(y));
|
||||
texel[3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Safe(float coordinate) {
|
||||
for (std::uint32_t size = Size; size >= Size / 2; size /= 2) {
|
||||
for (const float offset : {0.0f, -0.5f}) {
|
||||
const float scaled = coordinate * static_cast<float>(size) + offset;
|
||||
const float fraction = scaled - std::floor(scaled);
|
||||
if (fraction < Margin || fraction > 1.0f - Margin) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void FillInput() {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const float u = (static_cast<float>((tid * 5u + 1u) % 12u) - 1.7f) / 8.0f;
|
||||
const float v = (static_cast<float>((tid * 7u + 3u) % 12u) - 1.7f) / 8.0f;
|
||||
Require(Safe(u) && Safe(v), "image sample reduction: thread " + std::to_string(tid) + " samples too close to a texel boundary");
|
||||
Input[tid * 3u + 0u] = u;
|
||||
Input[tid * 3u + 1u] = v;
|
||||
Input[tid * 3u + 2u] = tid % 2u == 0u ? 0.25f : 0.75f;
|
||||
}
|
||||
}
|
||||
|
||||
float Texel(std::uint32_t level, int x, int y) {
|
||||
const int size = static_cast<int>(Size >> level);
|
||||
const auto cx = static_cast<std::uint32_t>(std::clamp(x, 0, size - 1));
|
||||
const auto cy = static_cast<std::uint32_t>(std::clamp(y, 0, size - 1));
|
||||
return level == 0 ? Level0[cy * Size + cx] : Level1[cy * (Size / 2) + cx];
|
||||
}
|
||||
|
||||
float Expected(std::uint32_t tid, const Sampler& sampler) {
|
||||
const float u = Input[tid * 3u + 0u];
|
||||
const float v = Input[tid * 3u + 1u];
|
||||
const float lod = Input[tid * 3u + 2u];
|
||||
std::vector<float> footprint;
|
||||
const auto gather = [&](std::uint32_t level) {
|
||||
const float size = static_cast<float>(Size >> level);
|
||||
if (sampler.filter == FilterPoint) {
|
||||
footprint.push_back(Texel(level, static_cast<int>(std::floor(u * size)), static_cast<int>(std::floor(v * size))));
|
||||
return;
|
||||
}
|
||||
const int x = static_cast<int>(std::floor(u * size - 0.5f));
|
||||
const int y = static_cast<int>(std::floor(v * size - 0.5f));
|
||||
for (int dy = 0; dy < 2; ++dy) {
|
||||
for (int dx = 0; dx < 2; ++dx) footprint.push_back(Texel(level, x + dx, y + dy));
|
||||
}
|
||||
};
|
||||
gather(sampler.mip == MipPoint && lod > 0.5f ? 1u : 0u);
|
||||
return sampler.reduction == ReductionMin ? *std::min_element(footprint.begin(), footprint.end()) : *std::max_element(footprint.begin(), footprint.end());
|
||||
}
|
||||
|
||||
std::array<float, 4> ExpectedGather(std::uint32_t tid) {
|
||||
const int x = static_cast<int>(std::floor(Input[tid * 3u + 0u] * static_cast<float>(Size) - 0.5f));
|
||||
const int y = static_cast<int>(std::floor(Input[tid * 3u + 1u] * static_cast<float>(Size) - 0.5f));
|
||||
return {Texel(0, x, y + 1), Texel(0, x + 1, y + 1), Texel(0, x + 1, y), Texel(0, x, y)};
|
||||
}
|
||||
|
||||
ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span<const std::uint32_t> code = Code) {
|
||||
std::vector<std::uint32_t> userData(28, 0u);
|
||||
const auto input = BufferDescriptor(Input.data(), static_cast<std::uint32_t>(sizeof(Input)));
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(sizeof(Output)));
|
||||
const auto texture = TextureDescriptor(format);
|
||||
const auto samplerWords = SamplerDescriptor(sampler);
|
||||
const auto colors = TextureDescriptor(Format8888UNorm, Colors.data(), 1);
|
||||
std::copy(input.begin(), input.end(), userData.begin());
|
||||
std::copy(output.begin(), output.end(), userData.begin() + 4);
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 8);
|
||||
std::copy(samplerWords.begin(), samplerWords.end(), userData.begin() + 16);
|
||||
std::copy(colors.begin(), colors.end(), userData.begin() + 20);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
return ShaderRecompiler::Recompile(request);
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) {
|
||||
Output.fill(-1.0f);
|
||||
const auto result = Compile(device, Format32Float, sampler);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(Code.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const float expected = Expected(tid, sampler);
|
||||
Require(Output[tid] == expected, std::string(name) + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + ", expected " + std::to_string(expected));
|
||||
}
|
||||
}
|
||||
|
||||
void Gather(AgcDriver::VulkanDevice& device, const Sampler& sampler, const char* name) {
|
||||
Output.fill(-1.0f);
|
||||
const auto result = Compile(device, Format32Float, sampler, GatherCode);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(GatherCode.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto expected = ExpectedGather(tid);
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
const float sampled = Output[tid * 4u + component];
|
||||
Require(sampled == expected[component], std::string(name) + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " gathered " + std::to_string(sampled) + ", expected " + std::to_string(expected[component]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::uint32_t PairedSamplers(const ShaderRecompiler::RecompileResult& result, std::uint32_t format) {
|
||||
for (const auto& binding : result.bindings) {
|
||||
if (binding.kind != ShaderRecompiler::DescriptorKind::SampledImage) continue;
|
||||
for (std::uint32_t element = 0; element < binding.count; ++element) {
|
||||
if (((binding.guestDescriptor.at(element * 8u + 1u) >> 20u) & 0x1ffu) == format) return binding.imageSamplers.at(element);
|
||||
}
|
||||
}
|
||||
Require(false, "image sample reduction: no sampled image of format " + std::to_string(format));
|
||||
return 0;
|
||||
}
|
||||
|
||||
float ExpectedColor(std::uint32_t tid, bool sampled) {
|
||||
if (!sampled) return static_cast<float>(Red(static_cast<int>(tid & 3u), 0)) / 255.0f;
|
||||
const int x = static_cast<int>(std::floor(Input[tid * 3u + 0u] * static_cast<float>(Size) - 0.5f));
|
||||
const int y = static_cast<int>(std::floor(Input[tid * 3u + 1u] * static_cast<float>(Size) - 0.5f));
|
||||
return static_cast<float>(std::min({Red(x, y), Red(x + 1, y), Red(x, y + 1), Red(x + 1, y + 1)})) / 255.0f;
|
||||
}
|
||||
|
||||
void RunWithColors(AgcDriver::VulkanDevice& device, const ShaderRecompiler::RecompileResult& result, std::span<const std::uint32_t> code, bool sampled, const char* name) {
|
||||
const Sampler sampler{ReductionMin, FilterBilinear, MipPoint};
|
||||
Output.fill(-1.0f);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const float expected = Expected(tid, sampler);
|
||||
Require(Output[tid] == expected, std::string(name) + ": thread " + std::to_string(tid) + " sampled " + std::to_string(Output[tid]) + " from 32_FLOAT, expected " + std::to_string(expected));
|
||||
const float color = ExpectedColor(tid, sampled);
|
||||
Require(std::fabs(Output[Threads + tid] - color) <= 1.0f / 1024.0f, std::string(name) + ": thread " + std::to_string(tid) + " read " + std::to_string(Output[Threads + tid]) + " from 8_8_8_8, expected " + std::to_string(color));
|
||||
}
|
||||
}
|
||||
|
||||
void Reject(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::string_view reason) {
|
||||
try {
|
||||
static_cast<void>(Compile(device, format, sampler));
|
||||
} catch (const std::exception& error) {
|
||||
Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string("unexpected rejection: ") + error.what());
|
||||
return;
|
||||
}
|
||||
Require(false, std::string("expected rejection: ") + std::string(reason));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
FillTexels();
|
||||
FillColors();
|
||||
FillInput();
|
||||
const Sampler minBilinear{ReductionMin, FilterBilinear, MipPoint};
|
||||
const auto loaded = Compile(*device, Format32Float, minBilinear, LoadedCode);
|
||||
Require(PairedSamplers(loaded, Format32Float) == 1u && PairedSamplers(loaded, Format8888UNorm) == 0u, "image_load of 8_8_8_8 next to a min sampler: wrong image-sampler pairs");
|
||||
const auto sampled = Compile(*device, Format32Float, minBilinear, SampledCode);
|
||||
Require(PairedSamplers(sampled, Format32Float) == 1u && PairedSamplers(sampled, Format8888UNorm) == 1u, "8_8_8_8 sampled through a min sampler: wrong image-sampler pairs");
|
||||
static_cast<void>(Compile(*device, Format32SInt, {ReductionMin, FilterPoint, MipPoint}));
|
||||
Reject(*device, Format32SInt, {ReductionMin, FilterBilinear, MipPoint}, "needs point filtering");
|
||||
Reject(*device, Format32SInt, {ReductionMax, FilterPoint, MipLinear}, "needs point filtering");
|
||||
if (!device->SamplerFilterMinmax()) {
|
||||
std::puts("skipped, the device has no min/max sampler reduction with component mapping");
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
Run(*device, {ReductionMin, FilterBilinear, MipPoint}, "min, bilinear, point mip");
|
||||
Run(*device, {ReductionMax, FilterBilinear, MipPoint}, "max, bilinear, point mip");
|
||||
Run(*device, {ReductionMin, FilterPoint, MipPoint}, "min, point, point mip");
|
||||
Run(*device, {ReductionMax, FilterPoint, MipPoint}, "max, point, point mip");
|
||||
Run(*device, {ReductionMin, FilterBilinear, MipNone}, "min, bilinear, no mip");
|
||||
Run(*device, {ReductionMax, FilterPoint, MipNone}, "max, point, no mip");
|
||||
Gather(*device, {ReductionMin, FilterBilinear, MipPoint}, "gather4_lz, min");
|
||||
Gather(*device, {ReductionMax, FilterBilinear, MipPoint}, "gather4_lz, max");
|
||||
RunWithColors(*device, loaded, LoadedCode, false, "min 32_FLOAT, image_load 8_8_8_8");
|
||||
try {
|
||||
RunWithColors(*device, sampled, SampledCode, true, "min 32_FLOAT and 8_8_8_8");
|
||||
std::puts("the device filters 8_8_8_8 UNORM with min/max reduction");
|
||||
} catch (const std::exception& error) {
|
||||
Require(std::string_view(error.what()).find("format 37 does not support min/max filtering is sampled through a min or max reduction sampler") != std::string_view::npos, std::string("min 32_FLOAT and 8_8_8_8: unexpected error: ") + error.what());
|
||||
}
|
||||
std::puts("image sample reduction tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 128;
|
||||
constexpr std::uint32_t Words = 16;
|
||||
constexpr std::uint32_t ResultWord = 4;
|
||||
constexpr std::uint32_t Results = 12;
|
||||
constexpr std::int32_t Width = 13;
|
||||
constexpr std::int32_t Height = 7;
|
||||
constexpr std::uint32_t StorageLevels = 4;
|
||||
constexpr std::uint32_t Format8888UNorm = 56;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint8_t OtherLevelTexel = 0x40u;
|
||||
alignas(256) std::array<std::uint32_t, Threads * Words> Buffer{};
|
||||
alignas(256) std::array<std::uint8_t, 16384> SingleLevel{};
|
||||
alignas(256) std::array<std::uint8_t, 16384> MultiLevel{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 40> Code{
|
||||
0x34020086, 0xe0301000, 0x80000201, 0xe0301004, 0x80000301, 0xbf8c3f70, 0x7e0802ff, 0x402ccccd,
|
||||
0xf09c0f08, 0x00610802, 0xf0900f08, 0x00610c02, 0xf0800f08, 0x00611002, 0xbf8c3f70, 0xe0701010,
|
||||
0x80000801, 0xe0701014, 0x80000901, 0xe0701018, 0x80000a01, 0xe070101c, 0x80000b01, 0xe0701020,
|
||||
0x80000c01, 0xe0701024, 0x80000d01, 0xe0701028, 0x80000e01, 0xe070102c, 0x80000f01, 0xe0701030,
|
||||
0x80001001, 0xe0701034, 0x80001101, 0xe0701038, 0x80001201, 0xe070103c, 0x80001301, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::array<const char*, 3> Instructions{"image_sample_lz", "image_sample_l 2.7", "image_sample"};
|
||||
|
||||
struct SamplerCase {
|
||||
const char* name;
|
||||
std::array<std::uint32_t, 4> words;
|
||||
bool linear;
|
||||
bool border;
|
||||
};
|
||||
|
||||
constexpr std::array<SamplerCase, 5> Samplers{{
|
||||
{"bilinear clamp-to-last-texel, point mips, MAX_LOD 0xfff", {0x00008092u, 0x00fff000u, 0x05500000u, 0u}, true, false},
|
||||
{"point clamp-to-last-texel, point mips", {0x00008092u, 0x00fff000u, 0x04000000u, 0u}, false, false},
|
||||
{"bilinear clamp-to-last-texel, linear mips, MIN_LOD 1, LOD bias 1.5", {0x00008092u, 0x00fff100u, 0x09500180u, 0u}, true, false},
|
||||
{"bilinear clamp-to-border, transparent black", {0x000080b6u, 0x00fff000u, 0x05500000u, 0u}, true, true},
|
||||
{"point clamp-to-border, no mips, transparent black", {0x000080b6u, 0u, 0u, 0u}, false, true},
|
||||
}};
|
||||
|
||||
struct Coordinate {
|
||||
float u;
|
||||
float v;
|
||||
};
|
||||
|
||||
std::vector<Coordinate> Coordinates() {
|
||||
std::vector<float> us;
|
||||
for (std::int32_t k = 0; k < Width; ++k) {
|
||||
for (const float fraction : {0.5f, 0.0f, 0.25f, 0.75f}) us.push_back(static_cast<float>(k) + fraction);
|
||||
}
|
||||
for (const float edge : {-1.0f, -0.5f, 13.0f, 14.0f, 12.75f}) us.push_back(edge);
|
||||
constexpr std::array<float, 3> fractions{0.5f, 0.0f, 0.75f};
|
||||
std::vector<Coordinate> result;
|
||||
for (std::size_t index = 0; index < us.size(); ++index) {
|
||||
for (std::size_t fraction = 0; fraction < fractions.size(); ++fraction) {
|
||||
result.push_back({us[index], static_cast<float>((index * fractions.size() + fraction) % Height) + fractions[fraction]});
|
||||
}
|
||||
}
|
||||
for (std::int32_t x = 0; x < Width; ++x) {
|
||||
const float v = static_cast<float>(x % Height) + 0.5f;
|
||||
result.push_back({static_cast<float>(x) - 1.0f, v});
|
||||
result.push_back({static_cast<float>(x) + 2.0f, v});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::array<double, 4> TexelOf(std::int32_t x, std::int32_t y) {
|
||||
return {240.0 * ((x + y) & 1), 16.0 * x + 16.0, 32.0 * y + 32.0, 240.0};
|
||||
}
|
||||
|
||||
void FillTexture(std::span<std::uint8_t> texels, std::uint32_t levels) {
|
||||
const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Format8888UNorm, Width, Height, levels);
|
||||
Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= texels.size(), "image sample unnormalized: the mip chain does not fit the texel storage");
|
||||
std::fill(texels.begin(), texels.end(), std::uint8_t{0xeeu});
|
||||
for (std::uint32_t level = 0; level < levels; ++level) {
|
||||
const auto& mip = mips[level];
|
||||
for (std::uint32_t y = 0; y < mip.height; ++y) {
|
||||
for (std::uint32_t x = 0; x < mip.width; ++x) {
|
||||
auto* texel = &texels[mip.tiledOffset + static_cast<std::uint64_t>(y) * mip.pitchBytes + x * 4u];
|
||||
const auto value = TexelOf(static_cast<std::int32_t>(x), static_cast<std::int32_t>(y));
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
texel[component] = level == 0u ? static_cast<std::uint8_t>(value[component]) : OtherLevelTexel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const void* data, std::uint32_t lastLevel, std::uint32_t maxMip) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | ((static_cast<std::uint32_t>(Width - 1) & 3u) << 30u),
|
||||
(static_cast<std::uint32_t>(Width - 1) >> 2u) | (static_cast<std::uint32_t>(Height - 1) << 14u),
|
||||
0xfacu | (lastLevel << 16u) | (Type2D << 28u),
|
||||
0u,
|
||||
maxMip << 4u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
using Samples = std::vector<std::array<std::uint32_t, Results>>;
|
||||
|
||||
Samples Run(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, const std::array<std::uint32_t, 4>& sampler, const std::vector<Coordinate>& coordinates) {
|
||||
Samples samples;
|
||||
for (std::size_t first = 0; first < coordinates.size(); first += Threads) {
|
||||
Buffer.fill(0xdeadbeefu);
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
const auto coordinate = first + lane < coordinates.size() ? coordinates[first + lane] : Coordinate{0.5f, 0.5f};
|
||||
Buffer[lane * Words] = std::bit_cast<std::uint32_t>(coordinate.u);
|
||||
Buffer[lane * Words + 1u] = std::bit_cast<std::uint32_t>(coordinate.v);
|
||||
}
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
const auto buffer = BufferDescriptor(Buffer.data(), static_cast<std::uint32_t>(Buffer.size() * 4u));
|
||||
std::copy(buffer.begin(), buffer.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
std::copy(sampler.begin(), sampler.end(), userData.begin() + 12);
|
||||
const std::span<const std::uint32_t> code(Code);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t lane = 0; lane < Threads && first + lane < coordinates.size(); ++lane) {
|
||||
std::array<std::uint32_t, Results> values{};
|
||||
std::copy_n(&Buffer[lane * Words + ResultWord], Results, values.begin());
|
||||
samples.push_back(values);
|
||||
}
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
std::array<double, 4> Fetch(const SamplerCase& sampler, std::int32_t x, std::int32_t y) {
|
||||
if (sampler.border && (x < 0 || x >= Width || y < 0 || y >= Height)) return {0.0, 0.0, 0.0, 0.0};
|
||||
return TexelOf(std::clamp(x, 0, Width - 1), std::clamp(y, 0, Height - 1));
|
||||
}
|
||||
|
||||
std::array<double, 4> Reference(const SamplerCase& sampler, const Coordinate& coordinate) {
|
||||
if (!sampler.linear) return Fetch(sampler, static_cast<std::int32_t>(std::floor(coordinate.u)), static_cast<std::int32_t>(std::floor(coordinate.v)));
|
||||
const double u = static_cast<double>(coordinate.u) - 0.5;
|
||||
const double v = static_cast<double>(coordinate.v) - 0.5;
|
||||
const auto i = static_cast<std::int32_t>(std::floor(u));
|
||||
const auto j = static_cast<std::int32_t>(std::floor(v));
|
||||
const double alpha = u - i;
|
||||
const double beta = v - j;
|
||||
std::array<double, 4> result{};
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
result[component] = (1.0 - alpha) * (1.0 - beta) * Fetch(sampler, i, j)[component] + alpha * (1.0 - beta) * Fetch(sampler, i + 1, j)[component]
|
||||
+ (1.0 - alpha) * beta * Fetch(sampler, i, j + 1)[component] + alpha * beta * Fetch(sampler, i + 1, j + 1)[component];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Check(const SamplerCase& sampler, const char* image, const std::vector<Coordinate>& coordinates, const Samples& samples) {
|
||||
Require(samples.size() == coordinates.size(), "image sample unnormalized: a dispatch lost samples");
|
||||
const double tolerance = sampler.linear ? 0.5 : 0.25;
|
||||
for (std::size_t index = 0; index < coordinates.size(); ++index) {
|
||||
const auto expected = Reference(sampler, coordinates[index]);
|
||||
for (std::uint32_t instruction = 0; instruction < Instructions.size(); ++instruction) {
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
const double value = static_cast<double>(std::bit_cast<float>(samples[index][instruction * 4u + component])) * 255.0;
|
||||
Require(std::fabs(value - expected[component]) <= tolerance, std::string(Instructions[instruction]) + ", " + sampler.name + ", " + image + ": (" + std::to_string(coordinates[index].u) + ", " + std::to_string(coordinates[index].v) + ") component " + std::to_string(component) + " is " + std::to_string(value) + ", expected " + std::to_string(expected[component]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectFailure(AgcDriver::VulkanDevice& device, const std::array<std::uint32_t, 8>& texture, const std::array<std::uint32_t, 4>& sampler, std::string_view reason, const char* what) {
|
||||
const std::vector<Coordinate> one{{0.5f, 0.5f}};
|
||||
try {
|
||||
static_cast<void>(Run(device, texture, sampler, one));
|
||||
} catch (const std::exception& error) {
|
||||
Require(std::string_view(error.what()).find(reason) != std::string_view::npos, std::string(what) + ": unexpected error: " + error.what());
|
||||
return;
|
||||
}
|
||||
throw std::runtime_error(std::string(what) + " was accepted");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
FillTexture(SingleLevel, 1);
|
||||
FillTexture(MultiLevel, StorageLevels);
|
||||
const auto coordinates = Coordinates();
|
||||
const auto single = TextureDescriptor(SingleLevel.data(), 0u, 0u);
|
||||
const auto multi = TextureDescriptor(MultiLevel.data(), 0u, StorageLevels - 1u);
|
||||
for (const auto& sampler : Samplers) {
|
||||
const auto singleSamples = Run(*device, single, sampler.words, coordinates);
|
||||
Check(sampler, "1-level image", coordinates, singleSamples);
|
||||
const auto multiSamples = Run(*device, multi, sampler.words, coordinates);
|
||||
Check(sampler, "first level of a 4-level image", coordinates, multiSamples);
|
||||
Require(multiSamples == singleSamples, std::string(sampler.name) + ": the first level of a 4-level image does not sample like a 1-level image");
|
||||
}
|
||||
ExpectFailure(*device, TextureDescriptor(MultiLevel.data(), 2u, 2u), Samplers[0].words, "single-level", "a 3-level view");
|
||||
ExpectFailure(*device, single, {0x00008092u, 0x00fff000u, 0x05100000u, 0u}, "different minification", "unequal minification and magnification filters");
|
||||
ExpectFailure(*device, single, {0x00008090u, 0x00fff000u, 0x05500000u, 0u}, "clamp mode 0", "wrap on X");
|
||||
std::puts("image sample unnormalized tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Width = 128;
|
||||
constexpr std::uint32_t Height = 128;
|
||||
constexpr std::uint32_t Texels = Width * Height;
|
||||
constexpr std::uint32_t Sint8 = 6;
|
||||
constexpr std::uint32_t Unorm8 = 1;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint32_t SwizzleX111 = 0x24cu;
|
||||
constexpr std::uint32_t IdentitySwizzle = 0xfacu;
|
||||
constexpr std::array<std::uint32_t, 4> PointSampler{0x00000092u, 0x00fff000u, 0u, 0u};
|
||||
constexpr std::array<std::uint32_t, 4> LinearSampler{0x00000092u, 0x00fff000u, 0x00500000u, 0u};
|
||||
|
||||
alignas(256) std::array<std::uint32_t, Texels * 4> Output{};
|
||||
alignas(256) std::array<std::uint8_t, 256 * Height> Texture{};
|
||||
|
||||
constexpr std::array<std::uint32_t, 17> TexelCode(std::uint32_t word0) {
|
||||
return {
|
||||
0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x2c3e3c87u, 0x363c3cffu, 0x0000007fu, 0x7e140280u,
|
||||
0x7e160280u, 0x7e180280u, 0x7e1a0280u, word0, 0x00010a1eu, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u,
|
||||
0xbf810000u,
|
||||
};
|
||||
}
|
||||
|
||||
constexpr std::array<std::uint32_t, 11> SamplerCode(std::uint32_t word0, std::uint32_t u, std::uint32_t v) {
|
||||
return {
|
||||
0x34060084u, 0x7e2802ffu, u, 0x7e2a02ffu, v, word0, 0x00610a14u, 0xbf8c3f70u,
|
||||
0xe0781000u, 0x80000a03u, 0xbf810000u,
|
||||
};
|
||||
}
|
||||
|
||||
alignas(256) constexpr auto LoadX = TexelCode(0xf0001108u);
|
||||
alignas(256) constexpr auto LoadXyzw = TexelCode(0xf0001f08u);
|
||||
alignas(256) constexpr auto StoreX = TexelCode(0xf0201108u);
|
||||
alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u, 0x3ce00000u, 0x3d300000u);
|
||||
alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u, 0x3c000000u, 0x3c000000u);
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::uint32_t PitchBytes() {
|
||||
const auto mips = AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, Unorm8, Width, Height, 1u);
|
||||
Require(AgcDriver::Graphics::ComputeSurfaceSize(mips, 1) <= Texture.size(), "image 8_SINT: the texture does not fit its storage");
|
||||
return mips[0].pitchBytes;
|
||||
}
|
||||
|
||||
std::uint8_t& Texel(std::uint32_t x, std::uint32_t y) {
|
||||
static const auto pitch = PitchBytes();
|
||||
return Texture[static_cast<std::size_t>(y) * pitch + x];
|
||||
}
|
||||
|
||||
std::uint32_t Signed(std::uint32_t x, std::uint32_t y) {
|
||||
return static_cast<std::uint32_t>(static_cast<std::int32_t>(static_cast<std::int8_t>(Texel(x, y))));
|
||||
}
|
||||
|
||||
void Fill() {
|
||||
Texture.fill(0x5au);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
Texel(x, y) = static_cast<std::uint8_t>(0x80u + 37u * x + 11u * y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t records, std::uint32_t stride = 0u) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu) | (stride << 16u), records, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const void* texels, std::uint32_t format, std::uint32_t width, std::uint32_t height, std::uint32_t swizzle) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((width - 1u) & 3u) << 30u),
|
||||
((width - 1u) >> 2u) | ((height - 1u) << 14u),
|
||||
swizzle | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint32_t format, const std::array<std::uint32_t, 4>& sampler, std::uint32_t groups) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(24, 0u);
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
const auto texture = TextureDescriptor(Texture.data(), format, Width, Height, SwizzleX111);
|
||||
std::copy(output.begin(), output.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
std::copy(sampler.begin(), sampler.end(), userData.begin() + 12);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Expect(std::uint32_t slot, const std::array<std::uint32_t, 4>& expected, const std::string& what) {
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
const auto actual = Output[slot * 4u + component];
|
||||
Require(actual == expected[component], what + ": result " + std::to_string(slot) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected[component]));
|
||||
}
|
||||
}
|
||||
|
||||
void CheckLoads(AgcDriver::VulkanDevice& device) {
|
||||
Fill();
|
||||
Require(Texel(0, 0) == 0x80u && Signed(0, 0) == 0xffffff80u, "image 8_SINT: texel (0, 0) must hold 0x80");
|
||||
Run(device, LoadX, Sint8, PointSampler, Texels / Threads);
|
||||
for (std::uint32_t texel = 0; texel < Texels; ++texel) {
|
||||
Expect(texel, {Signed(texel % Width, texel / Width), 0u, 0u, 0u}, "8_SINT image_load dmask:0x1");
|
||||
}
|
||||
Run(device, LoadXyzw, Sint8, PointSampler, Texels / Threads);
|
||||
for (std::uint32_t texel = 0; texel < Texels; ++texel) {
|
||||
Expect(texel, {Signed(texel % Width, texel / Width), 1u, 1u, 1u}, "8_SINT image_load dmask:0xf X 1 1 1");
|
||||
}
|
||||
}
|
||||
|
||||
void CheckSamples(AgcDriver::VulkanDevice& device) {
|
||||
Fill();
|
||||
Run(device, SampleLz, Sint8, PointSampler, 1);
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
Expect(lane, {Signed(3, 5), 1u, 1u, 1u}, "8_SINT image_sample_lz with point filtering");
|
||||
}
|
||||
Run(device, Gather4Lz, Sint8, LinearSampler, 1);
|
||||
for (std::uint32_t lane = 0; lane < Threads; ++lane) {
|
||||
Expect(lane, {Signed(0, 1), Signed(1, 1), Signed(1, 0), Signed(0, 0)}, "8_SINT image_gather4_lz through a bilinear sampler");
|
||||
}
|
||||
}
|
||||
|
||||
void RequireRefused(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint32_t format, const std::array<std::uint32_t, 4>& sampler, const std::string& reason, const std::string& what) {
|
||||
std::string refusal;
|
||||
try {
|
||||
Run(device, code, format, sampler, 1);
|
||||
} catch (const std::exception& error) {
|
||||
refusal = error.what();
|
||||
}
|
||||
Require(refusal.find(reason) != std::string::npos, what + " was not refused: " + refusal);
|
||||
}
|
||||
|
||||
struct alignas(4096) GuestTables {
|
||||
std::array<std::array<std::uint32_t, 8>, 2> heap{};
|
||||
std::array<std::array<std::uint32_t, 4>, 3> materials{};
|
||||
std::array<std::uint32_t, 4> output{};
|
||||
std::array<std::uint32_t, 16> srt{};
|
||||
};
|
||||
|
||||
struct alignas(256) TableTexture {
|
||||
std::array<std::uint8_t, 4096> bytes{};
|
||||
};
|
||||
|
||||
GuestTables Tables;
|
||||
TableTexture TableTextures[2];
|
||||
|
||||
alignas(256) const std::array<std::uint32_t, 20> TableCode{
|
||||
0xf4080100u, 0xfa000000u, 0xf4080200u, 0xfa000010u, 0xf4080300u, 0xfa000020u, 0xf4080700u, 0xfa000030u,
|
||||
0x7e200500u, 0x93109010u, 0xf4200406u, 0x20000004u, 0x8f108510u, 0xf42c0502u, 0x20000000u, 0xf09c0f08u,
|
||||
0x00450000u, 0xe0700000u, 0x80070000u, 0xbf810000u,
|
||||
};
|
||||
|
||||
void CheckTable(AgcDriver::VulkanDevice& device) {
|
||||
TableTextures[0].bytes.fill(0x80u);
|
||||
TableTextures[1].bytes.fill(0xffu);
|
||||
for (std::uint32_t entry = 0; entry < 2u; ++entry) Tables.heap[entry] = TextureDescriptor(TableTextures[entry].bytes.data(), Sint8, 4u, 4u, IdentitySwizzle);
|
||||
const auto heap = BufferDescriptor(Tables.heap.data(), 2u, 32u);
|
||||
const auto materials = BufferDescriptor(Tables.materials.data(), 3u, 16u);
|
||||
const auto output = BufferDescriptor(Tables.output.data(), 16u);
|
||||
std::copy(heap.begin(), heap.end(), Tables.srt.begin());
|
||||
std::copy(materials.begin(), materials.end(), Tables.srt.begin() + 8);
|
||||
std::copy(output.begin(), output.end(), Tables.srt.begin() + 12);
|
||||
const auto srtAddress = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Tables.srt.data()));
|
||||
const std::vector<std::uint32_t> userData{static_cast<std::uint32_t>(srtAddress), static_cast<std::uint32_t>(srtAddress >> 32u)};
|
||||
const std::span<const std::uint32_t> code(TableCode);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 2> memory{{
|
||||
{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)},
|
||||
{reinterpret_cast<std::uintptr_t>(&Tables), std::as_bytes(std::span<const GuestTables, 1>(&Tables, 1))},
|
||||
}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{8, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
for (const std::uint32_t key : {0u, 1u}) {
|
||||
Tables.materials = {{{0u, key, 0u, 0u}, {0u, 0u, 0u, 0u}, {0u, 1u, 0u, 0u}}};
|
||||
Tables.output.fill(0xdeadbeefu);
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
const auto expected = key == 0u ? 0xffffff80u : 0xffffffffu;
|
||||
Require(Tables.output[0] == expected, "8_SINT image table entry " + std::to_string(key) + ": image_sample_lz read " + Hex(Tables.output[0]) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
CheckLoads(*device);
|
||||
CheckSamples(*device);
|
||||
CheckTable(*device);
|
||||
RequireRefused(*device, StoreX, Sint8, PointSampler, "storage image descriptor uses an unsupported format", "8_SINT image_store");
|
||||
std::puts("image 8_SINT tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Width = 256;
|
||||
constexpr std::uint32_t Groups = Width / Threads;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint32_t Unorm8 = 1;
|
||||
constexpr std::uint32_t Unorm8_8 = 14;
|
||||
constexpr std::uint32_t Srgb8 = 128;
|
||||
constexpr std::uint32_t Srgb8_8 = 129;
|
||||
constexpr std::uint32_t SwizzleXY01 = 0x22cu;
|
||||
constexpr std::uint32_t SwizzleYX10 = 0x065u;
|
||||
constexpr std::uint32_t SwizzleXXX1 = 0x324u;
|
||||
constexpr std::uint32_t One = 0x3f800000u;
|
||||
constexpr std::array<std::uint32_t, 4> PointSampler{0x00000092u, 0x00fff000u, 0u, 0u};
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, Width * 4> Output{};
|
||||
alignas(256) std::array<std::uint8_t, Width * 2> Texels{};
|
||||
|
||||
constexpr std::array<std::uint32_t, 15> LoadCode(std::uint32_t dmask) {
|
||||
return {
|
||||
0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x34063c84u, 0x7e3e0280u, 0x7e140280u, 0x7e160280u, 0x7e180280u,
|
||||
0x7e1a0280u, 0xf0001008u | (dmask << 8u), 0x00010a1eu, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, 0xbf810000u,
|
||||
};
|
||||
}
|
||||
|
||||
constexpr std::array<std::uint32_t, 15> StoreCode(std::uint32_t dmask) {
|
||||
return {
|
||||
0x7e080218u, 0x343c0885u, 0x4a3c3d00u, 0x7e3e0280u, 0x7e140d1eu, 0x4c163cffu, 0x000000ffu, 0x7e160d0bu,
|
||||
0x101414ffu, 0x3b808081u, 0x101616ffu, 0x3b808081u, 0xf0201008u | (dmask << 8u), 0x00010a1eu, 0xbf810000u,
|
||||
};
|
||||
}
|
||||
|
||||
constexpr std::array<std::uint32_t, 9> SamplerCode(std::uint32_t word0) {
|
||||
return {
|
||||
0x34060084u, 0x7e280280u, 0x7e2a0280u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u,
|
||||
0xbf810000u,
|
||||
};
|
||||
}
|
||||
|
||||
alignas(256) constexpr auto LoadXyzw = LoadCode(0xfu);
|
||||
alignas(256) constexpr auto LoadXy = LoadCode(0x3u);
|
||||
alignas(256) constexpr auto StoreX = StoreCode(0x1u);
|
||||
alignas(256) constexpr auto StoreXy = StoreCode(0x3u);
|
||||
alignas(256) constexpr auto SampleLz = SamplerCode(0xf09c0f08u);
|
||||
alignas(256) constexpr auto Gather4Lz = SamplerCode(0xf11c0108u);
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "image sRGB load: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint8_t* Data() { return block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::uint32_t Linear(std::uint32_t code) {
|
||||
const double encoded = code / 255.0;
|
||||
const double linear = encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4);
|
||||
int exponent = 0;
|
||||
const double mantissa = std::frexp(linear, &exponent);
|
||||
return std::bit_cast<std::uint32_t>(static_cast<float>(std::ldexp(std::nearbyint(mantissa * 256.0), exponent - 8)));
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const void* texels, std::uint32_t format, std::uint32_t swizzle) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(texels));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u),
|
||||
(Width - 1u) >> 2u,
|
||||
swizzle | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
void Fill(std::uint32_t format) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
if (format == Srgb8) {
|
||||
Texels[x] = static_cast<std::uint8_t>(x);
|
||||
} else {
|
||||
Texels[x * 2u] = static_cast<std::uint8_t>(x);
|
||||
Texels[x * 2u + 1u] = static_cast<std::uint8_t>(255u - x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const void* texels, std::uint32_t format, std::uint32_t swizzle, std::uint32_t groups) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
std::vector<std::uint32_t> userData(24, 0u);
|
||||
const auto output = BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u));
|
||||
const auto texture = TextureDescriptor(texels, format, swizzle);
|
||||
std::copy(output.begin(), output.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
std::copy(PointSampler.begin(), PointSampler.end(), userData.begin() + 12);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {true, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, groups, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Verify(std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const std::array<std::uint32_t, 4> channels{Linear(x), format == Srgb8 ? 0u : Linear(255u - x), 0u, One};
|
||||
std::uint32_t slot = 0;
|
||||
for (std::uint32_t component = 0; component < 4u; ++component) {
|
||||
if (((dmask >> component) & 1u) == 0u) continue;
|
||||
const auto selector = (swizzle >> (component * 3u)) & 7u;
|
||||
const auto expected = selector == 0u ? 0u : selector == 1u ? One : channels[selector - 4u];
|
||||
const auto actual = Output[x * 4u + slot++];
|
||||
Require(actual == expected, what + ": texel " + std::to_string(x) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckLoad(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) {
|
||||
Fill(format);
|
||||
Run(device, code, Texels.data(), format, swizzle, Groups);
|
||||
Verify(dmask, format, swizzle, what);
|
||||
}
|
||||
|
||||
void CheckStoredLoad(AgcDriver::VulkanDevice& device, GuestBlock& block, std::span<const std::uint32_t> code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(block.Data());
|
||||
const auto bytes = Width * (format == Srgb8 ? 1u : 2u);
|
||||
std::fill(block.Data(), block.Data() + BlockBytes, std::uint8_t{0x5au});
|
||||
Run(device, format == Srgb8 ? std::span<const std::uint32_t>(StoreX) : std::span<const std::uint32_t>(StoreXy), block.Data(), format == Srgb8 ? Unorm8 : Unorm8_8, SwizzleXY01, Groups);
|
||||
Require(AgcDriver::Graphics::StorageTexture::FindPending(address, bytes) != nullptr, what + ": the image_store results are not pending in the storage image");
|
||||
Run(device, code, block.Data(), format, swizzle, Groups);
|
||||
Verify(dmask, format, swizzle, what);
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(address, BlockBytes, nullptr, "test");
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void RequireRefused(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, const std::string& what) {
|
||||
Fill(Srgb8_8);
|
||||
std::string refusal;
|
||||
try {
|
||||
Run(device, code, Texels.data(), Srgb8_8, SwizzleXY01, 1);
|
||||
} catch (const std::exception& error) {
|
||||
refusal = error.what();
|
||||
}
|
||||
Require(refusal.find("samples or gathers an sRGB image the device cannot sample") != std::string::npos, what + " of an 8_8_SRGB image read through its UNORM view was not refused: " + refusal);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
#ifdef _WIN32
|
||||
_putenv_s("APS5_SRGB_SHADER_DECODE", "1");
|
||||
#else
|
||||
setenv("APS5_SRGB_SHADER_DECODE", "1", 1);
|
||||
#endif
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
CheckLoad(*device, LoadXyzw, 0xfu, Srgb8_8, SwizzleXY01, "8_8_SRGB image_load dmask:0xf X Y 0 1");
|
||||
CheckLoad(*device, LoadXyzw, 0xfu, Srgb8_8, SwizzleYX10, "8_8_SRGB image_load dmask:0xf Y X 1 0");
|
||||
CheckLoad(*device, LoadXy, 0x3u, Srgb8_8, SwizzleXY01, "8_8_SRGB image_load dmask:0x3 X Y 0 1");
|
||||
CheckLoad(*device, LoadXyzw, 0xfu, Srgb8, SwizzleXXX1, "8_SRGB image_load dmask:0xf X X X 1");
|
||||
GuestBlock block;
|
||||
CheckStoredLoad(*device, block, LoadXy, 0x3u, Srgb8_8, SwizzleXY01, "8_8_SRGB image_load dmask:0x3 X Y 0 1 of 8_8_UNORM image_stores");
|
||||
CheckStoredLoad(*device, block, LoadXyzw, 0xfu, Srgb8_8, SwizzleYX10, "8_8_SRGB image_load dmask:0xf Y X 1 0 of 8_8_UNORM image_stores");
|
||||
CheckStoredLoad(*device, block, LoadXyzw, 0xfu, Srgb8, SwizzleXXX1, "8_SRGB image_load dmask:0xf X X X 1 of 8_UNORM image_stores");
|
||||
RequireRefused(*device, SampleLz, "image_sample_lz");
|
||||
RequireRefused(*device, Gather4Lz, "image_gather4_lz");
|
||||
std::puts("image sRGB load tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,316 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Texture.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp"
|
||||
#include "prx/libc/include/GuestAllocations.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#ifdef _WIN32
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t Width = 32;
|
||||
constexpr std::uint32_t Height = 8;
|
||||
constexpr std::uint32_t Type2D = 9;
|
||||
constexpr std::uint32_t IdentitySwizzle = 0xfacu;
|
||||
constexpr std::uint32_t TwoChannelSwizzle = 0x22cu;
|
||||
constexpr std::uint32_t TitleSwizzle = 0x24cu;
|
||||
constexpr std::size_t TexelBytes = 8192;
|
||||
constexpr std::size_t BlockBytes = 65536;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Input{};
|
||||
alignas(256) std::array<std::uint32_t, Threads * 4> Output{};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 12> StoreCode{
|
||||
0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e,
|
||||
0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 9> TitleCode{
|
||||
0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0202108, 0x0001141e,
|
||||
0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 9> LoadCode{
|
||||
0x34020084, 0x7e3c0300, 0x7e3e0280, 0xf0001f08, 0x0001141e, 0xbf8c3f70, 0xe0781000, 0x80001401,
|
||||
0xbf810000,
|
||||
};
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 5> D16Code{0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x8001141e, 0xbf810000};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 5> PackedCode{0x7e3c0300, 0x7e3e0280, 0xf0281108, 0x0001141e, 0xbf810000};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 7> AtomicD16Code{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xf0201108, 0x8001141e, 0xbf810000};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 9> PackedWideCode{0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0281308, 0x0001141e, 0xbf810000};
|
||||
alignas(256) constexpr std::array<std::uint32_t, 5> AtomicCode{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xbf810000};
|
||||
|
||||
constexpr std::array<std::uint32_t, 24> Edges{
|
||||
0x00000000u, 0x00000001u, 0xffffffffu, 0x0000007fu, 0x00000080u, 0xffffff80u, 0xffffff7fu, 0x000000ffu,
|
||||
0x00000100u, 0x00007fffu, 0x00008000u, 0xffff8000u, 0xffff7fffu, 0x0000ffffu, 0x00010000u, 0xffff0000u,
|
||||
0x7fffffffu, 0x80000000u, 0x12345678u, 0xedcba988u, 0x00007ffeu, 0xffff8001u, 0x0000007eu, 0xffffff81u,
|
||||
};
|
||||
|
||||
class GuestBlock {
|
||||
public:
|
||||
GuestBlock() {
|
||||
#ifdef _WIN32
|
||||
block = static_cast<std::uint8_t*>(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
|
||||
#else
|
||||
block = static_cast<std::uint8_t*>(std::aligned_alloc(BlockBytes, BlockBytes));
|
||||
#endif
|
||||
Require(block != nullptr, "image store sint: cannot allocate the guest block");
|
||||
GuestAllocations::Mutation().Add(block, BlockBytes, true, true);
|
||||
}
|
||||
|
||||
~GuestBlock() {
|
||||
GuestAllocations::Mutation().Remove(block);
|
||||
#ifdef _WIN32
|
||||
VirtualFree(block, 0, MEM_RELEASE);
|
||||
#else
|
||||
std::free(block);
|
||||
#endif
|
||||
}
|
||||
|
||||
GuestBlock(const GuestBlock&) = delete;
|
||||
GuestBlock& operator=(const GuestBlock&) = delete;
|
||||
|
||||
std::uint8_t* Data() { return block; }
|
||||
|
||||
private:
|
||||
std::uint8_t* block = nullptr;
|
||||
};
|
||||
|
||||
struct Format {
|
||||
const char* name;
|
||||
std::uint32_t format;
|
||||
std::uint32_t components;
|
||||
std::uint32_t bits;
|
||||
};
|
||||
|
||||
constexpr std::array<Format, 8> Formats{{
|
||||
{"16_SINT", 12u, 1u, 16u},
|
||||
{"8_8_SINT", 19u, 2u, 8u},
|
||||
{"32_SINT", 21u, 1u, 32u},
|
||||
{"16_16_SINT", 28u, 2u, 16u},
|
||||
{"8_8_8_8_SINT", 61u, 4u, 8u},
|
||||
{"32_32_SINT", 63u, 2u, 32u},
|
||||
{"16_16_16_16_SINT", 70u, 4u, 16u},
|
||||
{"32_32_32_32_SINT", 76u, 4u, 32u},
|
||||
}};
|
||||
|
||||
struct Write {
|
||||
std::uint32_t row;
|
||||
std::uint32_t dmask;
|
||||
};
|
||||
|
||||
constexpr std::array<Write, 2> StoreWrites{{{0u, 0xfu}, {1u, 0x5u}}};
|
||||
constexpr std::array<Write, 1> TitleWrites{{{0u, 0x1u}}};
|
||||
constexpr std::array<Write, 1> PackedWideWrites{{{0u, 0x3u}}};
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::uint32_t Value(std::uint32_t tid, std::uint32_t index) {
|
||||
if (tid < Edges.size()) return Edges[(tid + 7u * index) % Edges.size()];
|
||||
const auto mixed = (tid * 4u + index + 1u) * 0x9e3779b1u;
|
||||
return static_cast<std::uint32_t>(static_cast<std::int32_t>(mixed) >> ((tid * 4u + index) % 25u));
|
||||
}
|
||||
|
||||
std::uint32_t Saturate(std::uint32_t value, std::uint32_t bits) {
|
||||
if (bits == 32u) return value;
|
||||
const auto high = (std::int64_t{1} << (bits - 1u)) - 1;
|
||||
const auto clamped = std::clamp<std::int64_t>(static_cast<std::int32_t>(value), -high - 1, high);
|
||||
return static_cast<std::uint32_t>(clamped) & ((1u << bits) - 1u);
|
||||
}
|
||||
|
||||
std::uint32_t SignExtend(std::uint32_t value, std::uint32_t bits) {
|
||||
if (bits == 32u) return value;
|
||||
const auto shift = 32u - bits;
|
||||
return static_cast<std::uint32_t>(static_cast<std::int32_t>(value << shift) >> shift);
|
||||
}
|
||||
|
||||
std::uint32_t SwizzleOf(const Format& format) {
|
||||
return format.components == 2u ? TwoChannelSwizzle : IdentitySwizzle;
|
||||
}
|
||||
|
||||
std::uint32_t TexelBytesOf(const Format& format) {
|
||||
return format.components * format.bits / 8u;
|
||||
}
|
||||
|
||||
AgcDriver::Graphics::TileMipLayout Mip(const Format& format) {
|
||||
return AgcDriver::Graphics::ComputeMipLayout(AgcDriver::Graphics::TextureTileMode::kLinear, format.format, Width, Height, 1u).at(0);
|
||||
}
|
||||
|
||||
std::uint64_t TexelOffset(const Format& format, std::uint32_t x, std::uint32_t y) {
|
||||
const auto mip = Mip(format);
|
||||
return mip.tiledOffset + static_cast<std::uint64_t>(y) * mip.pitchBytes + static_cast<std::uint64_t>(x) * TexelBytesOf(format);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Initial() {
|
||||
std::vector<std::uint8_t> bytes(TexelBytes);
|
||||
for (std::size_t index = 0; index < bytes.size(); ++index) {
|
||||
bytes[index] = static_cast<std::uint8_t>(0x80u + 37u * index + 11u * (index >> 4u));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::uint32_t StoredComponent(const Format& format, std::uint32_t tid, std::uint32_t dmask, std::uint32_t component) {
|
||||
if (((dmask >> component) & 1u) == 0u) return 0u;
|
||||
std::uint32_t index = 0;
|
||||
for (std::uint32_t lower = 0; lower < component; ++lower) index += (dmask >> lower) & 1u;
|
||||
return Saturate(Value(tid, index), format.bits);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> Expected(const Format& format, std::span<const Write> writes) {
|
||||
auto bytes = Initial();
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
for (const auto& write : writes) {
|
||||
auto* texel = bytes.data() + TexelOffset(format, tid, write.row);
|
||||
for (std::uint32_t component = 0; component < format.components; ++component) {
|
||||
const auto value = StoredComponent(format, tid, write.dmask, component);
|
||||
std::memcpy(texel + component * format.bits / 8u, &value, format.bits / 8u);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x01016facu};
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 8> TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle) {
|
||||
const auto address = static_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(data));
|
||||
return {
|
||||
static_cast<std::uint32_t>(address >> 8u),
|
||||
static_cast<std::uint32_t>((address >> 40u) & 0xffu) | (format << 20u) | (((Width - 1u) & 3u) << 30u),
|
||||
((Width - 1u) >> 2u) | ((Height - 1u) << 14u),
|
||||
swizzle | (Type2D << 28u),
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
0u,
|
||||
};
|
||||
}
|
||||
|
||||
void Dispatch(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span<const std::uint32_t> code, std::uint32_t format, std::uint32_t swizzle, const std::array<std::uint32_t, 4>& buffer) {
|
||||
std::vector<std::uint32_t> userData(16, 0u);
|
||||
const auto texture = TextureDescriptor(texels, format, swizzle);
|
||||
std::copy(buffer.begin(), buffer.end(), userData.begin());
|
||||
std::copy(texture.begin(), texture.end(), userData.begin() + 4);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast<std::uintptr_t>(texels), TexelBytes, nullptr, "test");
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span<const std::uint32_t> code, std::uint32_t format, std::uint32_t swizzle) {
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
for (std::uint32_t index = 0; index < 4u; ++index) Input[tid * 4u + index] = Value(tid, index);
|
||||
}
|
||||
const auto initial = Initial();
|
||||
std::copy(initial.begin(), initial.end(), texels);
|
||||
Dispatch(device, texels, code, format, swizzle, BufferDescriptor(Input.data(), static_cast<std::uint32_t>(Input.size() * 4u)));
|
||||
}
|
||||
|
||||
void CheckTexels(const std::uint8_t* texels, const Format& format, std::span<const Write> writes, const std::string& what) {
|
||||
const auto expected = Expected(format, writes);
|
||||
for (std::uint32_t y = 0; y < Height; ++y) {
|
||||
for (std::uint32_t x = 0; x < Width; ++x) {
|
||||
const auto offset = TexelOffset(format, x, y);
|
||||
for (std::uint32_t component = 0; component < format.components; ++component) {
|
||||
std::uint32_t actual = 0;
|
||||
std::uint32_t wanted = 0;
|
||||
std::memcpy(&actual, texels + offset + component * format.bits / 8u, format.bits / 8u);
|
||||
std::memcpy(&wanted, expected.data() + offset + component * format.bits / 8u, format.bits / 8u);
|
||||
Require(actual == wanted, what + " " + format.name + ": texel (" + std::to_string(x) + ", " + std::to_string(y) + ") component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CheckLoad(AgcDriver::VulkanDevice& device, std::uint8_t* texels, const Format& format) {
|
||||
Output.fill(0xdeadbeefu);
|
||||
Dispatch(device, texels, LoadCode, format.format, SwizzleOf(format), BufferDescriptor(Output.data(), static_cast<std::uint32_t>(Output.size() * 4u)));
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
for (std::uint32_t component = 0; component < format.components; ++component) {
|
||||
const auto actual = Output[tid * 4u + component];
|
||||
const auto wanted = SignExtend(StoredComponent(format, tid, 0xfu, component), format.bits);
|
||||
Require(actual == wanted, std::string("image_load after image_store of ") + format.name + ": thread " + std::to_string(tid) + " component " + std::to_string(component) + " is " + Hex(actual) + ", expected " + Hex(wanted));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RequireRefused(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span<const std::uint32_t> code, std::uint32_t format, const std::string& reason, const std::string& what) {
|
||||
std::string refusal;
|
||||
try {
|
||||
Store(device, texels, code, format, IdentitySwizzle);
|
||||
} catch (const std::exception& error) {
|
||||
refusal = error.what();
|
||||
}
|
||||
Require(refusal.find(reason) != std::string::npos, what + " was not refused: " + refusal);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
GuestBlock block;
|
||||
auto* texels = block.Data();
|
||||
for (const auto& format : Formats) {
|
||||
Store(*device, texels, StoreCode, format.format, SwizzleOf(format));
|
||||
CheckTexels(texels, format, StoreWrites, "image_store");
|
||||
CheckLoad(*device, texels, format);
|
||||
}
|
||||
Store(*device, texels, TitleCode, Formats[0].format, TitleSwizzle);
|
||||
CheckTexels(texels, Formats[0], TitleWrites, "image_store dmask:0x1 glc with DST_SEL X,1,1,1");
|
||||
RequireRefused(*device, texels, D16Code, 12u, "stores 16-bit data to an image of a SINT format", "image_store d16 of 16_SINT");
|
||||
RequireRefused(*device, texels, D16Code, 21u, "stores 16-bit data to an image of a SINT format", "image_store d16 of 32_SINT");
|
||||
RequireRefused(*device, texels, PackedCode, 12u, "storage image descriptor uses an unsupported format", "image_store_pck of 16_SINT");
|
||||
RequireRefused(*device, texels, AtomicCode, 12u, "atomic image descriptor uses an unsupported format 12", "image_atomic_add of 16_SINT");
|
||||
RequireRefused(*device, texels, StoreCode, 73u, "storage image descriptor uses an unsupported format", "image_store of 32_32_32_SINT");
|
||||
for (const auto index : {2u, 5u, 7u}) {
|
||||
Store(*device, texels, PackedWideCode, Formats[index].format, SwizzleOf(Formats[index]));
|
||||
CheckTexels(texels, Formats[index], PackedWideWrites, "image_store_pck dmask:0x3");
|
||||
}
|
||||
RequireRefused(*device, texels, AtomicD16Code, 21u, "stores 16-bit data to an image of a SINT format", "image_store d16 of an atomically updated 32_SINT image");
|
||||
std::puts("image store sint tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <array>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, 43> ScanCode{
|
||||
0xbefe0481, 0x7e020281, 0x7e000280, 0xe0c85000, 0x80000100, 0xbf8c3f70, 0x7e200501, 0x34040282,
|
||||
0xe0301000, 0x80010302, 0x34080283, 0xbf8c3f70, 0xbf068010, 0x85118182, 0x7e0c0211, 0x7e0e0303,
|
||||
0xe1401000, 0x80020604, 0x7e100280, 0xbf068010, 0xbf85000f, 0x80928110, 0x8f138312, 0x7e120213,
|
||||
0xe0345000, 0x80020a09, 0xbf8c3f70, 0x7e28050a, 0x7e2a050b, 0xbf068014, 0xbf85fff7, 0x4a101015,
|
||||
0xbf068214, 0xbf850002, 0x80928112, 0xbf82fff2, 0x4a1a0708, 0x7e180282, 0xe1401000, 0x80020c04,
|
||||
0xe0701000, 0x80030802, 0xbf810000,
|
||||
};
|
||||
|
||||
constexpr std::uint32_t Lanes = 64;
|
||||
constexpr std::uint32_t Blocks = 4096;
|
||||
|
||||
alignas(256) std::array<std::uint32_t, 4> Counter{};
|
||||
alignas(256) std::array<std::uint32_t, Blocks> Aggregates{};
|
||||
alignas(256) std::array<std::uint32_t, Blocks * 2> Pairs{};
|
||||
alignas(256) std::array<std::uint32_t, Blocks> Prefixes{};
|
||||
|
||||
std::uint32_t Value(std::uint32_t index, std::uint32_t seed) {
|
||||
return ((index * 2654435761u + seed * 40503u) >> 13u) % 1000u;
|
||||
}
|
||||
|
||||
std::array<std::uint32_t, 4> BufferDescriptor(const void* data, std::uint32_t bytes) {
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(data);
|
||||
return {static_cast<std::uint32_t>(address), static_cast<std::uint32_t>((address >> 32u) & 0xffffu), bytes, 0x31016facu};
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) {
|
||||
alignas(256) static std::array<std::uint32_t, ScanCode.size()> code{};
|
||||
code = ScanCode;
|
||||
std::vector<std::uint32_t> userData;
|
||||
for (const auto& descriptor : {BufferDescriptor(Counter.data(), sizeof(Counter)), BufferDescriptor(Aggregates.data(), sizeof(Aggregates)), BufferDescriptor(Pairs.data(), sizeof(Pairs)), BufferDescriptor(Prefixes.data(), sizeof(Prefixes))}) {
|
||||
userData.insert(userData.end(), descriptor.begin(), descriptor.end());
|
||||
}
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(code.data());
|
||||
const std::span<const std::uint32_t> words(code);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 1> memory{{{address, std::as_bytes(words)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Lanes, 1, 1}, 0, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, address, words, 0, {}},
|
||||
{Lanes, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
device.Dispatch(result, groups, 1, 1, {}, address);
|
||||
device.WaitIdle();
|
||||
}
|
||||
|
||||
void CheckLookBack(AgcDriver::VulkanDevice& device, std::uint32_t block) {
|
||||
const auto name = "look-back from block " + std::to_string(block);
|
||||
Pairs.fill(0u);
|
||||
std::uint32_t prefix = 0;
|
||||
for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, block);
|
||||
for (std::uint32_t b = 0; b < block; ++b) {
|
||||
prefix += Aggregates[b];
|
||||
Pairs[2 * b] = b == 0 ? 2u : 1u;
|
||||
Pairs[2 * b + 1] = b == 0 ? prefix : Aggregates[b];
|
||||
}
|
||||
Counter.fill(0u);
|
||||
Counter[0] = block;
|
||||
Prefixes.fill(0xdeadbeefu);
|
||||
Run(device, 1);
|
||||
Require(Counter[0] == block + 1, name + ": the counter is " + std::to_string(Counter[0]));
|
||||
Require(Prefixes[block] == prefix, name + ": its prefix is " + std::to_string(Prefixes[block]) + ", expected " + std::to_string(prefix));
|
||||
Require(Pairs[2 * block] == 2u, name + ": its flag is " + std::to_string(Pairs[2 * block]) + ", expected 2");
|
||||
Require(Pairs[2 * block + 1] == prefix + Aggregates[block], name + ": it published " + std::to_string(Pairs[2 * block + 1]) + ", expected " + std::to_string(prefix + Aggregates[block]));
|
||||
}
|
||||
|
||||
void CheckWhole(AgcDriver::VulkanDevice& device, std::uint32_t seed) {
|
||||
const auto name = "whole scan " + std::to_string(seed);
|
||||
for (std::uint32_t b = 0; b < Blocks; ++b) Aggregates[b] = Value(b, seed);
|
||||
Counter.fill(0u);
|
||||
Pairs.fill(0u);
|
||||
Prefixes.fill(0xdeadbeefu);
|
||||
Run(device, Blocks);
|
||||
std::uint32_t running = 0;
|
||||
for (std::uint32_t b = 0; b < Blocks; ++b) {
|
||||
Require(Prefixes[b] == running, name + ": block " + std::to_string(b) + "'s prefix is " + std::to_string(Prefixes[b]) + ", expected " + std::to_string(running));
|
||||
running += Aggregates[b];
|
||||
Require(Pairs[2 * b] == 2u && Pairs[2 * b + 1] == running, name + ": block " + std::to_string(b) + " published {" + std::to_string(Pairs[2 * b]) + ", " + std::to_string(Pairs[2 * b + 1]) + "}, expected {2, " + std::to_string(running) + "}");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
if (device->Target().subgroupSize < 32) {
|
||||
std::printf("skipped, subgroup size %u cannot hold a wave32\n", device->Target().subgroupSize);
|
||||
return VulkanTestSkipped;
|
||||
}
|
||||
for (const std::uint32_t block : {0u, 1u, 2u, 63u, 64u, 1000u, Blocks - 1}) CheckLookBack(*device, block);
|
||||
for (std::uint32_t seed = 0; seed < 50; ++seed) CheckWhole(*device, seed);
|
||||
std::puts("look-back scan tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp"
|
||||
#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp"
|
||||
#include "Recompiler.hpp"
|
||||
#include "VulkanTestDevice.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <iostream>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
using AgcDriver::Graphics::Require;
|
||||
using ShaderRecompiler::DescriptorRole;
|
||||
using ShaderRecompiler::ShaderStage;
|
||||
|
||||
constexpr std::uint32_t Threads = 32;
|
||||
constexpr std::uint32_t PushBuffers = 52;
|
||||
constexpr std::uint32_t DataBuffers = 124;
|
||||
constexpr std::uint32_t MaxBuffers = DataBuffers;
|
||||
constexpr std::uint32_t RegionWords = 2u * Threads + 4u;
|
||||
constexpr std::uint32_t RangeBytes = (RegionWords + 4u) * 4u;
|
||||
constexpr std::uint32_t TableRegister = 0;
|
||||
constexpr std::uint32_t DescriptorRegister = 4;
|
||||
constexpr std::uint32_t NullRegister = 0x7du;
|
||||
constexpr std::uint32_t ThreadVector = 0;
|
||||
constexpr std::uint32_t OffsetVector = 1;
|
||||
constexpr std::uint32_t DataVector = 2;
|
||||
constexpr std::uint32_t ConstantZero = 0x80u;
|
||||
constexpr std::uint32_t ConstantTwo = 0x82u;
|
||||
constexpr std::uint32_t Literal = 0xffu;
|
||||
constexpr std::uint32_t WaitScalarLoads = 0xbf8cc07fu;
|
||||
constexpr std::uint32_t WaitVectorLoads = 0xbf8c3f70u;
|
||||
constexpr std::uint32_t Endpgm = 0xbf810000u;
|
||||
|
||||
alignas(4096) std::array<std::uint32_t, (MaxBuffers + 1u) * RegionWords> Data{};
|
||||
alignas(256) std::array<std::uint32_t, MaxBuffers * 4u> Table{};
|
||||
|
||||
constexpr std::uint32_t Vop2(std::uint32_t opcode, std::uint32_t destination, std::uint32_t source0, std::uint32_t source1) {
|
||||
return (opcode << 25u) | (destination << 17u) | (source1 << 9u) | source0;
|
||||
}
|
||||
|
||||
constexpr std::uint32_t Increment(std::uint32_t buffer) {
|
||||
return (buffer + 1u) * 0x01000193u;
|
||||
}
|
||||
|
||||
constexpr std::size_t CodeWords(std::uint32_t buffers) {
|
||||
return 2u + buffers * 10u;
|
||||
}
|
||||
|
||||
template <std::uint32_t Buffers>
|
||||
constexpr std::array<std::uint32_t, CodeWords(Buffers)> BuildCode() {
|
||||
constexpr std::uint32_t vLshlrevB32 = 0x1au;
|
||||
constexpr std::uint32_t vAddNcU32 = 0x25u;
|
||||
constexpr std::uint32_t sLoadDwordx4 = 0xf4080000u | (DescriptorRegister << 6u) | (TableRegister / 2u);
|
||||
constexpr std::uint32_t bufferLoadDword = 0xe0301000u;
|
||||
constexpr std::uint32_t bufferStoreDword = 0xe0701000u | (Threads * 4u);
|
||||
constexpr std::uint32_t operands = (ConstantZero << 24u) | ((DescriptorRegister / 4u) << 16u) | (DataVector << 8u) | OffsetVector;
|
||||
std::array<std::uint32_t, CodeWords(Buffers)> code{};
|
||||
std::size_t count = 0;
|
||||
code[count++] = Vop2(vLshlrevB32, OffsetVector, ConstantTwo, ThreadVector);
|
||||
for (std::uint32_t buffer = 0; buffer < Buffers; ++buffer) {
|
||||
code[count++] = sLoadDwordx4;
|
||||
code[count++] = (NullRegister << 25u) | (buffer * 16u);
|
||||
code[count++] = WaitScalarLoads;
|
||||
code[count++] = bufferLoadDword;
|
||||
code[count++] = operands;
|
||||
code[count++] = WaitVectorLoads;
|
||||
code[count++] = Vop2(vAddNcU32, DataVector, Literal, DataVector);
|
||||
code[count++] = Increment(buffer);
|
||||
code[count++] = bufferStoreDword;
|
||||
code[count++] = operands;
|
||||
}
|
||||
code[count++] = Endpgm;
|
||||
return code;
|
||||
}
|
||||
|
||||
alignas(256) constexpr std::array<std::uint32_t, CodeWords(PushBuffers)> PushCode = BuildCode<PushBuffers>();
|
||||
alignas(256) constexpr std::array<std::uint32_t, CodeWords(DataBuffers)> DataCode = BuildCode<DataBuffers>();
|
||||
|
||||
std::uint32_t BufferBase(std::uint32_t buffer) {
|
||||
return buffer * RegionWords + buffer % 4u;
|
||||
}
|
||||
|
||||
std::uint32_t InputValue(std::uint32_t buffer, std::uint32_t tid) {
|
||||
return 0x40000000u | (buffer << 8u) | tid;
|
||||
}
|
||||
|
||||
std::string Hex(std::uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof(text), "0x%08x", value);
|
||||
return text;
|
||||
}
|
||||
|
||||
void Run(AgcDriver::VulkanDevice& device, std::span<const std::uint32_t> code, std::uint32_t buffers, bool shaderData) {
|
||||
const std::string label = "many buffers (" + std::to_string(buffers) + "): ";
|
||||
Data.fill(0xdeadbeefu);
|
||||
for (std::uint32_t buffer = 0; buffer < buffers; ++buffer) {
|
||||
const auto base = BufferBase(buffer);
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) Data[base + tid] = InputValue(buffer, tid);
|
||||
const auto address = reinterpret_cast<std::uintptr_t>(&Data[base]);
|
||||
Table[buffer * 4u + 0u] = static_cast<std::uint32_t>(address);
|
||||
Table[buffer * 4u + 1u] = static_cast<std::uint32_t>((address >> 32u) & 0xffffu);
|
||||
Table[buffer * 4u + 2u] = RangeBytes;
|
||||
Table[buffer * 4u + 3u] = 0x01016facu;
|
||||
}
|
||||
const auto table = reinterpret_cast<std::uintptr_t>(Table.data());
|
||||
const std::vector<std::uint32_t> userData{static_cast<std::uint32_t>(table), static_cast<std::uint32_t>(table >> 32u)};
|
||||
const std::span<const std::uint32_t> tableWords(Table.data(), buffers * 4u);
|
||||
const std::array<ShaderRecompiler::MemoryRegion, 2> memory{{{reinterpret_cast<std::uintptr_t>(code.data()), std::as_bytes(code)}, {table, std::as_bytes(tableWords)}}};
|
||||
const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0u, {false, false, false}, false, 1};
|
||||
ShaderRecompiler::RecompileRequest request{
|
||||
{ShaderStage::Compute, reinterpret_cast<std::uintptr_t>(code.data()), code, 0, {}},
|
||||
{32, 0, userData, compute, std::nullopt, std::nullopt, memory},
|
||||
device.Target(),
|
||||
{0, 0, 0, 128}
|
||||
};
|
||||
request.useCache = false;
|
||||
const auto result = ShaderRecompiler::Recompile(request);
|
||||
const auto guest = std::find_if(result.bindings.begin(), result.bindings.end(), [](const auto& binding) { return binding.role == DescriptorRole::GuestBuffers; });
|
||||
Require(guest != result.bindings.end() && guest->count == buffers, label + "the program does not bind every buffer");
|
||||
const bool hasShaderData = std::any_of(result.bindings.begin(), result.bindings.end(), [](const auto& binding) { return binding.role == DescriptorRole::ShaderData; });
|
||||
Require(hasShaderData == shaderData, label + (shaderData ? "the buffer offsets are not in shader data" : "the buffer offsets are not in push constants"));
|
||||
device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast<std::uintptr_t>(code.data()));
|
||||
device.WaitIdle();
|
||||
for (std::uint32_t buffer = 0; buffer < buffers; ++buffer) {
|
||||
const auto base = BufferBase(buffer);
|
||||
for (std::uint32_t tid = 0; tid < Threads; ++tid) {
|
||||
const auto input = Data[base + tid];
|
||||
const auto actual = Data[base + Threads + tid];
|
||||
const auto expected = InputValue(buffer, tid) + Increment(buffer);
|
||||
Require(input == InputValue(buffer, tid), label + "buffer " + std::to_string(buffer) + " thread " + std::to_string(tid) + " input changed to " + Hex(input));
|
||||
Require(actual == expected, label + "buffer " + std::to_string(buffer) + " thread " + std::to_string(tid) + " is " + Hex(actual) + ", expected " + Hex(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main() {
|
||||
try {
|
||||
const auto device = OpenVulkanTestDevice();
|
||||
if (!device) return VulkanTestSkipped;
|
||||
Run(*device, PushCode, PushBuffers, false);
|
||||
Run(*device, DataCode, DataBuffers, true);
|
||||
std::puts("many buffers tests passed");
|
||||
return 0;
|
||||
} catch (const std::exception& error) {
|
||||
std::cerr << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue