diff --git a/.github/workflows/conventions.yml b/.github/workflows/conventions.yml new file mode 100644 index 000000000..a6ef53eb0 --- /dev/null +++ b/.github/workflows/conventions.yml @@ -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 }} diff --git a/.gitmodules b/.gitmodules index 0a5f83fb7..fc2e48631 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/3rdparty/libjpeg-turbo b/3rdparty/libjpeg-turbo new file mode 160000 index 000000000..c85e6b905 --- /dev/null +++ b/3rdparty/libjpeg-turbo @@ -0,0 +1 @@ +Subproject commit c85e6b905bf237038faa936dab160ebfc5da0344 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a34bd71a5..a901ee126 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/README.md b/README.md index a06939e2b..5e57cb94f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/core/Decoder/Jpeg/CMakeLists.txt b/core/Decoder/Jpeg/CMakeLists.txt index 2280f398b..fcb338b8c 100644 --- a/core/Decoder/Jpeg/CMakeLists.txt +++ b/core/Decoder/Jpeg/CMakeLists.txt @@ -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 diff --git a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp index 275217fb2..13c1d8acc 100644 --- a/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp +++ b/core/Decoder/Jpeg/include/Decoder/Jpeg.hpp @@ -21,8 +21,15 @@ struct Header { std::uint32_t channels; }; +enum class Sampling { + Yuv444, + Yuv422, + Yuv420, +}; + std::vector Encode(std::span 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
ParseHeader(std::span jpeg); diff --git a/core/Decoder/Jpeg/src/Jpeg.cpp b/core/Decoder/Jpeg/src/Jpeg.cpp index 74a50c4f8..d446841cc 100644 --- a/core/Decoder/Jpeg/src/Jpeg.cpp +++ b/core/Decoder/Jpeg/src/Jpeg.cpp @@ -4,16 +4,12 @@ #include #include -#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*>(context); - const auto* bytes = static_cast(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 Encode(std::span 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 Encode(std::span pixels, std::uint if (pixels.size() < static_cast(width) * height * channels) { throw std::invalid_argument("Jpeg::Encode: pixel buffer is too small"); } - - std::vector output; - const int written = stbi_write_jpg_to_func(appendBytes, &output, static_cast(width), static_cast(height), - static_cast(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(restartBlocks)) < 0) || + (restartRows != 0 && tj3Set(handle, TJPARAM_RESTARTROWS, static_cast(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(width), 0, static_cast(height), pixelFormat, &buffer.data, &size) < 0) { + throw std::runtime_error("Jpeg::Encode: encoding failed"); + } + return {buffer.data, buffer.data + size}; } std::optional
ParseHeader(std::span jpeg) { diff --git a/core/Decoder/Jpeg/tests/Jpeg.cpp b/core/Decoder/Jpeg/tests/Jpeg.cpp index 38e2aa42d..f5f9c309a 100644 --- a/core/Decoder/Jpeg/tests/Jpeg.cpp +++ b/core/Decoder/Jpeg/tests/Jpeg.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include 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& 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(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 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 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(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 garbage{1, 2, 3, 4, 5, 6, 7, 8}; Require(!Decoder::Jpeg::Decode(garbage).has_value()); diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 510110335..ff4a072a2 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -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:$") - 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) diff --git a/core/libs/SceTypes.hpp b/core/libs/SceTypes.hpp index f58476bec..5bd1b5553 100644 --- a/core/libs/SceTypes.hpp +++ b/core/libs/SceTypes.hpp @@ -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; diff --git a/core/libs/nid/include/nid/ElfPatcher.hpp b/core/libs/nid/include/nid/ElfPatcher.hpp index dd70791c7..8dbc7d4bf 100644 --- a/core/libs/nid/include/nid/ElfPatcher.hpp +++ b/core/libs/nid/include/nid/ElfPatcher.hpp @@ -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; diff --git a/core/libs/nid/src/ElfNidPatcher.cpp b/core/libs/nid/src/ElfNidPatcher.cpp index aa9bd06c0..319cf1feb 100644 --- a/core/libs/nid/src/ElfNidPatcher.cpp +++ b/core/libs/nid/src/ElfNidPatcher.cpp @@ -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(elf, entryOffset); - if (dyn.d_tag != kDtNeeded && dyn.d_tag != kDtSoname) continue; + if (!IsDynamicNameTag(dyn.d_tag)) continue; const auto oldNameOffset = static_cast(dyn.d_val); const auto mapped = std::find_if( @@ -266,7 +266,7 @@ std::vector CollectDynamicNameOffsets(const std::vector(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(dyn.d_val)); } } diff --git a/core/libs/nid/tests/test_dynamic_strings.py b/core/libs/nid/tests/test_dynamic_strings.py new file mode 100644 index 000000000..1f9bbc847 --- /dev/null +++ b/core/libs/nid/tests/test_dynamic_strings.py @@ -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(" -#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; -} - -} diff --git a/core/libs/prx/libRenoirCore.PS5/Export.cpp b/core/libs/prx/libRenoirCore.PS5/Export.cpp deleted file mode 100644 index c0cf955a3..000000000 --- a/core/libs/prx/libRenoirCore.PS5/Export.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include -#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; -} - -} diff --git a/core/libs/prx/libSceAcm/Export.cpp b/core/libs/prx/libSceAcm/Export.cpp index db47c5fea..e62e5934d 100644 --- a/core/libs/prx/libSceAcm/Export.cpp +++ b/core/libs/prx/libSceAcm/Export.cpp @@ -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; +} + } diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index d3bb48f0b..2eb2c5aef 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -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:$") 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:$") + 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:$") + 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:$") + 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}) diff --git a/core/libs/prx/libSceAgc/Cb/src/Branch.cpp b/core/libs/prx/libSceAgc/Cb/src/Branch.cpp index 649b810f6..80e08767b 100644 --- a/core/libs/prx/libSceAgc/Cb/src/Branch.cpp +++ b/core/libs/prx/libSceAgc/Cb/src/Branch.cpp @@ -18,9 +18,9 @@ std::uint32_t* APS5_VABI sceAgcCbBranch(CommandBuffer* buf, std::uint8_t mode, s const auto address = reinterpret_cast(compareAddress); const auto first = reinterpret_cast(buffer1); const auto second = reinterpret_cast(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(compareFunction) << 8u), static_cast(address), static_cast(address >> 32u), static_cast(mask), static_cast(mask >> 32u), static_cast(reference), static_cast(reference >> 32u), static_cast(first), static_cast(first >> 32u), sizeInDwords1 | (static_cast(cachePolicy1) << 28u), static_cast(second), static_cast(second >> 32u), sizeInDwords2 | (static_cast(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(writeAddress); + const auto source = reinterpret_cast(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(source), static_cast(source >> 32u), reference, mask, static_cast(destination), static_cast(destination >> 32u), writeData}, __func__); } std::uint32_t APS5_VABI sceAgcCbCondWriteGetSize() { - NotImplemented_nid_no_patch(__func__); - return 0; + return 36; } } diff --git a/core/libs/prx/libSceAgc/Command/src/Packet.cpp b/core/libs/prx/libSceAgc/Command/src/Packet.cpp index e2bf38b21..9a9a1827a 100644 --- a/core/libs/prx/libSceAgc/Command/src/Packet.cpp +++ b/core/libs/prx/libSceAgc/Command/src/Packet.cpp @@ -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(registers); - CheckAddress(address, 4, function); + if (address != 0 || count != 0) CheckAddress(address, 4, function); CheckBits(count, 0x3fffu, function); return Emit(buffer, opcode, {static_cast(address), static_cast(address >> 32u), 0x80000000u, count}, function); } diff --git a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp index 7dc690c4a..bb68d80d8 100644 --- a/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp +++ b/core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp @@ -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__); } diff --git a/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp b/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp index 27f037bd2..8e78f4864 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp @@ -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() { diff --git a/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp b/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp index 11d3ce528..f9b0b4c99 100644 --- a/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp +++ b/core/libs/prx/libSceAgc/DcbState/src/Registers.cpp @@ -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__); } diff --git a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp index 820fb9a77..d5ff3372f 100644 --- a/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp @@ -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__); } diff --git a/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp b/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp index bae2206ad..7fff4987c 100644 --- a/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp @@ -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(command); - Agc::Command::CheckAddress(address, 4, __func__); + Agc::Command::CheckAddress(address, 4, function); cmd[1] = (cmd[1] & 3u) | (static_cast(address) & ~3u); cmd[2] = static_cast(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(buffer); const auto packetEnd = reinterpret_cast(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(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__); } } diff --git a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp index daa5b4216..4da4af578 100644 --- a/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp @@ -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(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(address); + cmd[3] = static_cast(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(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(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(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(cmd), 4, __func__); - Agc::Command::Require(((cmd[0] >> 8u) & 0xffu) == 0x37u, __func__, "not a WRITE_DATA packet"); - cmd[2] = static_cast(address); - cmd[3] = static_cast(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__); } } diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index 9c447d703..8c6244952 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -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__); diff --git a/core/libs/prx/libSceAgc/tests/BranchPatch.cpp b/core/libs/prx/libSceAgc/tests/BranchPatch.cpp index 0062068af..2382676ee 100644 --- a/core/libs/prx/libSceAgc/tests/BranchPatch.cpp +++ b/core/libs/prx/libSceAgc/tests/BranchPatch.cpp @@ -58,6 +58,21 @@ void testWriter() { check(storage.packet[expected.size()] == 0xabcdef01u, "branch command overwrote following word"); } +void testUnusedFields() { + std::array 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"); diff --git a/core/libs/prx/libSceAgc/tests/CbCondWrite.cpp b/core/libs/prx/libSceAgc/tests/CbCondWrite.cpp new file mode 100644 index 000000000..69186e10b --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/CbCondWrite.cpp @@ -0,0 +1,86 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include + +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 +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(address); +} + +struct Storage { + std::array 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; + } +} diff --git a/core/libs/prx/libSceAgc/tests/Commands.cpp b/core/libs/prx/libSceAgc/tests/Commands.cpp index 4e11a5bed..8abf0f24a 100644 --- a/core/libs/prx/libSceAgc/tests/Commands.cpp +++ b/core/libs/prx/libSceAgc/tests/Commands.cpp @@ -11,14 +11,19 @@ #include 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(); diff --git a/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp b/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp index f338beea7..762f40b1a 100644 --- a/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp +++ b/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp @@ -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 -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(address) && storage.packet[2] == static_cast(address >> 32u), "SetCommandAddress wrote the wrong address"); + check(setCommandAddress(storage.packet, at(address)) == 0, "SetCommandAddress failed"); + check(storage.packet[1] == (static_cast(address) | 3u) && storage.packet[2] == static_cast(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) { diff --git a/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp b/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp new file mode 100644 index 000000000..df935a053 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/EndOfShaderActionSize.cpp @@ -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 +#include +#include +#include + +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 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((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; + } +} diff --git a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp index ffe1e8690..65343c02d 100644 --- a/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp +++ b/core/libs/prx/libSceAgc/tests/RegisterCommands.cpp @@ -6,6 +6,8 @@ #include 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 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(reinterpret_cast(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 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(registers.data()); + check(packet[1] == static_cast(address) && packet[2] == static_cast(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; diff --git a/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp b/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp new file mode 100644 index 000000000..5f543c870 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/WriteDataPatch.cpp @@ -0,0 +1,123 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include +#include +#include + +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 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 +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 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(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(variant.maxDst + 1u)); }); + expectFailure([&] { variant.setDst(written, 0); }); + expectFailure([&] { variant.setCachePolicy(written, 4); }); + check(packet.words == before, "rejected patch modified the packet"); + + std::array 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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index 6fec2dd1f..a850b0a93 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -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:$") 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:$") + 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) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp index 665b5d159..1f643db73 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver.hpp @@ -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); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp index cfb690a7b..3c0093fbe 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Driver/Diagnostics.hpp @@ -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 diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp index b5522dda3..e78777f28 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4Opcodes.hpp @@ -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"}, diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index 0064178b9..8828e54a3 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -12,6 +12,7 @@ #include #include #include +#include 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 diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp index 002fb2ed3..59d038112 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp @@ -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; +} + } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp index 7bc2326f4..9f1d52007 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Dispatch/Dispatch.cpp @@ -24,7 +24,7 @@ void Driver::dispatch(QueueState& queue, std::span packet, const auto userCount = (readRegister(queue.shader, 0x213) >> 1u) & 0x1fu; std::vector 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 memory{{snapshot.codeAddress, std::as_bytes(std::span(snapshot.code))}}; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp index 42a10f51d..f3d2e0120 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawDecode.cpp @@ -40,7 +40,7 @@ std::shared_ptr 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; }; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp index 9b622dbff..91e589164 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/EntryPoints.cpp @@ -1,4 +1,5 @@ #include "prx/libSceAgcDriver/Execution/include/Driver/Driver.hpp" +#include 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(); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp index f77bb313a..442cae836 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp @@ -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; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp index cdc6d68b8..08f5cee6c 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -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(static_cast(guest[cursor + 8] & ~3u) | (static_cast(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(static_cast(guest[cursor + 1] & ~3u) | (static_cast(guest[cursor + 2] & 0xffffu) << 32u)); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp index 8ba86476a..85acc59f5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp @@ -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(shader->code); - GuestMemory::CheckRange(code, shader->shader_size, 256); - ShaderSnapshot snapshot{reinterpret_cast(code), reinterpret_cast(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(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(fields.code); + GuestMemory::CheckRange(code, fields.shader_size, 256); + ShaderSnapshot snapshot{reinterpret_cast(code), reinterpret_cast(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(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(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(registers + i), sizeof(value)); + std::fprintf(stderr, " %x=%08x", value.offset, value.value); + } + }; + std::fprintf(stderr, "[shader] 0x%llx type %u cx", static_cast(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); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index 26f818d92..fb1d124a8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -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 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 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 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) { diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index 5cadb337f..415baf87d 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -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 #include #include +#include #include #include #include @@ -42,6 +45,7 @@ #include #include #include +#include #include #include #include @@ -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(name[start + index])) == std::tolower(static_cast(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("vkGetPhysicalDeviceProperties")(physical, &properties); APS5_LOG_OUT("Physical device candidate=%p name=%s api=0x%x type=%d", reinterpret_cast(physical), properties.deviceName, properties.apiVersion, static_cast(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("vkGetPhysicalDeviceProperties2")(selected, &properties); state->properties = properties.properties; state->physical = selected; + state->srgbDecodeFormats = Graphics::SrgbDecodeFormats(state->InstanceFunction("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("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("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; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp index 238c05424..842d3230f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp @@ -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 TFunction Function(const char* name) const { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp index baf70ca07..0374fc933 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp @@ -148,6 +148,7 @@ struct MirrorStats { std::uint64_t heapRefills = 0; }; MirrorStats MirrorCounters(); +void ClearImageMirrors(VkDevice device); struct AddressCopy { std::uint64_t begin; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp index c64f6726e..5312d9b5b 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/GuestSamplerResource.hpp @@ -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 words); +GuestSamplerResource DecodeSamplerResource(std::span words, bool unnormalizedProven = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp index fe55c0dce..27e6293ad 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp @@ -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 ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false); +std::set ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing = false, bool imageInt64Atomics = false); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp index e560d391a..564679119 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Sampler.hpp @@ -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 Get(const Context& context, std::span words, bool compareEnable); + std::shared_ptr Get(const Context& context, std::span 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> samplers); + } #endif diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp index 69d086ca4..29e48af83 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp @@ -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 fastTexture(const ImageRecord& record); - void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item); + void resolveImageBinding(const ShaderRecompiler::DescriptorBinding& binding, Binding& item, std::span> shaderSamplers); void forgetDeferredInputs(); void release() noexcept; void prepareAddressBindings(std::span shaders, std::span snapshots); @@ -412,6 +412,7 @@ private: std::vector storageFirstLayer; std::vector storageWritten; std::vector storageAtomic; + std::vector storageAtomic64; std::vector> samplers; bool reusable = false; std::vector 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; std::uint64_t storageBuffers = 0; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp index 198939b7b..44f685783 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp @@ -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 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 source; std::shared_ptr storageSource; std::unique_ptr 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 extraViews; std::map firstLayerViews; std::map, VkImageView> atomicViews; + std::map, VkImageView> uintViews; bool attachable = false; std::map, VkImageView> attachmentViews; VkFormat storageFormat = VK_FORMAT_UNDEFINED; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp index b23f2f1da..8820c85bc 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/TextureFormat.hpp @@ -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 FindGuestTextureFormat(VkFormat format, std::uint32_t elementBytes); std::optional 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); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 1f8c2d1d8..463093477 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -15,6 +15,8 @@ #include #include #include +#include +#include #include #include @@ -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(sizeof(probe), BytesPerElement(resource.format))))) { static std::mutex reportedMutex; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp index aafdb1e31..cd8095a8f 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp @@ -518,7 +518,7 @@ std::set CachedFragmentOutputs(const Context& context, std::span< } std::set 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()); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp index bb16630cc..1a1c8941e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp @@ -1002,6 +1002,19 @@ MirrorStats MirrorCounters() { return stats; } +void ClearImageMirrors(VkDevice device) { + auto& state = Mirrors(); + std::map> 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 diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp index d28297551..e70e48018 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp @@ -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((raw ^ 0x2000u) - 0x2000u); return static_cast(extended) / 256.0f; @@ -41,7 +54,7 @@ float toSignedLodBias(std::uint32_t raw) { } -GuestSamplerResource DecodeSamplerResource(std::span words) { +GuestSamplerResource DecodeSamplerResource(std::span 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 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 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 words) static_cast(anisoThreshold); static_cast(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 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 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; } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp index 660b16bb1..851ae3bf7 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Sampler.cpp @@ -1,6 +1,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Sampler.hpp" #include #include +#include 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("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(capacity, 1)) {} - std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable) { + std::shared_ptr SamplerCache::Get(const Context& context, std::span words, bool compareEnable, bool unnormalizedProven) { Require(words.size() == 4, "guest sampler descriptor must contain 4 dwords"); - const std::array key{words[0], words[1], words[2], words[3], compareEnable ? 1u : 0u}; + const std::array 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(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> 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"); + } + } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp index d6d69c35f..5a75b94a1 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp @@ -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 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 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 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 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(binding.count) * 4, "guest buffer descriptor must contain four DWORDs per array element"); @@ -943,6 +954,10 @@ void ShaderResources::buildPrepare(std::span 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>(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 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(binding.imageSamplers.size())); + key.insert(key.end(), binding.imageSamplers.begin(), binding.imageSamplers.end()); } return key; } @@ -1697,7 +1716,7 @@ bool ShaderResources::Revalidate(std::span shaders, ProofR for (std::uint32_t element = 0; element < binding.count; ++element) { const auto words = std::span(binding.guestDescriptor).subspan(static_cast(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(binding.guestDescriptor).subspan(static_cast(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(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 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> 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(binding.guestDescriptor).subspan(static_cast(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(*binding.imageShape)) + ", dimension " + std::to_string(static_cast(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; 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(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(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(resource.tileMode), resource.dccAddress}); item.imageAllocations.push_back(storageTextures.size() - 1); } diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index 67a61cc4e..296b7f0ee 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -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(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 ValidateShaders(std::span shaders, const State& state, const VkPhysicalDeviceSubgroupProperties& subgroup, bool fragmentShaderBarycentric, bool descriptorIndexing) { +std::set ValidateShaders(std::span 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 ValidateShaders(std::span 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); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index 02a893d26..53288a665 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -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(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; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp index a6e017329..71ac7dd10 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp @@ -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("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& 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(descriptor.baseAddress), descriptor.width, descriptor.height, descriptor.mipCount, static_cast(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& 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("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("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("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("vkDestroyImageView")(context.device, atomic, nullptr); atomicViews.clear(); + for (const auto& [key, uint] : uintViews) context.Function("vkDestroyImageView")(context.device, uint, nullptr); + uintViews.clear(); for (const auto& [format, attachment] : attachmentViews) context.Function("vkDestroyImageView")(context.device, attachment, nullptr); attachmentViews.clear(); if (view) context.Function("vkDestroyImageView")(context.device, view, nullptr); diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp index 62bde9b8c..04fda0eda 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp @@ -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 +#include +#include #include #include @@ -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(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(guestFormat); + if ((context.srgbDecodeFormats & ShaderRecompiler::SrgbDecodeBit(format)) == 0u) return ResolveTextureFormat(guestFormat); + return ResolveTextureFormat(static_cast(ShaderRecompiler::SrgbUnormFormat(format))); +} + std::optional 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 FindGuestColorTargetFormat(VkFormat format, std::uint32_t elementBytes) { switch (format) { case VK_FORMAT_B8G8R8A8_UNORM: format = VK_FORMAT_R8G8B8A8_UNORM; break; diff --git a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp index 4e5eaa6ab..ff798fca2 100644 --- a/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp +++ b/core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp @@ -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(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; diff --git a/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp b/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp index 4639d647f..56db31822 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp +++ b/core/libs/prx/libSceAgcDriver/Submit/include/Dcb.hpp @@ -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 diff --git a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp index 48da441e6..6ced7913b 100644 --- a/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp +++ b/core/libs/prx/libSceAgcDriver/Submit/src/Dcb.cpp @@ -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); } } diff --git a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp index 8add7f045..934422871 100644 --- a/core/libs/prx/libSceAgcDriver/Unimplemented.cpp +++ b/core/libs/prx/libSceAgcDriver/Unimplemented.cpp @@ -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; } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp index 4cea9514c..faf2f911c 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaExecution.cpp @@ -130,6 +130,39 @@ void RunBdaExecutionTests(const Context& context) { run(guest, 8, 0, Abi::FaultReason::InvalidTable, 3); run(std::numeric_limits::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 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 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); } diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp index b05dabe90..9c4dd099d 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.cpp @@ -3,11 +3,19 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" -std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset) { +namespace { + +template +std::vector 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 MakeBdaTestShader(std::uint64_t address, std::uint32_ state.bdaPagetableVariable = define(0); state.faultBufferVariable = define(1); const auto output = define(2); + std::vector 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 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(offset) : static_cast(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 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(offset) : static_cast(offset)), offset < 0); + return std::vector{EmitBdaRead(ctx, instruction, base, bits)}; + }); +} + +std::vector 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(values.begin(), values.begin() + dwords); + }); +} diff --git a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp index 312a64ba1..2baa4468a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp +++ b/core/libs/prx/libSceAgcDriver/tests/BdaShader.hpp @@ -6,6 +6,7 @@ #include std::vector MakeBdaTestShader(std::uint64_t address, std::uint32_t bits, std::int64_t offset = 0); +std::vector MakeBdaDwordReadTestShader(std::uint64_t address, std::uint32_t dwords, bool coherent, bool stops); void RunBdaExecutionTests(const AgcDriver::Graphics::Context& context); void RunBdaContractTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index ee6cd1e82..df874dd91 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -53,6 +53,13 @@ std::vector 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 mergeOwners; std::vector> 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 diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index d7f881661..2d45534e2 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -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(0x1000), 8)); }), "an unmapped V# element"); expectSingleFailure(changed([&](auto& binding) { binding.count = 2; binding.guestDescriptor = join(vsharp(guestFirst.data(), 16), vsharp(reinterpret_cast(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(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(capability)}); + const std::array 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(); diff --git a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp index 4af9a844c..4a423ed0a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/GuestSamplerResource.cpp @@ -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 #include +#include +#include #include #include +#include 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 +THandle fakeHandle(std::uint64_t value) { + if constexpr (std::is_pointer_v) return reinterpret_cast(static_cast(value)); + else return static_cast(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(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(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(captureCreateSampler); + if (std::strcmp(name, "vkDestroySampler") == 0) return reinterpret_cast(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 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(context, DecodeSamplerResource(pack(base))), std::make_shared(context, captured), std::make_shared(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& 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 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 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; diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 3af51bbd8..661d07a16 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -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 #include #include +#include #include #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, 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 branch(std::uint32_t mode, std::uint32_t function, const std::vector* first, const std::vector* second) { + const auto address = [](const std::vector* target) { return target ? reinterpret_cast(target->data()) : std::uintptr_t{0}; }; + const auto size = [](const std::vector* target) { return target ? static_cast(target->size()) : 0u; }; + return makePacket(0x3f, {mode | (function << 8u), 0, 0, 0, 0, 0, 0, static_cast(address(first)), static_cast(address(first) >> 32u), size(first), static_cast(address(second)), static_cast(address(second) >> 32u), size(second)}); +} + +void testBranchSubmission() { + static std::array results{}; + static std::vector 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 written{}; @@ -780,6 +817,81 @@ void testPredicatedSubmission() { AgcDriverWaitIdle_nid_postfix(); } +std::vector 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, 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(&memory.target)) { + memory.written = true; + } else { + check(address == reinterpret_cast(&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 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(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(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; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp b/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp deleted file mode 100644 index e7860d456..000000000 --- a/core/libs/prx/libSceAgcDriver/tests/Recompiler.cpp +++ /dev/null @@ -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 -#include -#include -#include -#include - -int main(int argc, char** argv) { - try { - alignas(256) const std::array 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(code.data()); - std::vector commands{ - 0xc0027600, 0x20c, static_cast(address >> 8u), static_cast(address >> 40u), - 0xc0017600, 0x213, 0, - 0xc0031500, 1, 1, 1, 0x8041 - }; - Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; - const std::array arguments{1, 1, 1}; - alignas(256) const std::array vertexCode{0xbf810000}; - Shader vertexShader = shader; - const std::array 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(vertexCode.data()) + 256; - const auto indexAddress = reinterpret_cast(indices.data()); - const auto pixelAddress = std::string(argv[1]) == "vertex" ? 0 : address; - commands = { - 0xc0027600, 0x0c8, static_cast(vertexAddress >> 8u), static_cast(vertexAddress >> 40u), - 0xc0017600, 0x08b, 0, - 0xc0027600, 0x008, static_cast(pixelAddress >> 8u), static_cast(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(&destination); - const std::array draw{ - 0xc0012600, static_cast(indexAddress), static_cast(indexAddress >> 32u), - 0xc0033500, 3, 1, 3, 0x20, - 0xc0033700, 0x100, static_cast(destinationAddress), static_cast(destinationAddress >> 32u), 7 - }; - commands.insert(commands.end(), draw.begin(), draw.end()); - packet = Packet{commands.data(), static_cast(commands.size()), 0, {}}; - } - if (argc == 2 && std::string(argv[1]) == "indirect") { - const auto argumentAddress = reinterpret_cast(arguments.data()); - commands[7] = 0xc0021600; - commands[8] = static_cast(argumentAddress); - commands[9] = static_cast(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; - } -} diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp index 20dd3e202..11af875da 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp @@ -1,5 +1,6 @@ #include "ShaderDiskCache.hpp" #include "ShaderCacheDirectory.hpp" +#include #include #include #include @@ -7,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -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& left, const std::vector& 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 code{0xf4900100u, 0x00000000u, 0xbf8cc07fu, 0x7e020204u, 0xe0700000u, 0x80000100u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + std::array userData{0x10000000u, 0x00000000u, 0x40u, 0x00027facu}; + std::array 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(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(emissionFailure(layout.request)); + require(diskMisses() == before.misses + 2, "another binding layout reused the failure"); + ClockRequest stride(16); + static_cast(emissionFailure(stride.request)); + require(diskMisses() == before.misses + 3, "another buffer specialization reused the failure"); + ClockRequest relocated; + relocated.request.shader.codeAddress += 0x1000u; + static_cast(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, 4> failures; + for (auto& future : failures) future = std::async(std::launch::async, [&concurrent] { return emissionFailure(concurrent.request); }); + std::vector 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"; diff --git a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp index a65c7d9e5..7a2e71bca 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp @@ -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 #include #include @@ -24,6 +26,7 @@ #include #include #include +#include #include #include @@ -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 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 code, std::uint32_t subgroupSize) { + static constexpr std::array 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 lane63{0xd7600006u, 0x00017f00u, 0x7e020206u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + static constexpr std::array lane31{0xd7600006u, 0x00013f00u, 0x7e020206u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + static constexpr std::array waterfall{0xbe80047eu, 0xbe821400u, 0xd7600003u, 0x00000500u, 0xbe801c02u, 0xbf138000u, 0xbf85fffau, 0x7e020203u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + static constexpr std::array computeLane63{0xd7600006u, 0x00017f00u, 0x7e020206u, 0xe0700000u, 0x80000100u, 0xbf810000u}; + static constexpr std::array permlanex16{0xd7780001u, 0x02010100u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + static constexpr std::array permlane16{0xd7770001u, 0x02010100u, 0xf800180fu, 0x01010101u, 0xbf810000u}; + expectFailure([&] { static_cast(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(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(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(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 code{0xbf810000u}; + const std::array userData{0x12345678u, 0xabcdef01u, 0x87654321u}; + const std::array 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, 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 key; + RecompileCacheKey::Build(request, key); + std::vector 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(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(serializer.Deserialize(unsupported)); }, "serialization version", "an unsupported request version was accepted"); + } +} + +void verifyLegacyPixelRequests() { + using namespace ShaderRecompiler; + static constexpr std::array 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{}, "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 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 noPerspectiveLocations(std::span 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 bytes{}; }; + static Texture texture; + static std::array output{}; + const auto textureBase = static_cast(reinterpret_cast(texture.bytes.data())); + const auto outputBase = static_cast(reinterpret_cast(output.data())); + const std::array unnormalized{0x00008092u, 0x00fff000u, 0x05500000u, 0u}; + const std::array normalized{0x00000092u, 0x00fff000u, 0x05500000u, 0u}; + const auto imageData = [&](const std::array& sampler, std::uint32_t type, std::uint32_t format, std::uint32_t depth) { + return std::array{ + static_cast(textureBase >> 8u), static_cast((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(outputBase), static_cast((outputBase >> 32u) & 0xffffu), 64u, 0xfacu}; + }; + const auto userData = [&](const std::array& sampler) { + return imageData(sampler, Type2D, Format8888UNorm, 0u); + }; + const auto program = [](std::uint32_t mimg) { + return std::vector{0x7e020280u, 0x7e040280u, 0x7e060280u, 0x7e080280u, 0x7e0a0280u, 0x7e0c0280u, mimg, 0x00400801u, 0xe0700000u, 0x80030800u, 0xbf810000u}; + }; + const std::array capabilities{1u, static_cast(spv::CapabilityImageGatherExtended), static_cast(spv::CapabilityMinLod)}; + std::uint64_t nextAddress = 0x40000u; + const auto recompile = [&](const std::vector& code, const std::array& 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(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{expected} && flags(result, DescriptorRole::GuestImages) == std::vector{expected}; + }; + const auto auditSpirv = [](const std::vector& 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& data, const char* reason, const char* what) { + expectFailure([&] { static_cast(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(recompile(program(mimg), imageData(normalized, mimg == 0xf09c0f18u ? TypeCube : Type2D, Format8888UNorm, mimg == 0xf09c0f18u ? 5u : 0u))); + } + + const std::array 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(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{true} && bindings[1].samplerUnnormalized == std::vector{true}, "unnormalized samplers: an S# without live uses was not flagged"); + auto selected = info; + selected.images[0].indirectRoot = 0u; + expectFailure([&] { static_cast(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(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{false} && normalized[1].samplerUnnormalized == std::vector{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 capabilities{spv::CapabilityInt64, spv::CapabilityPhysicalStorageBufferAddresses, spv::CapabilityStorageBuffer8BitAccess}; + const std::array extensions{"SPV_KHR_physical_storage_buffer", "SPV_KHR_8bit_storage"}; + alignas(256) static std::array output{}; + const auto outputAddress = static_cast(reinterpret_cast(output.data())); + const std::array userData{0x10000000u, 0u, 0u, 0u, static_cast(outputAddress), static_cast((outputAddress >> 32u) & 0xffffu), 128u, 0x01016facu}; + const auto compile = [&](std::uint32_t loads, bool barrier, bool coherent) { + std::vector 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 names; + std::map 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 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(&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 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"); diff --git a/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp new file mode 100644 index 000000000..bfd9fd061 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/SpirvModule.cpp @@ -0,0 +1,203 @@ +#include "SpirvBackend/SpirvModule.hpp" +#include +#include +#include +#include +#include + +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 opcodeCounts(const std::vector& words) { + std::map 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(32), static_cast(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(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(-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 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 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(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; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index 6888c50b6..d611db473 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -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 invalid{0xc0017600, 0x20c, 0}; + std::array invalidAddresses{invalid.data()}; + std::array 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 addresses{first.data(), second.data(), third.data()}; + std::array sizes{static_cast(first.size()), static_cast(second.size()), static_cast(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 code{0xbf810000}; + alignas(8) static std::array 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(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(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 words{0xc0031500, 1, 1, 1, 0x41}; Packet packet{words.data(), static_cast(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"); diff --git a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp index 6fa05cd45..245766a02 100644 --- a/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/TextureFormat.cpp @@ -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 +#include #include #include @@ -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 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 keys{}; + GuestTextureResource resource{}; + resource.dccAddress = reinterpret_cast(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(ShaderRecompiler::RemapTextureFormat(static_cast(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(); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp new file mode 100644 index 000000000..7ba31cd23 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/BufferFormat.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +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 Input{}; +alignas(256) std::array Output{}; +alignas(256) std::array Scratch{}; + +alignas(256) constexpr std::array 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 FormattedWord3{0x31038facu, 0x31041facu, 0x31042facu, 0x31039facu, 0x31041977u, 0x3103b80du, 0x31038facu, 0x31041facu, 0x31039facu, 0x31042facu}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes, std::uint32_t word3) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((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 userData(8u + 4u * FormattedWord3.size(), 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(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(Scratch.size() * 4u), FormattedWord3[index]) + : BufferDescriptor(Input.data(), static_cast(Input.size() * 4u), FormattedWord3[index]); + std::copy(descriptor.begin(), descriptor.end(), userData.begin() + 8 + 4 * index); + } + const std::span code(Code); + const std::array memory{{{reinterpret_cast(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(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(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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DeviceSelection.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceSelection.cpp new file mode 100644 index 000000000..b4579b321 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DeviceSelection.cpp @@ -0,0 +1,54 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include + +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(std::toupper(static_cast(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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DsAppendConsume.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DsAppendConsume.cpp new file mode 100644 index 000000000..46ad1d7b6 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DsAppendConsume.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 Output{}; + +alignas(256) constexpr auto Code = std::to_array({ + 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 BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (4u << 16u), count, 0x01016facu}; +} + +void Run(AgcDriver::VulkanDevice& device) { + Output.fill(0xdeadbeefu); + std::vector userData(4, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(output.begin(), output.end(), userData.begin()); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(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(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(code.data())); + device.WaitIdle(); +} + +void Check() { + constexpr std::array 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 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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp new file mode 100644 index 000000000..9da6c4caf --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr auto Code = std::to_array({ + 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({0xd8b80505, 0x0a07061e, 0xbf810000}); +alignas(256) constexpr auto CoincidingOffsetsSt64 = std::to_array({0xd8bc0000, 0x0a07061e, 0xbf810000}); +alignas(256) constexpr auto DestinationOverflow = std::to_array({0xd8b82000, 0xff07061e, 0xbf810000}); + +std::array BufferDescriptor(const void* data, std::uint32_t count) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((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 +void Translate(AgcDriver::VulkanDevice& device, std::span code, TUse&& use) { + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size())); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size())); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(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(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(Code.data())); + device.WaitIdle(); + }); +} + +void Reject(AgcDriver::VulkanDevice& device, std::span 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 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 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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2B64.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2B64.cpp new file mode 100644 index 000000000..b07927ea4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/DsWrxchg2B64.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr auto Code = std::to_array({ + 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({0xd9b80101, 0x1208062e, 0xbf810000}); +alignas(256) constexpr auto CoincidingOffsetsSt64 = std::to_array({0xd9bc0000, 0x1a08062f, 0xbf810000}); +alignas(256) constexpr auto DestinationOverflow = std::to_array({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(seed >> 32u); + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((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 +void Translate(AgcDriver::VulkanDevice& device, std::span code, TUse&& use) { + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::array memory{{{reinterpret_cast(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(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(Code.data())); + device.WaitIdle(); + }); +} + +void Reject(AgcDriver::VulkanDevice& device, std::span 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 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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp index ac3b614f2..c664f1d29 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/EmulatedColorCompare.cpp @@ -5,6 +5,7 @@ #include "VulkanTestDevice.hpp" #include #include +#include #include #include #include @@ -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 Red{0, 64, 128, 255}; alignas(256) std::array Input{}; alignas(256) std::array Output{}; +alignas(256) std::array Extra{}; alignas(4096) std::array Texels{}; alignas(256) constexpr std::array Code{ @@ -49,10 +52,17 @@ alignas(256) constexpr std::array Code{ 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, }; +alignas(256) constexpr std::array 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 BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -73,9 +83,9 @@ std::array TextureDescriptor(std::uint32_t format) { std::array 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(tid % 7u) - 3; + const auto y = static_cast((tid / 7u) % 5u) - 2; + Extra[tid * 3u + 0u] = (static_cast(x) & 0x3fu) | ((static_cast(y) & 0x3fu) << 8u) | (tid % 3u == 0u ? 0x3f0000u : 0u); + Extra[tid * 3u + 1u] = std::bit_cast(-2.5f + 0.25f * static_cast(tid % 21u)); + Extra[tid * 3u + 2u] = std::bit_cast(0.5f * static_cast(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(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(Side); const float v = Input[tid * 3u + 2u] * static_cast(Side); const float reference = std::clamp(Input[tid * 3u + 0u], 0.0f, 1.0f); - if (sampler.filter == FilterPoint) return ReferenceTexel(static_cast(std::floor(u)), static_cast(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(std::floor(u)) + offsetX, static_cast(std::floor(v)) + offsetY, reference, sampler); const float cu = u - 0.5f; const float cv = v - 0.5f; - const int x = static_cast(std::floor(cu)); - const int y = static_cast(std::floor(cv)); + const int x = static_cast(std::floor(cu)) + offsetX; + const int y = static_cast(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 userData(20, 0u); +ShaderRecompiler::RecompileResult Compile(AgcDriver::VulkanDevice& device, std::uint32_t format, const Sampler& sampler, std::span code = Code) { + std::vector userData(24, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); const auto output = BufferDescriptor(Output.data(), static_cast(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 code(Code); + const auto extra = BufferDescriptor(Extra.data(), static_cast(sizeof(Extra))); + std::copy(extra.begin(), extra.end(), userData.begin() + 20); const std::array memory{{{reinterpret_cast(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(Code.data())); + const std::span code = offsets ? std::span(OffsetCode) : std::span(Code); + const auto result = Compile(device, Format8888UNorm, sampler, code); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(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) { diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomics64.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomics64.cpp new file mode 100644 index 000000000..79da0abe0 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageAtomics64.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +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 Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array 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, 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(words[1]) << 32u; +} + +void SetWord(std::uint32_t* words, std::uint64_t value) { + words[0] = static_cast(value); + words[1] = static_cast(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 BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t width, std::uint32_t height, std::uint32_t format) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((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 userData(16, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(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 code(Code); + const std::array memory{{{reinterpret_cast(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(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(code.data())); + device.WaitIdle(); +} + +std::string Hex(std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%016llx", static_cast(value)); + return text; +} + +void Check(std::uint32_t format) { + constexpr std::array 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(a); + const auto sb = static_cast(b); + const std::array expected{ + b, + a + b, + a - b, + static_cast(std::min(sa, sb)), + std::min(a, b), + static_cast(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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp new file mode 100644 index 000000000..376473101 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageConvertedUnorm.cpp @@ -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 +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; + +alignas(256) std::array Output{}; +alignas(256) std::array Input{}; + +constexpr std::array 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 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 SamplerCode(std::uint32_t word0) { + return { + 0x34060084u, 0x7e280280u, 0x7e2a0280u, word0, 0x00610a14u, 0xbf8c3f70u, 0xe0781000u, 0x80000a03u, + 0xbf810000u, + }; +} + +alignas(256) constexpr std::array 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 StoreValues{ + 0x00000000u, 0x3f800000u, 0x3f000000u, 0x3eaaaaabu, 0x80000000u, 0xbf800000u, + 0x40000000u, 0x7f800000u, 0x7fc00000u, 0x000116c2u, 0x3f7fbe77u, +}; + +class GuestBlock { +public: + GuestBlock() { +#ifdef _WIN32 + block = static_cast(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(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 BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle, std::uint32_t width) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((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(static_cast(static_cast(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(bits); + if (!(value > 0.0f)) return 0u; + if (value >= 1.0f) return maximum; + return static_cast(std::nearbyint(static_cast(value) * maximum)); +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const std::array& texture, const std::array& extra, std::uint32_t groups, const std::array& target = {}) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(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 memory{{{reinterpret_cast(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(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(code.data())); + device.WaitIdle(); +} + +void CheckLoad(AgcDriver::VulkanDevice& device, const std::uint8_t* texels, std::span 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(0xa5u)); + Run(device, RoundTrip, TextureDescriptor(texels, UnormFormat, SwizzleXYZ1, LoadWidth), {}, LoadGroups, TextureDescriptor(target, UnormFormat, SwizzleXYZ1, LoadWidth)); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(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 code, std::uint32_t dmask, const std::string& what) { + std::fill(texels, texels + Threads * 4u, static_cast(0xa5u)); + Input.fill(0u); + std::array expected{}; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::array 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(Input.size() * 4u)), 1); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(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 code, const std::uint8_t* texels, std::uint32_t swizzle, const std::array& 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(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(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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp new file mode 100644 index 000000000..ac769d0b4 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageGatherOneDimensional.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; + +alignas(256) constexpr std::array Gather1D{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf11c0100, 0x00820603, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; +alignas(256) constexpr std::array Gather1DOffset{ + 0x34020084, 0xe0381000, 0x80000201, 0xbf8c3f70, 0xf15c0100, 0x00820602, 0xbf8c3f70, 0xe0781000, + 0x80010601, 0xbf810000, +}; + +struct Variant { + std::span code; + bool offset; + std::uint32_t clamp; + const char* name; +}; + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor() { + const auto address = static_cast(reinterpret_cast(Texels.data())); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | (((Width - 1u) & 3u) << 30u), + (Width - 1u) >> 2u, + 0xfacu | (Type1D << 28u), + 0u, 0u, 0u, 0u, + }; +} + +std::array 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(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(tid % 11u) - 5; + Input[tid * 4u + 0u] = (static_cast(offset) & 0x3fu) | (tid % 3u == 0u ? 0x3f00u : 0u); + Input[tid * 4u + 1u] = std::bit_cast(-0.3f + 0.05f * static_cast(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(Width); + const int address = clamp == ClampEdge ? std::clamp(x, 0, size - 1) : ((x % size) + size) % size; + return Red(static_cast(address)); +} + +std::array Expected(std::uint32_t tid, const Variant& variant) { + const float u = std::bit_cast(Input[tid * 4u + 1u]); + int left = static_cast(std::floor(u * static_cast(Width) - 0.5f)); + if (variant.offset) { + const auto field = Input[tid * 4u] & 0x3fu; + left += static_cast(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 userData(20, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(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 memory{{{reinterpret_cast(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(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(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::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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleDerivatives.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleDerivatives.cpp index 5c29b7e6c..34488c71a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleDerivatives.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleDerivatives.cpp @@ -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 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); } diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp new file mode 100644 index 000000000..648a88e52 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleReduction.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 Level0{17, 11, 23, 14, 20, 25, 12, 19, 13, 22, 16, 24, 21, 10, 18, 15}; +constexpr std::array Level1{5, 30, 16.5f, 9}; + +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; +alignas(4096) std::array Texels{}; +alignas(4096) std::array Colors{}; + +alignas(256) constexpr std::array Code{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf0900108, 0x00820401, 0xbf8c3f70, + 0x34160082, 0xe0701000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array GatherCode{ + 0x1614008c, 0xe03c1000, 0x8000010a, 0xbf8c3f70, 0xf11c0108, 0x00820401, 0xbf8c3f70, + 0x34160084, 0xe0781000, 0x8001040b, 0xbf810000, 0xbf810000, 0xbf810000, +}; + +alignas(256) constexpr std::array 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 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 BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(std::uint32_t format, const void* texels = Texels.data(), std::uint32_t levels = Levels) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((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 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(Level0) : std::span(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(values[y * mip.width + x]); + auto* texel = &Texels[mip.tiledOffset + static_cast(y) * mip.pitchBytes + x * 4u]; + for (std::uint32_t byte = 0; byte < 4u; ++byte) texel[byte] = static_cast(bits >> (byte * 8u)); + } + } + } +} + +std::uint8_t Red(int x, int y) { + const auto cx = static_cast(std::clamp(x, 0, static_cast(Size) - 1)); + const auto cy = static_cast(std::clamp(y, 0, static_cast(Size) - 1)); + return static_cast(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(y) * mips[0].pitchBytes + x * 4u]; + texel[0] = Red(static_cast(x), static_cast(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(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((tid * 5u + 1u) % 12u) - 1.7f) / 8.0f; + const float v = (static_cast((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(Size >> level); + const auto cx = static_cast(std::clamp(x, 0, size - 1)); + const auto cy = static_cast(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 footprint; + const auto gather = [&](std::uint32_t level) { + const float size = static_cast(Size >> level); + if (sampler.filter == FilterPoint) { + footprint.push_back(Texel(level, static_cast(std::floor(u * size)), static_cast(std::floor(v * size)))); + return; + } + const int x = static_cast(std::floor(u * size - 0.5f)); + const int y = static_cast(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 ExpectedGather(std::uint32_t tid) { + const int x = static_cast(std::floor(Input[tid * 3u + 0u] * static_cast(Size) - 0.5f)); + const int y = static_cast(std::floor(Input[tid * 3u + 1u] * static_cast(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 code = Code) { + std::vector userData(28, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(sizeof(Input))); + const auto output = BufferDescriptor(Output.data(), static_cast(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 memory{{{reinterpret_cast(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(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(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(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(Red(static_cast(tid & 3u), 0)) / 255.0f; + const int x = static_cast(std::floor(Input[tid * 3u + 0u] * static_cast(Size) - 0.5f)); + const int y = static_cast(std::floor(Input[tid * 3u + 1u] * static_cast(Size) - 0.5f)); + return static_cast(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 code, bool sampled, const char* name) { + const Sampler sampler{ReductionMin, FilterBilinear, MipPoint}; + Output.fill(-1.0f); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(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(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(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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp new file mode 100644 index 000000000..dc7da4f9a --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSampleUnnormalized.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 Buffer{}; +alignas(256) std::array SingleLevel{}; +alignas(256) std::array MultiLevel{}; + +alignas(256) constexpr std::array 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 Instructions{"image_sample_lz", "image_sample_l 2.7", "image_sample"}; + +struct SamplerCase { + const char* name; + std::array words; + bool linear; + bool border; +}; + +constexpr std::array 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 Coordinates() { + std::vector 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(k) + fraction); + } + for (const float edge : {-1.0f, -0.5f, 13.0f, 14.0f, 12.75f}) us.push_back(edge); + constexpr std::array fractions{0.5f, 0.0f, 0.75f}; + std::vector 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((index * fractions.size() + fraction) % Height) + fractions[fraction]}); + } + } + for (std::int32_t x = 0; x < Width; ++x) { + const float v = static_cast(x % Height) + 0.5f; + result.push_back({static_cast(x) - 1.0f, v}); + result.push_back({static_cast(x) + 2.0f, v}); + } + return result; +} + +std::array 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 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(y) * mip.pitchBytes + x * 4u]; + const auto value = TexelOf(static_cast(x), static_cast(y)); + for (std::uint32_t component = 0; component < 4u; ++component) { + texel[component] = level == 0u ? static_cast(value[component]) : OtherLevelTexel; + } + } + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t lastLevel, std::uint32_t maxMip) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((address >> 40u) & 0xffu) | (Format8888UNorm << 20u) | ((static_cast(Width - 1) & 3u) << 30u), + (static_cast(Width - 1) >> 2u) | (static_cast(Height - 1) << 14u), + 0xfacu | (lastLevel << 16u) | (Type2D << 28u), + 0u, + maxMip << 4u, + 0u, + 0u, + }; +} + +using Samples = std::vector>; + +Samples Run(AgcDriver::VulkanDevice& device, const std::array& texture, const std::array& sampler, const std::vector& 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(coordinate.u); + Buffer[lane * Words + 1u] = std::bit_cast(coordinate.v); + } + std::vector userData(16, 0u); + const auto buffer = BufferDescriptor(Buffer.data(), static_cast(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 code(Code); + const std::array memory{{{reinterpret_cast(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(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(code.data())); + device.WaitIdle(); + for (std::uint32_t lane = 0; lane < Threads && first + lane < coordinates.size(); ++lane) { + std::array values{}; + std::copy_n(&Buffer[lane * Words + ResultWord], Results, values.begin()); + samples.push_back(values); + } + } + return samples; +} + +std::array 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 Reference(const SamplerCase& sampler, const Coordinate& coordinate) { + if (!sampler.linear) return Fetch(sampler, static_cast(std::floor(coordinate.u)), static_cast(std::floor(coordinate.v))); + const double u = static_cast(coordinate.u) - 0.5; + const double v = static_cast(coordinate.v) - 0.5; + const auto i = static_cast(std::floor(u)); + const auto j = static_cast(std::floor(v)); + const double alpha = u - i; + const double beta = v - j; + std::array 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& 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(std::bit_cast(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& texture, const std::array& sampler, std::string_view reason, const char* what) { + const std::vector one{{0.5f, 0.5f}}; + try { + static_cast(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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSint8.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSint8.cpp new file mode 100644 index 000000000..87dd6105b --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSint8.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; +constexpr std::array LinearSampler{0x00000092u, 0x00fff000u, 0x00500000u, 0u}; + +alignas(256) std::array Output{}; +alignas(256) std::array Texture{}; + +constexpr std::array 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 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(y) * pitch + x]; +} + +std::uint32_t Signed(std::uint32_t x, std::uint32_t y) { + return static_cast(static_cast(static_cast(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(0x80u + 37u * x + 11u * y); + } + } +} + +std::array BufferDescriptor(const void* data, std::uint32_t records, std::uint32_t stride = 0u) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu) | (stride << 16u), records, 0x01016facu}; +} + +std::array 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(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((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 code, std::uint32_t format, const std::array& sampler, std::uint32_t groups) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(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 memory{{{reinterpret_cast(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(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(code.data())); + device.WaitIdle(); +} + +void Expect(std::uint32_t slot, const std::array& 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 code, std::uint32_t format, const std::array& 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, 2> heap{}; + std::array, 3> materials{}; + std::array output{}; + std::array srt{}; +}; + +struct alignas(256) TableTexture { + std::array bytes{}; +}; + +GuestTables Tables; +TableTexture TableTextures[2]; + +alignas(256) const std::array 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(reinterpret_cast(Tables.srt.data())); + const std::vector userData{static_cast(srtAddress), static_cast(srtAddress >> 32u)}; + const std::span code(TableCode); + const std::array memory{{ + {reinterpret_cast(code.data()), std::as_bytes(code)}, + {reinterpret_cast(&Tables), std::as_bytes(std::span(&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(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(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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp new file mode 100644 index 000000000..d03b3026e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageSrgbLoad.cpp @@ -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 +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 PointSampler{0x00000092u, 0x00fff000u, 0u, 0u}; +constexpr std::size_t BlockBytes = 65536; + +alignas(256) std::array Output{}; +alignas(256) std::array Texels{}; + +constexpr std::array 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 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 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(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(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(static_cast(std::ldexp(std::nearbyint(mantissa * 256.0), exponent - 8))); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* texels, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(texels)); + return { + static_cast(address >> 8u), + static_cast((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(x); + } else { + Texels[x * 2u] = static_cast(x); + Texels[x * 2u + 1u] = static_cast(255u - x); + } + } +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, const void* texels, std::uint32_t format, std::uint32_t swizzle, std::uint32_t groups) { + Output.fill(0xdeadbeefu); + std::vector userData(24, 0u); + const auto output = BufferDescriptor(Output.data(), static_cast(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 memory{{{reinterpret_cast(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(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(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 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 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 code, std::uint32_t dmask, std::uint32_t format, std::uint32_t swizzle, const std::string& what) { + const auto address = reinterpret_cast(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(StoreX) : std::span(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 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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp new file mode 100644 index 000000000..5fa74af2e --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ImageStoreSint.cpp @@ -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 +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array StoreCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0201f08, 0x0001141e, + 0x7e3e0281, 0xf0201508, 0x0001141e, 0xbf810000, +}; + +alignas(256) constexpr std::array TitleCode{ + 0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0202108, 0x0001141e, + 0xbf810000, +}; + +alignas(256) constexpr std::array LoadCode{ + 0x34020084, 0x7e3c0300, 0x7e3e0280, 0xf0001f08, 0x0001141e, 0xbf8c3f70, 0xe0781000, 0x80001401, + 0xbf810000, +}; + +alignas(256) constexpr std::array D16Code{0x7e3c0300, 0x7e3e0280, 0xf0201108, 0x8001141e, 0xbf810000}; +alignas(256) constexpr std::array PackedCode{0x7e3c0300, 0x7e3e0280, 0xf0281108, 0x0001141e, 0xbf810000}; +alignas(256) constexpr std::array AtomicD16Code{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xf0201108, 0x8001141e, 0xbf810000}; +alignas(256) constexpr std::array PackedWideCode{0x34020084, 0xe0381000, 0x80001401, 0xbf8c3f70, 0x7e3c0300, 0x7e3e0280, 0xf0281308, 0x0001141e, 0xbf810000}; +alignas(256) constexpr std::array AtomicCode{0x7e3c0300, 0x7e3e0280, 0xf0441108, 0x0001141e, 0xbf810000}; + +constexpr std::array 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(VirtualAlloc(nullptr, BlockBytes, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); +#else + block = static_cast(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 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 StoreWrites{{{0u, 0xfu}, {1u, 0x5u}}}; +constexpr std::array TitleWrites{{{0u, 0x1u}}}; +constexpr std::array 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(static_cast(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(static_cast(value), -high - 1, high); + return static_cast(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(static_cast(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(y) * mip.pitchBytes + static_cast(x) * TexelBytesOf(format); +} + +std::vector Initial() { + std::vector bytes(TexelBytes); + for (std::size_t index = 0; index < bytes.size(); ++index) { + bytes[index] = static_cast(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 Expected(const Format& format, std::span 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 BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x01016facu}; +} + +std::array TextureDescriptor(const void* data, std::uint32_t format, std::uint32_t swizzle) { + const auto address = static_cast(reinterpret_cast(data)); + return { + static_cast(address >> 8u), + static_cast((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 code, std::uint32_t format, std::uint32_t swizzle, const std::array& buffer) { + std::vector 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 memory{{{reinterpret_cast(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(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(code.data())); + device.WaitIdle(); + AgcDriver::Graphics::StorageTexture::FlushPending(reinterpret_cast(texels), TexelBytes, nullptr, "test"); + device.WaitIdle(); +} + +void Store(AgcDriver::VulkanDevice& device, std::uint8_t* texels, std::span 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(Input.size() * 4u))); +} + +void CheckTexels(const std::uint8_t* texels, const Format& format, std::span 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(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 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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp new file mode 100644 index 000000000..48e3d4346 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/LookBackScan.cpp @@ -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 +#include +#include +#include +#include +#include + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +alignas(256) constexpr std::array 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 Counter{}; +alignas(256) std::array Aggregates{}; +alignas(256) std::array Pairs{}; +alignas(256) std::array Prefixes{}; + +std::uint32_t Value(std::uint32_t index, std::uint32_t seed) { + return ((index * 2654435761u + seed * 40503u) >> 13u) % 1000u; +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((address >> 32u) & 0xffffu), bytes, 0x31016facu}; +} + +void Run(AgcDriver::VulkanDevice& device, std::uint32_t groups) { + alignas(256) static std::array code{}; + code = ScanCode; + std::vector 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(code.data()); + const std::span words(code); + const std::array 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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ManyBuffers.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ManyBuffers.cpp new file mode 100644 index 000000000..3a26a3ca1 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ManyBuffers.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include + +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 Data{}; +alignas(256) std::array 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 +constexpr std::array 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 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 PushCode = BuildCode(); +alignas(256) constexpr std::array DataCode = BuildCode(); + +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 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(&Data[base]); + Table[buffer * 4u + 0u] = static_cast(address); + Table[buffer * 4u + 1u] = static_cast((address >> 32u) & 0xffffu); + Table[buffer * 4u + 2u] = RangeBytes; + Table[buffer * 4u + 3u] = 0x01016facu; + } + const auto table = reinterpret_cast(Table.data()); + const std::vector userData{static_cast(table), static_cast(table >> 32u)}; + const std::span tableWords(Table.data(), buffers * 4u); + const std::array memory{{{reinterpret_cast(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(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(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; + } +} diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarProgramEnd.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarProgramEnd.cpp index a4362f4c3..335a99a9a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarProgramEnd.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarProgramEnd.cpp @@ -30,6 +30,11 @@ alignas(256) constexpr std::array Code{ 0xe0701000, 0x80010603, 0xbf9b0000, 0xe0701004, 0x80010603, 0xbf810000, }; +alignas(256) constexpr std::array SetkillCode{ + 0x34020084, 0x34060086, 0xe0301000, 0x80000401, 0xbf8c3f70, 0x4a0a0881, 0x7e0c02ff, 0x00000bad, 0xbf060000, 0xbf840007, + 0xbf840003, 0xe0701000, 0x80010503, 0xbf8b0000, 0xe0701000, 0x80010603, 0xbf8b0001, 0xe0701004, 0x80010603, 0xbf810000, +}; + alignas(256) constexpr std::array RoundTowardZeroCode{0xbfa40003, 0xbf810000}; void Fill(std::uint32_t tid, std::uint32_t* words) { words[0] = tid * 0x01010101u + 7u; } @@ -74,7 +79,7 @@ void ExpectRefused(AgcDriver::VulkanDevice& device) { Require(refused, "scalar program end: s_round_mode 3 was translated"); } -void Run(AgcDriver::VulkanDevice& device) { +void Run(AgcDriver::VulkanDevice& device, std::span code) { for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); Output.fill(0xdeadbeefu); std::vector userData(8, 0u); @@ -82,7 +87,6 @@ void Run(AgcDriver::VulkanDevice& device) { const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); std::copy(input.begin(), input.end(), userData.begin()); std::copy(output.begin(), output.end(), userData.begin() + 4); - const std::span code(Code); const std::array memory{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; const auto result = ShaderRecompiler::Recompile(Request(device, code, userData, memory)); device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); @@ -104,7 +108,9 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - Run(*device); + Run(*device, Code); + Check(); + Run(*device, SetkillCode); Check(); ExpectRefused(*device); std::puts("scalar program end tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSop2.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSop2.cpp index cec18794d..ed6a6bbe0 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSop2.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSop2.cpp @@ -23,7 +23,7 @@ constexpr std::uint32_t Results = 64; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array Code{ +alignas(256) constexpr std::array Code{ 0x34020084, 0x34060088, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xbe940380, 0xd7600008, 0x02002904, 0xd7600009, 0x02002905, 0xd760000a, 0x02002906, 0xd760000b, 0x02002907, 0xbe9e0380, 0x871f810b, 0x828c0a08, 0x821e1e1e, 0xd761000a, 0x0200280c, 0x871f810b, 0x858c0a08, 0x821e1e1e, 0xd761000b, @@ -51,22 +51,22 @@ alignas(256) constexpr std::array Code{ 0xd761003e, 0x0200280c, 0xbf060b0b, 0x990c0a08, 0x821e1e1e, 0xd761003f, 0x0200280c, 0xbf060b0b, 0x998c0a08, 0x821e1e1e, 0xd7610040, 0x0200280c, 0xbf060b0b, 0x9a0c0a08, 0x821e1e1e, 0xd7610041, 0x0200280c, 0xbf060b0b, 0x9a8c0a08, 0x821e1e1e, 0xd7610042, 0x0200280c, 0xbf060b0b, 0x9b0c0a08, - 0x821e1e1e, 0xd7610043, 0x0200280c, 0xd7610049, 0x0200281e, 0x80148114, 0xbf0aa014, 0xbf85ff26, - 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, - 0xe0781040, 0x80011a03, 0xe0781050, 0x80011e03, 0xe0781060, 0x80012203, 0xe0781070, 0x80012603, - 0xe0781080, 0x80012a03, 0xe0781090, 0x80012e03, 0xe07810a0, 0x80013203, 0xe07810b0, 0x80013603, - 0xe07810c0, 0x80013a03, 0xe07810d0, 0x80013e03, 0xe07810e0, 0x80014203, 0xe07810f0, 0x80014603, - 0xbf810000, + 0x821e1e1e, 0xd7610043, 0x0200280c, 0x950c0a08, 0x821e1e1e, 0xd7610044, 0x0200280c, 0xd7610045, + 0x0200280d, 0xd7610049, 0x0200281e, 0x80148114, 0xbf0aa014, 0xbf85ff20, 0xe0781000, 0x80010a03, + 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xe0781040, 0x80011a03, + 0xe0781050, 0x80011e03, 0xe0781060, 0x80012203, 0xe0781070, 0x80012603, 0xe0781080, 0x80012a03, + 0xe0781090, 0x80012e03, 0xe07810a0, 0x80013203, 0xe07810b0, 0x80013603, 0xe07810c0, 0x80013a03, + 0xe07810d0, 0x80013e03, 0xe07810e0, 0x80014203, 0xe07810f0, 0x80014603, 0xbf810000, }; constexpr std::uint32_t Rows[32][4] = { {0xbde5c099u, 0x00000001u, 0xc6a53877u, 0xa6233255u}, {0x00000000u, 0x4e5a4cafu, 0x00000000u, 0xd86f17ecu}, - {0x00410003u, 0x80000000u, 0xffffffffu, 0xffff8000u}, - {0x80000000u, 0x00000040u, 0x7fffffffu, 0x0010001cu}, - {0xe05e9899u, 0xb77701e2u, 0x007f0000u, 0x0000001fu}, - {0x7fffffffu, 0xb89c4e56u, 0xff297d0eu, 0xffffffffu}, + {0xfffffffeu, 0x80000000u, 0x00400000u, 0x9abcdef0u}, + {0x00000000u, 0x40000000u, 0x98a5c920u, 0x40000000u}, + {0xfffffffeu, 0xffffffffu, 0x7fffffffu, 0x00200000u}, {0x4b78dc3du, 0x0000003fu, 0x741b324du, 0xb057c162u}, + {0xfffffffeu, 0x80000000u, 0x00000001u, 0x8a887204u}, {0x00008000u, 0x40000000u, 0x61394c10u, 0x0000003fu}, {0xffffffffu, 0x40000000u, 0x7fffffffu, 0x66868695u}, {0x0000003fu, 0x40000000u, 0x617959ceu, 0xffff8000u}, @@ -81,6 +81,7 @@ constexpr std::uint32_t Rows[32][4] = { {0xf129c8c6u, 0x10000000u, 0x00080004u, 0x1ba95a54u}, {0x0001001fu, 0x0960afe9u, 0x7fffffffu, 0x10000000u}, {0xeb07c30du, 0x0010001cu, 0x8b8e8f4eu, 0x00400000u}, + {0x7fffffffu, 0xb89c4e56u, 0xff297d0eu, 0xffffffffu}, {0x007f0000u, 0x0020003fu, 0x74a677c6u, 0x2547f19cu}, {0x00000002u, 0x558d2adbu, 0x216d27a2u, 0x90656c8fu}, {0xa161d909u, 0x69f0441eu, 0x0040003fu, 0xffffffffu}, @@ -90,44 +91,43 @@ constexpr std::uint32_t Rows[32][4] = { {0x43cdc356u, 0xb0ddb48bu, 0xf4f6717cu, 0xa6523995u}, {0x00000000u, 0x00080004u, 0x0010001cu, 0x10000000u}, {0xcc3136d8u, 0x0010001cu, 0xbce240c6u, 0x68916d08u}, - {0x00000040u, 0xb036d9ddu, 0x0001001fu, 0x84a4c93fu}, - {0x0000003fu, 0x0001001fu, 0x7a2c54a0u, 0x7fffffffu} + {0x00000040u, 0xb036d9ddu, 0x0001001fu, 0x84a4c93fu} }; constexpr std::uint32_t Expected[32][64] = { - {0xf7408821u, 0xbde5c099u, 0x00000001u, 0x848af910u, 0xf7408822u, 0xbde5c099u, 0xbde5c099u, 0xc6a53877u, 0xc6a53877u, 0x7b40f8eeu, 0x3940c088u, 0xbdffc799u, 0x001a0700u, 0x84bf0711u, 0x00000000u, 0x0000199fu, 0xffe5f8ffu, 0xa6233255u, 0x7b40f8eeu, 0xa6233254u, 0x3940c088u, 0x00000000u, 0xbdffc799u, 0x59dccdabu, 0x7b5affeeu, 0xfffffffeu, 0x001a0700u, 0x59dccdaau, 0x84bf0711u, 0x59dccdabu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x4c800000u, 0x0000017bu, 0x00000000u, 0x00000000u, 0xffffff7bu, 0x00000000u, 0x00000000u, 0xff800000u, 0x00000000u, 0xff800000u, 0xffffff7bu, 0x0000017bu, 0x00000000u, 0x00000000u, 0x0000035eu, 0x08bf77deu, 0x4270b9a9u, 0xbe3c3adbu, 0xb5d33d3fu, 0xa5014207u, 0x3877c099u, 0xc6a5c099u, 0xc6a5bde5u, 0x935a41eeu, 0x0ecf48deu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x00000000u, 0x00000000u, 0xd86f17ecu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x00000000u, 0x00000007u, 0x00000000u, 0xde7f5fefu, 0x00000000u, 0x96355b43u, 0x00000000u, 0x06104803u, 0xffffffffu, 0x6fdaecbfu, 0xffffffffu, 0xb7b5fb53u, 0xffffffffu, 0x2180a010u, 0xffffffffu, 0x69caa4bcu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002b8u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000001fu}, - {0x00410004u, 0xffffffffu, 0xffff8000u, 0x00410002u, 0x00410004u, 0xffffffffu, 0x00410003u, 0x00410003u, 0xffffffffu, 0xffbefffcu, 0x00000000u, 0x00410003u, 0x00000000u, 0x00410003u, 0x00000000u, 0x000010d5u, 0xffffffffu, 0xffff8000u, 0xffbefffcu, 0x7fff8000u, 0x00000000u, 0x00000000u, 0x00410003u, 0x80007fffu, 0xffbefffcu, 0x7fffffffu, 0x00000000u, 0x00007fffu, 0x00410003u, 0x80007fffu, 0x00000000u, 0x0000006fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x000002b9u, 0x00410004u, 0x00820005u, 0x0104000bu, 0x02080017u, 0x0410002fu, 0xffff0003u, 0xffff0003u, 0xffff0041u, 0x00410002u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x00000001u, 0x7fffffffu, 0x0010001cu, 0xffffffffu, 0x00000001u, 0x80000000u, 0x7fffffffu, 0x7fffffffu, 0x80000000u, 0xffffffffu, 0x80000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000033cu, 0xffffffffu, 0x0010005cu, 0xffffffffu, 0x0010005cu, 0x80000000u, 0x00000040u, 0x80000000u, 0xffefffe3u, 0xffffffffu, 0xffffffffu, 0x00000000u, 0xffefffa3u, 0x00000000u, 0xffefffa3u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000015eu, 0x00000001u, 0x7fffffffu, 0x7fffffffu, 0x7fffffffu, 0x7fffffffu, 0xffff0000u, 0x7fff0000u, 0x7fff8000u, 0x3fffffffu, 0xc0000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xdfdf9898u, 0xe05e9899u, 0xb77701e2u, 0xe0dd9899u, 0xdfdf9899u, 0xe05e9899u, 0x007f0000u, 0x007f0000u, 0xe05e9899u, 0xe0219899u, 0xe0009899u, 0xffdeffffu, 0x1f806766u, 0x1fde6766u, 0x00000000u, 0x0000093fu, 0xe07f9899u, 0xb77701ffu, 0xe0219899u, 0xb77701fdu, 0xe0009899u, 0xb77701e0u, 0xffdeffffu, 0xffffffe2u, 0xffa1ffffu, 0xfffffffdu, 0x1f806766u, 0x4888fe00u, 0x1fde6766u, 0x4888fe02u, 0x00000000u, 0x0000007fu, 0xe05e9899u, 0xb77701e2u, 0xe05e9899u, 0xe05e9899u, 0xb77701e2u, 0xe05e9899u, 0xe05e9899u, 0xb77701e2u, 0x01ffffffu, 0x01ffffffu, 0x00000000u, 0xe05e9899u, 0xe05e9899u, 0xe05e9899u, 0xb77701e2u, 0x000003ffu, 0x20206767u, 0xc13c3132u, 0x81f96264u, 0x0373c4c8u, 0x06688990u, 0x00009899u, 0x007f9899u, 0x007fe05eu, 0x006f4eedu, 0xfff04eedu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x80d682f0u, 0x7fffffffu, 0xb89c4e56u, 0x7f297d0du, 0x80d682f1u, 0xff297d0eu, 0x7fffffffu, 0x7fffffffu, 0xff297d0eu, 0x80d682f1u, 0x00d682f1u, 0x7fffffffu, 0x00000000u, 0x7f297d0eu, 0x00000000u, 0x00001addu, 0xffffffffu, 0xffffffffu, 0x80d682f1u, 0x4763b1a9u, 0x00d682f1u, 0x00000000u, 0x7fffffffu, 0xb89c4e56u, 0x80d682f1u, 0x4763b1a9u, 0x00000000u, 0x00000000u, 0x7f297d0eu, 0xb89c4e56u, 0x00000000u, 0x0000007du, 0xffffc000u, 0x13959fffu, 0x0001ffffu, 0x3959ffffu, 0x0002e271u, 0x0001ffffu, 0x3959ffffu, 0xfffee271u, 0xffffc000u, 0xffffc000u, 0xffffffffu, 0x0001ffffu, 0x0001ffffu, 0x3959ffffu, 0x00000071u, 0x000003ffu, 0x7f297d0fu, 0xff297d0cu, 0xff297d0au, 0xff297d06u, 0xff297cfeu, 0x7d0effffu, 0xff29ffffu, 0xff297fffu, 0x7f94be86u, 0xff94be87u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xd75da9f0u, 0x741b324du, 0xb057c162u, 0xbf940e8au, 0xd75da9f0u, 0x4b78dc3du, 0x4b78dc3du, 0x741b324du, 0x741b324du, 0x3f63ee70u, 0x0b60cc30u, 0xcbfcddbfu, 0x80840182u, 0xc09c118fu, 0x00000000u, 0x0000159fu, 0x7f7bfe7du, 0xb057c17fu, 0x3f63ee70u, 0xb057c15du, 0x0b60cc30u, 0x0000001du, 0xcbfcddbfu, 0x4fa83ebfu, 0xbfe7eff2u, 0xffffffddu, 0x80840182u, 0x4fa83e80u, 0xc09c118fu, 0x4fa83ea2u, 0x00000000u, 0x0000007fu, 0x1b87a000u, 0x0007e96fu, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0xffffe000u, 0xffffe000u, 0xffffffffu, 0x00025bc6u, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0x000003ffu, 0x28a25610u, 0x0b0ceac7u, 0xa1fea341u, 0xcfe21435u, 0x2ba8f61du, 0x324ddc3du, 0x741bdc3du, 0x741b4b78u, 0x223ac85fu, 0x223ac85fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x9ec733efu, 0x00008000u, 0x40000000u, 0x6139cc10u, 0x9ec733f0u, 0x00008000u, 0x00008000u, 0x61394c10u, 0x61394c10u, 0x6139cc10u, 0x00008000u, 0x9ec6b3efu, 0x9ec633efu, 0x9ec633efu, 0x00000000u, 0x0000199fu, 0x6139cc10u, 0x4000003fu, 0x6139cc10u, 0x4000003fu, 0x00008000u, 0x40000000u, 0x9ec6b3efu, 0xffffffc0u, 0xffffffffu, 0xffffffffu, 0x9ec633efu, 0xbfffffc0u, 0x9ec633efu, 0xbfffffc0u, 0x00000000u, 0x0000007fu, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x000002b9u, 0x6138cc10u, 0x613a4c10u, 0x613b4c10u, 0x613d4c10u, 0x61414c10u, 0x4c108000u, 0x61398000u, 0x61390000u, 0x0000309cu, 0x0000309cu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x7fffffffu, 0xffffffffu, 0x40000000u, 0x7ffffffeu, 0x80000000u, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0xffffffffu, 0x80000000u, 0x80000000u, 0xffffffffu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x0000093du, 0xffffffffu, 0x66868695u, 0x80000000u, 0x26868695u, 0x80000000u, 0x00000000u, 0xffffffffu, 0xd979796au, 0x80000000u, 0xbfffffffu, 0x00000000u, 0x9979796au, 0x7fffffffu, 0xd979796au, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000035eu, 0x80000000u, 0x7ffffffdu, 0x7ffffffbu, 0x7ffffff7u, 0x7fffffefu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x7ffffffeu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x9e86a671u, 0x617959ceu, 0xffff8000u, 0x61795a0du, 0x9e86a671u, 0x0000003fu, 0x0000003fu, 0x617959ceu, 0x617959ceu, 0x617959f1u, 0x00000031u, 0x9e86a63fu, 0x9e86a600u, 0x9e86a60eu, 0x00000000u, 0x0000119fu, 0x617959ffu, 0xffff8000u, 0x617959f1u, 0xbfff8000u, 0x00000031u, 0x00000000u, 0x9e86a63fu, 0x40007fffu, 0xfffffff1u, 0xbfffffffu, 0x9e86a600u, 0x00007fffu, 0x9e86a60eu, 0x40007fffu, 0x00000000u, 0x0000007fu, 0x000fc000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00010000u, 0xffffc000u, 0xffffc000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00010000u, 0x000002b9u, 0x6179598fu, 0x61795a4cu, 0x61795acau, 0x61795bc6u, 0x61795dbeu, 0x59ce003fu, 0x6179003fu, 0x61790000u, 0x00000017u, 0x00000017u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x3bfa6abbu, 0xc4069545u, 0x80000000u, 0xc4079545u, 0x3bfa6abbu, 0xc4069545u, 0x00010000u, 0x00010000u, 0xc4069545u, 0xc4079545u, 0x00010000u, 0x3bf96abau, 0x3bf86abau, 0x3bf86abau, 0x00000000u, 0x000010dfu, 0xc4079545u, 0x80000040u, 0xc4079545u, 0x80000040u, 0x00010000u, 0x00000040u, 0x3bf96abau, 0x7fffffffu, 0xffffffffu, 0xffffffffu, 0x3bf86abau, 0x7fffffbfu, 0x3bf86abau, 0x7fffffbfu, 0x00000000u, 0x0000007fu, 0x00200000u, 0x00000800u, 0x00000800u, 0x00000800u, 0x00000002u, 0x00000800u, 0x00000800u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x3bfa6abbu, 0xc4089545u, 0xc40a9545u, 0xc40e9545u, 0xc4169545u, 0x95450000u, 0xc4060000u, 0xc4060001u, 0x0000c406u, 0xffffc406u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x10d822c6u, 0x015c33b2u, 0x00010000u, 0x13908a2au, 0x10d822c6u, 0x015c33b2u, 0x015c33b2u, 0x12345678u, 0x12345678u, 0x136865cau, 0x12204448u, 0xfeb7de7du, 0xec838805u, 0xec979a35u, 0x00000000u, 0x0000007fu, 0x137c77fau, 0x00010000u, 0x136865cau, 0x00010000u, 0x12204448u, 0x00000000u, 0xfeb7de7du, 0xfffeffffu, 0xffebedcfu, 0xffffffffu, 0xec838805u, 0xfffeffffu, 0xec979a35u, 0xfffeffffu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x59e00000u, 0x0000048du, 0x00000000u, 0x00000000u, 0x0000048du, 0x00000000u, 0x00000000u, 0xfffc0000u, 0x00000000u, 0xfffc0000u, 0x0000048du, 0x0000048du, 0x00000000u, 0x00000000u, 0x0000035eu, 0x10d822c6u, 0x25c4e0a2u, 0x4a2d8d92u, 0x92fee772u, 0x24a19b32u, 0x33b25678u, 0x015c5678u, 0x015c1234u, 0x0018c2d2u, 0x0018c2d2u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000023fu}, - {0x2a74915au, 0x0000ffffu, 0x00010000u, 0x2a769158u, 0x2a74915au, 0x0000ffffu, 0x0000ffffu, 0x2a759159u, 0x2a759159u, 0x2a756ea6u, 0x2a750000u, 0xffff9159u, 0xd58a0000u, 0xd58a9159u, 0x00000000u, 0x0000007fu, 0x2a75ffffu, 0x4a0fe75du, 0x2a756ea6u, 0x4a0ee75du, 0x2a750000u, 0x4a0ee75du, 0xffff9159u, 0xffffffffu, 0xffff6ea6u, 0xfffeffffu, 0xd58a0000u, 0xb5f018a2u, 0xd58a9159u, 0xb5f118a2u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000218u, 0x2a74915au, 0x54ec22b1u, 0xa9d74563u, 0x53ad8ac7u, 0xa75a158fu, 0xffff9159u, 0x00009159u, 0x00002a75u, 0x00002a75u, 0x00002a75u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000027fu}, - {0x0000fffdu, 0x0000ffffu, 0xe4cc4132u, 0x00010000u, 0x0000fffeu, 0x00000001u, 0x00000001u, 0x0000ffffu, 0x0000ffffu, 0x0000fffeu, 0x0000fffeu, 0xffffffffu, 0xffff0000u, 0xffff0001u, 0x00000000u, 0x0000087fu, 0x0000ffffu, 0xe4cc413fu, 0x0000fffeu, 0xe4cc410du, 0x0000fffeu, 0xe4cc4100u, 0xffffffffu, 0xfffffff2u, 0xfffffffeu, 0xffffffcdu, 0xffff0000u, 0x1b33bec0u, 0xffff0001u, 0x1b33bef2u, 0x00000000u, 0x0000007fu, 0x0001fffeu, 0xc9988264u, 0x00007fffu, 0x00007fffu, 0x72662099u, 0x00007fffu, 0x00007fffu, 0xf2662099u, 0xfffffffeu, 0xfffffffeu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x0000fffeu, 0x0001ffffu, 0x0003fffdu, 0x0007fff9u, 0x000ffff1u, 0x0001ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xad7bd36cu, 0x43892dfcu, 0x00000005u, 0xd996888bu, 0xad7bd36du, 0x960d5a8fu, 0x43892dfcu, 0x43892dfcu, 0x960d5a8fu, 0xd5847773u, 0x41802570u, 0x6bfbadfcu, 0x28728000u, 0x2a7b888cu, 0x00000000u, 0x00001adfu, 0xd78d7fffu, 0x0021001fu, 0xd5847773u, 0x0021001au, 0x41802570u, 0x00000000u, 0x6bfbadfcu, 0xffdeffe5u, 0xfdf6f773u, 0xfffffffau, 0x28728000u, 0xffdeffe0u, 0x2a7b888cu, 0xffdeffe5u, 0x00000000u, 0x0000007fu, 0x96fe0000u, 0x0002a1c4u, 0x00008712u, 0x000a8712u, 0x00000000u, 0x00008712u, 0x000a8712u, 0x00000000u, 0xffff8000u, 0xffff8000u, 0xffffffffu, 0x00000712u, 0x00000712u, 0x00000712u, 0x00000000u, 0x000003ffu, 0x52842c93u, 0x1d1fb687u, 0xa432127fu, 0xb256ca6fu, 0xcea03a4fu, 0x5a8f2dfcu, 0x960d2dfcu, 0x960d4389u, 0x2795e6ccu, 0xe40cb8d0u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xd0d2b3b9u, 0x2f4d4c86u, 0x00000040u, 0x2f6d4cc5u, 0xd0d2b3b9u, 0x0020003fu, 0x0020003fu, 0x2f4d4c86u, 0x2f4d4c86u, 0x2f6d4cb9u, 0x00200039u, 0xd0b2b37fu, 0xd092b340u, 0xd092b346u, 0x00000000u, 0x0000119fu, 0x2f6d4cbfu, 0x7b121dc5u, 0x2f6d4cb9u, 0x7b121d85u, 0x00200039u, 0x7b121d85u, 0xd0b2b37fu, 0xffffffffu, 0xfffffff9u, 0xffffffbfu, 0xd092b340u, 0x84ede23au, 0xd092b346u, 0x84ede27au, 0x00000000u, 0x0000007fu, 0x08000fc0u, 0xc4877140u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0xffffffc0u, 0xffffffc0u, 0xffffffffu, 0x00008000u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0x000003ffu, 0x2f2d4c47u, 0x2f8d4d04u, 0x2fcd4d82u, 0x304d4e7eu, 0x314d5076u, 0x4c86003fu, 0x2f4d003fu, 0x2f4d0020u, 0x0005e9b5u, 0x0005e9b5u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xfb5fed18u, 0x00000001u, 0x5b8e8fb2u, 0x04a012e9u, 0xfb5fed19u, 0x00000001u, 0x00000001u, 0x04a012e8u, 0x04a012e8u, 0x04a012e9u, 0x00000001u, 0xfb5fed17u, 0xfb5fed16u, 0xfb5fed16u, 0x00000000u, 0x0000199fu, 0x04a012e9u, 0x5bcf8fb3u, 0x04a012e9u, 0x5bcf8fb1u, 0x00000001u, 0x5b8e8fb0u, 0xfb5fed17u, 0xffbefffeu, 0xffffffffu, 0xfffffffdu, 0xfb5fed16u, 0xa430704cu, 0xfb5fed16u, 0xa430704eu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000100u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000100u, 0x00000000u, 0x00000100u, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x000002b9u, 0x04a012e7u, 0x04a012eau, 0x04a012ecu, 0x04a012f0u, 0x04a012f8u, 0x12e80001u, 0x04a00001u, 0x04a00000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x0baccdb2u, 0xf453324eu, 0xffff8000u, 0xf453324eu, 0x0baccdb2u, 0xf453324eu, 0x00000000u, 0x00000000u, 0xf453324eu, 0xf453324eu, 0x00000000u, 0x0baccdb1u, 0x0baccdb1u, 0x0baccdb1u, 0x00000000u, 0x000010d7u, 0xf453324eu, 0xffff8040u, 0xf453324eu, 0xffff8040u, 0x00000000u, 0x00000040u, 0x0baccdb1u, 0x00007fffu, 0xffffffffu, 0xffffffffu, 0x0baccdb1u, 0x00007fbfu, 0x0baccdb1u, 0x00007fbfu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00100000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x000002b9u, 0x0baccdb2u, 0xf453324eu, 0xf453324eu, 0xf453324eu, 0xf453324eu, 0x324e0000u, 0xf4530000u, 0xf4530000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xf121c8c2u, 0x00080004u, 0x1ba95a54u, 0xf131c8cau, 0xf121c8c2u, 0xf129c8c6u, 0x00080004u, 0x00080004u, 0xf129c8c6u, 0xf121c8c2u, 0xf121c8c2u, 0xffffffffu, 0x0ed63739u, 0x0ede373du, 0x00000000u, 0x0000013fu, 0xf129c8c6u, 0x1ba95a54u, 0xf121c8c2u, 0x0ba95a54u, 0xf121c8c2u, 0x00000000u, 0xffffffffu, 0xf456a5abu, 0xfff7fffbu, 0xefffffffu, 0x0ed63739u, 0xe456a5abu, 0x0ede373du, 0xf456a5abu, 0x00000000u, 0x0000007fu, 0x129c8c60u, 0x0000000fu, 0x0f129c8cu, 0x0f129c8cu, 0x01000000u, 0xff129c8cu, 0x0f129c8cu, 0x01000000u, 0x000003f0u, 0x000003f0u, 0x00000000u, 0xffffff8cu, 0x0000008cu, 0x0000008cu, 0x00000000u, 0x000003ffu, 0x0ede373eu, 0xe25b9190u, 0xc4af231cu, 0x89564634u, 0x12a48c64u, 0x0004c8c6u, 0x0008c8c6u, 0x0008f129u, 0x00078952u, 0xffff894eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x80010020u, 0x7fffffffu, 0x10000000u, 0x8001001eu, 0x80010020u, 0x0001001fu, 0x0001001fu, 0x7fffffffu, 0x7fffffffu, 0x7ffeffe0u, 0x00000000u, 0x8001001fu, 0x80000000u, 0x8001001fu, 0x00000000u, 0x00001597u, 0x7fffffffu, 0x1960afe9u, 0x7ffeffe0u, 0x1960afe9u, 0x00000000u, 0x0960afe9u, 0x8001001fu, 0xefffffffu, 0xfffeffe0u, 0xffffffffu, 0x80000000u, 0xe69f5016u, 0x8001001fu, 0xe69f5016u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000218u, 0x7ffeffe0u, 0x8002003du, 0x8004007bu, 0x800800f7u, 0x801001efu, 0xffff001fu, 0x7fff001fu, 0x7fff0001u, 0x0000800fu, 0x0000800fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x5f7933bfu, 0x8b8e8f4eu, 0x00400000u, 0x7696525bu, 0x5f7933bfu, 0x8b8e8f4eu, 0x8b8e8f4eu, 0xeb07c30du, 0xeb07c30du, 0x60894c43u, 0x60014001u, 0xff77f3bdu, 0x147030b0u, 0x9f76b3bcu, 0x00000000u, 0x0000047fu, 0xeb8fcf4fu, 0x0050001cu, 0x60894c43u, 0x0050001cu, 0x60014001u, 0x0010001cu, 0xff77f3bdu, 0xffbfffffu, 0x74f97cf3u, 0xffffffffu, 0x147030b0u, 0xffafffe3u, 0x9f76b3bcu, 0xffafffe3u, 0x00000000u, 0x0000007fu, 0xf0c34000u, 0x00073ac1u, 0x0003ac1fu, 0x0073ac1fu, 0x00000040u, 0xffffac1fu, 0x0073ac1fu, 0x00000040u, 0x07ffc000u, 0x07ffc000u, 0x00000000u, 0xffffec1fu, 0x00002c1fu, 0x00002c1fu, 0x00000000u, 0x000003ffu, 0x5f7933bfu, 0x619e1568u, 0x37ad9b82u, 0xe3cca7b6u, 0x3c0ac01eu, 0x8f4ec30du, 0x8b8ec30du, 0x8b8eeb07u, 0x802018c7u, 0x0989c66cu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x8bd8883au, 0x74a677c6u, 0x2547f19cu, 0x752577c6u, 0x8bd8883au, 0x007f0000u, 0x007f0000u, 0x74a677c6u, 0x74a677c6u, 0x74d977c6u, 0x00590000u, 0x8b7f8839u, 0x8b008839u, 0x8b268839u, 0x00000000u, 0x0000119fu, 0x74ff77c6u, 0x2567f1bfu, 0x74d977c6u, 0x2567f1a3u, 0x00590000u, 0x00200023u, 0x8b7f8839u, 0xdab80e7fu, 0xffd9ffffu, 0xffffffe3u, 0x8b008839u, 0xda980e40u, 0x8b268839u, 0xda980e5cu, 0x00000000u, 0x0000007fu, 0x1fc00000u, 0x08000fc0u, 0x0001fc00u, 0xfc01fc00u, 0x00008000u, 0x0001fc00u, 0xfc01fc00u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0001fc00u, 0x0001fc00u, 0xfc01fc00u, 0x00000000u, 0x000003ffu, 0x742777c6u, 0x75a477c6u, 0x76a277c6u, 0x789e77c6u, 0x7c9677c6u, 0x77c60000u, 0x74a60000u, 0x74a6007fu, 0x0039de95u, 0x0039de95u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xde92d85fu, 0x00000002u, 0x558d2adbu, 0x216d27a4u, 0xde92d860u, 0x00000002u, 0x00000002u, 0x216d27a2u, 0x216d27a2u, 0x216d27a0u, 0x00000000u, 0xde92d85fu, 0xde92d85du, 0xde92d85fu, 0x00000000u, 0x00001997u, 0x216d27a2u, 0xd5ed6edfu, 0x216d27a0u, 0xc5e84654u, 0x00000000u, 0x45880250u, 0xde92d85fu, 0x7f9fbbfbu, 0xfffffffdu, 0xeffad774u, 0xde92d85du, 0x2a129120u, 0xde92d85fu, 0x3a17b9abu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000008u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x0000000cu, 0x00000000u, 0x0000000cu, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x000002b9u, 0x216d27a0u, 0x216d27a6u, 0x216d27aau, 0x216d27b2u, 0x216d27c2u, 0x27a20002u, 0x216d0002u, 0x216d0000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0xa121d8c9u, 0xa161d909u, 0x69f0441eu, 0xa1a1d948u, 0xa121d8cau, 0xa161d909u, 0x0040003fu, 0x0040003fu, 0xa161d909u, 0xa121d936u, 0xa121d900u, 0xffffffc9u, 0x5e9e26c0u, 0x5ede26c9u, 0x00000000u, 0x0000093fu, 0xa161d93fu, 0xffffffffu, 0xa121d936u, 0x960fbbe1u, 0xa121d900u, 0x00000000u, 0xffffffc9u, 0x69f0441eu, 0xffbffff6u, 0x960fbbe1u, 0x5e9e26c0u, 0x00000000u, 0x5ede26c9u, 0x69f0441eu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000035eu, 0x5ede2736u, 0x4303b251u, 0x85c76463u, 0x0b4ec887u, 0x165d90cfu, 0x003fd909u, 0x0040d909u, 0x0040a161u, 0x0028589du, 0xffe8585eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0x7aa2868bu, 0x00000021u, 0x00000040u, 0x7aa286cdu, 0x7aa2868bu, 0x00000021u, 0x00000021u, 0x7aa286acu, 0x7aa286acu, 0x7aa2868du, 0x7aa2868cu, 0xfffffffeu, 0x855d7952u, 0x855d7972u, 0x00000000u, 0x0000007fu, 0x7aa286adu, 0x40000040u, 0x7aa2868du, 0x40000040u, 0x7aa2868cu, 0x40000000u, 0xfffffffeu, 0xffffffbfu, 0xffffffdfu, 0xffffffffu, 0x855d7952u, 0xbfffffbfu, 0x855d7972u, 0xbfffffbfu, 0x00000000u, 0x0000007fu, 0x00000000u, 0xf5450d58u, 0x3d514356u, 0x20000000u, 0x00000000u, 0x3d514356u, 0x20000000u, 0x00000000u, 0x00001ffeu, 0x00000000u, 0xfffffffeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x7aa2868bu, 0xf5450d79u, 0xea8a1ad1u, 0xd5143581u, 0xaa286ae1u, 0x002186acu, 0x000086acu, 0x00007aa2u, 0x0000000fu, 0x0000000fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002ffu}, - {0x7fc3987cu, 0x0010001cu, 0x10000000u, 0x7fe398b4u, 0x7fc3987cu, 0x0010001cu, 0x0010001cu, 0x7fd39898u, 0x7fd39898u, 0x7fc39884u, 0x7fc39880u, 0xfffffffbu, 0x802c6763u, 0x803c677bu, 0x00000000u, 0x0000007fu, 0x7fd3989cu, 0x3e1c5d3au, 0x7fc39884u, 0x3e1c5d3au, 0x7fc39880u, 0x2e1c5d3au, 0xfffffffbu, 0xefffffffu, 0xffefffe7u, 0xffffffffu, 0x802c6763u, 0xc1e3a2c5u, 0x803c677bu, 0xc1e3a2c5u, 0x00000000u, 0x0000007fu, 0x80000000u, 0xa7fd3989u, 0x00000007u, 0xe1c5d3a7u, 0x00000002u, 0x00000007u, 0xe1c5d3a7u, 0x00000002u, 0xf0000000u, 0xf0000000u, 0x000fffffu, 0x00000007u, 0x00000007u, 0x0000d3a7u, 0x00000000u, 0x000003ffu, 0x7fc3987cu, 0xffb7314cu, 0xff5e627cu, 0xfeacc4dcu, 0xfd49899cu, 0x001c9898u, 0x00109898u, 0x00107fd3u, 0x0007fd47u, 0x0007fd47u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002ffu}, - {0x0020003au, 0x00000005u, 0x0010001cu, 0x00200044u, 0x0020003au, 0x00000005u, 0x00000005u, 0x0020003fu, 0x0020003fu, 0x0020003au, 0x0020003au, 0xffffffffu, 0xffdfffc0u, 0xffdfffc5u, 0x00000000u, 0x0000007fu, 0x0020003fu, 0xa9fd1cfdu, 0x0020003au, 0xa9ed1cf5u, 0x0020003au, 0xa9ed1ce1u, 0xffffffffu, 0xffffffebu, 0xfffffffau, 0xffeffff7u, 0xffdfffc0u, 0x5602e302u, 0xffdfffc5u, 0x5612e30au, 0x00000000u, 0x0000007fu, 0x040007e0u, 0x3fa39d20u, 0x00010001u, 0x48010001u, 0x054fe8e7u, 0x00010001u, 0x48010001u, 0xfd4fe8e7u, 0xffffffe0u, 0xffffffe0u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x0020003au, 0x00400083u, 0x00800101u, 0x010001fdu, 0x020003f5u, 0x0005003fu, 0x0000003fu, 0x00000020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x4ed751d9u, 0x43cdc356u, 0xb0ddb48bu, 0x38c434d2u, 0x4ed751dau, 0xf4f6717cu, 0x43cdc356u, 0x43cdc356u, 0xf4f6717cu, 0xb73bb22au, 0x03098202u, 0x4bcdcfd7u, 0x08000c81u, 0x48c44dd5u, 0x00000000u, 0x000018dfu, 0xf7fff37eu, 0xb6dfbd9fu, 0xb73bb22au, 0x168f8d1eu, 0x03098202u, 0x108d840au, 0x4bcdcfd7u, 0xf9fdf6ebu, 0xbf3bbeabu, 0x5fafcf7eu, 0x08000c81u, 0x49204260u, 0x48c44dd5u, 0xe97072e1u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x60000000u, 0x00000004u, 0x0000000bu, 0x00000000u, 0x00000004u, 0xfffffffbu, 0xffffffffu, 0xf0000000u, 0x00000000u, 0xf0000000u, 0x00000004u, 0x00000004u, 0x0000000bu, 0x00000000u, 0x000003ffu, 0x4ed751dau, 0x7c91f828u, 0x042d7ed4u, 0x13648c2cu, 0x31d2a6dcu, 0x717cc356u, 0xf4f6c356u, 0xf4f643cdu, 0x40e163f6u, 0xfd13a0a0u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xffefffe4u, 0x0010001cu, 0x10000000u, 0x0010001cu, 0xffefffe4u, 0x00000000u, 0x00000000u, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x00000000u, 0xffefffe3u, 0xffefffe3u, 0xffefffe3u, 0x00000000u, 0x00001197u, 0x0010001cu, 0x10080004u, 0x0010001cu, 0x10080004u, 0x00000000u, 0x00080004u, 0xffefffe3u, 0xefffffffu, 0xffffffffu, 0xffffffffu, 0xffefffe3u, 0xeff7fffbu, 0xffefffe3u, 0xeff7fffbu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x40000000u, 0x00000000u, 0x00800040u, 0x00000000u, 0x00000000u, 0x00800040u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000040u, 0x00000000u, 0x000002b9u, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x001c0000u, 0x00100000u, 0x00100000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x0f4ef612u, 0xbce240c6u, 0x68916d08u, 0x8913779eu, 0x0f4ef612u, 0xbce240c6u, 0xbce240c6u, 0xcc3136d8u, 0xcc3136d8u, 0x70d3761eu, 0x40113618u, 0xcf3dbff9u, 0x030c8921u, 0x8f2c89e1u, 0x00000000u, 0x0000007fu, 0xfcf376deu, 0x68916d1cu, 0x70d3761eu, 0x68816d14u, 0x40113618u, 0x00000014u, 0xcf3dbff9u, 0x977e92ffu, 0x73dfff3fu, 0xffeffff7u, 0x030c8921u, 0x976e92e3u, 0x8f2c89e1u, 0x977e92ebu, 0x00000000u, 0x0000007fu, 0x0c4db600u, 0x04000733u, 0x0330c4dbu, 0x7330c4dbu, 0x00004000u, 0xff30c4dbu, 0x7330c4dbu, 0x00004000u, 0x3fffffc0u, 0x3fffffc0u, 0x00000000u, 0xff30c4dbu, 0x0330c4dbu, 0x7330c4dbu, 0x00004000u, 0x000003ffu, 0x0f4ef612u, 0x5544ae76u, 0xeda71c26u, 0x1e6bf786u, 0x7ff5ae46u, 0x40c636d8u, 0xbce236d8u, 0xbce2cc31u, 0x96a89b63u, 0x0d9523c5u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003ffu}, - {0xffff0020u, 0x00000040u, 0xb036d9ddu, 0x0001005fu, 0xffff0021u, 0x00000040u, 0x00000040u, 0x0001001fu, 0x0001001fu, 0x0001005fu, 0x00000040u, 0xfffeffe0u, 0xfffeffa0u, 0xfffeffa0u, 0x00000000u, 0x0000199fu, 0x0001005fu, 0xb4b6d9ffu, 0x0001005fu, 0x349210e2u, 0x00000040u, 0x301210c0u, 0xfffeffe0u, 0xfb7fffddu, 0xffffffffu, 0x7fdb36e2u, 0xfffeffa0u, 0x4b492600u, 0xfffeffa0u, 0xcb6def1du, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000020u, 0x00000000u, 0x606db3bau, 0x00000001u, 0x00000000u, 0x606db3bau, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002b8u, 0x0000ffdfu, 0x0001009fu, 0x0001011fu, 0x0001021fu, 0x0001041fu, 0x001f0040u, 0x00010040u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu}, - {0x85d3ab9eu, 0x0000003fu, 0x0001001fu, 0x7a2c54dfu, 0x85d3ab9fu, 0x0000003fu, 0x0000003fu, 0x7a2c54a0u, 0x7a2c54a0u, 0x7a2c549fu, 0x0000001fu, 0x85d3ab7fu, 0x85d3ab40u, 0x85d3ab60u, 0x00000000u, 0x0000199fu, 0x7a2c54bfu, 0x7fffffffu, 0x7a2c549fu, 0x7ffeffe0u, 0x0000001fu, 0x00000000u, 0x85d3ab7fu, 0x8001001fu, 0xffffffdfu, 0xfffeffe0u, 0x85d3ab40u, 0x80000000u, 0x85d3ab60u, 0x8001001fu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x0000003fu, 0x0000003fu, 0x0001001fu, 0x00000000u, 0x0000003fu, 0x0001001fu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0xffffffffu, 0x0000003fu, 0x0000003fu, 0x0001001fu, 0x00000000u, 0x000003ffu, 0x7a2c5461u, 0x7a2c551eu, 0x7a2c559cu, 0x7a2c5698u, 0x7a2c5890u, 0x54a0003fu, 0x7a2c003fu, 0x7a2c0000u, 0x0000001eu, 0x0000001eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000021fu} + {0xf7408821u, 0xbde5c099u, 0x00000001u, 0x848af910u, 0xf7408822u, 0xbde5c099u, 0xbde5c099u, 0xc6a53877u, 0xc6a53877u, 0x7b40f8eeu, 0x3940c088u, 0xbdffc799u, 0x001a0700u, 0x84bf0711u, 0x00000000u, 0x0000199fu, 0xffe5f8ffu, 0xa6233255u, 0x7b40f8eeu, 0xa6233254u, 0x3940c088u, 0x00000000u, 0xbdffc799u, 0x59dccdabu, 0x7b5affeeu, 0xfffffffeu, 0x001a0700u, 0x59dccdaau, 0x84bf0711u, 0x59dccdabu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x4c800000u, 0x0000017bu, 0x00000000u, 0x00000000u, 0xffffff7bu, 0x00000000u, 0x00000000u, 0xff800000u, 0x00000000u, 0xff800000u, 0xffffff7bu, 0x0000017bu, 0x00000000u, 0x00000000u, 0x0000035eu, 0x08bf77deu, 0x4270b9a9u, 0xbe3c3adbu, 0xb5d33d3fu, 0xa5014207u, 0x3877c099u, 0xc6a5c099u, 0xc6a5bde5u, 0x935a41eeu, 0x0ecf48deu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007feu}, + {0x00000000u, 0x00000000u, 0xd86f17ecu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x00000000u, 0x00000007u, 0x00000000u, 0xde7f5fefu, 0x00000000u, 0x96355b43u, 0x00000000u, 0x06104803u, 0xffffffffu, 0x6fdaecbfu, 0xffffffffu, 0xb7b5fb53u, 0xffffffffu, 0x2180a010u, 0xffffffffu, 0x69caa4bcu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x4e5a4cafu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002b8u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000003eu}, + {0xffbffffeu, 0x00400000u, 0x9abcdef0u, 0x003ffffeu, 0xffbffffeu, 0xfffffffeu, 0x00400000u, 0x00400000u, 0xfffffffeu, 0xffbffffeu, 0xffbffffeu, 0xffffffffu, 0x00000001u, 0x00400001u, 0x00000000u, 0x0000013fu, 0xfffffffeu, 0x9abcdef0u, 0xffbffffeu, 0x1abcdef0u, 0xffbffffeu, 0x00000000u, 0xffffffffu, 0xe543210fu, 0xffbfffffu, 0x7fffffffu, 0x00000001u, 0x6543210fu, 0x00400001u, 0xe543210fu, 0x00000000u, 0x0000007fu, 0xfffffffeu, 0x80000000u, 0xfffffffeu, 0xfffffffeu, 0x80000000u, 0xfffffffeu, 0xfffffffeu, 0x80000000u, 0x3fffffffu, 0xffffffffu, 0x3fffffffu, 0xfffffffeu, 0xfffffffeu, 0xfffffffeu, 0x80000000u, 0x000003ffu, 0x00400002u, 0x003ffffcu, 0x003ffff8u, 0x003ffff0u, 0x003fffe0u, 0x0000fffeu, 0x0040fffeu, 0x0040ffffu, 0x003fffffu, 0xffffffffu, 0xfffffffeu, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0x675a36e0u, 0x98a5c920u, 0x40000000u, 0x98a5c920u, 0x675a36e0u, 0x98a5c920u, 0x00000000u, 0x00000000u, 0x98a5c920u, 0x98a5c920u, 0x00000000u, 0x675a36dfu, 0x675a36dfu, 0x675a36dfu, 0x00000000u, 0x000010d7u, 0x98a5c920u, 0x40000000u, 0x98a5c920u, 0x00000000u, 0x00000000u, 0x00000000u, 0x675a36dfu, 0xffffffffu, 0xffffffffu, 0xbfffffffu, 0x675a36dfu, 0xbfffffffu, 0x675a36dfu, 0xffffffffu, 0x00000000u, 0x0000006fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x000000b9u, 0x675a36e0u, 0x98a5c920u, 0x98a5c920u, 0x98a5c920u, 0x98a5c920u, 0xc9200000u, 0x98a50000u, 0x98a50000u, 0x00000000u, 0x00000000u, 0x40000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x7fffffffu, 0x7fffffffu, 0x00200000u, 0x7ffffffdu, 0x7fffffffu, 0xfffffffeu, 0x7fffffffu, 0x7fffffffu, 0xfffffffeu, 0x80000001u, 0x80000000u, 0xfffffffeu, 0x00000000u, 0x7ffffffeu, 0x00000000u, 0x0000033du, 0xffffffffu, 0xffffffffu, 0x80000001u, 0xffdfffffu, 0x80000000u, 0xffdfffffu, 0xfffffffeu, 0xffffffffu, 0x80000001u, 0xffdfffffu, 0x00000000u, 0x00000000u, 0x7ffffffeu, 0x00200000u, 0x00000000u, 0x0000007du, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000001u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000001u, 0x00000000u, 0x000001ffu, 0x7fffffffu, 0x7ffffffbu, 0x7ffffff7u, 0x7fffffefu, 0x7fffffdfu, 0xfffffffeu, 0x7ffffffeu, 0x7fffffffu, 0x7ffffffeu, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xd75da9f0u, 0x741b324du, 0xb057c162u, 0xbf940e8au, 0xd75da9f0u, 0x4b78dc3du, 0x4b78dc3du, 0x741b324du, 0x741b324du, 0x3f63ee70u, 0x0b60cc30u, 0xcbfcddbfu, 0x80840182u, 0xc09c118fu, 0x00000000u, 0x0000159fu, 0x7f7bfe7du, 0xb057c17fu, 0x3f63ee70u, 0xb057c15du, 0x0b60cc30u, 0x0000001du, 0xcbfcddbfu, 0x4fa83ebfu, 0xbfe7eff2u, 0xffffffddu, 0x80840182u, 0x4fa83e80u, 0xc09c118fu, 0x4fa83ea2u, 0x00000000u, 0x0000007fu, 0x1b87a000u, 0x0007e96fu, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0xffffe000u, 0xffffe000u, 0xffffffffu, 0x00025bc6u, 0x00025bc6u, 0x01fa5bc6u, 0x00000000u, 0x000003ffu, 0x28a25610u, 0x0b0ceac7u, 0xa1fea341u, 0xcfe21435u, 0x2ba8f61du, 0x324ddc3du, 0x741bdc3du, 0x741b4b78u, 0x223ac85fu, 0x223ac85fu, 0x01fa5bc6u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xfffffffdu, 0x00000001u, 0x8a887204u, 0xffffffffu, 0xfffffffdu, 0xfffffffeu, 0x00000001u, 0x00000001u, 0xfffffffeu, 0xffffffffu, 0xfffffffeu, 0xfffffffeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000013cu, 0xffffffffu, 0x8a887204u, 0xffffffffu, 0x0a887204u, 0xfffffffeu, 0x00000000u, 0xfffffffeu, 0xf5778dfbu, 0xffffffffu, 0x7fffffffu, 0x00000000u, 0x75778dfbu, 0x00000000u, 0xf5778dfbu, 0x00000000u, 0x0000007fu, 0xfffffffcu, 0x00000001u, 0x7fffffffu, 0x7fffffffu, 0x40000000u, 0xffffffffu, 0x7fffffffu, 0xc0000000u, 0x7ffffffeu, 0xfffffffeu, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x00000003u, 0xfffffffdu, 0xfffffff9u, 0xfffffff1u, 0xffffffe1u, 0x0001fffeu, 0x0000fffeu, 0x0000ffffu, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007feu}, + {0x9ec733efu, 0x00008000u, 0x40000000u, 0x6139cc10u, 0x9ec733f0u, 0x00008000u, 0x00008000u, 0x61394c10u, 0x61394c10u, 0x6139cc10u, 0x00008000u, 0x9ec6b3efu, 0x9ec633efu, 0x9ec633efu, 0x00000000u, 0x0000199fu, 0x6139cc10u, 0x4000003fu, 0x6139cc10u, 0x4000003fu, 0x00008000u, 0x40000000u, 0x9ec6b3efu, 0xffffffc0u, 0xffffffffu, 0xffffffffu, 0x9ec633efu, 0xbfffffc0u, 0x9ec633efu, 0xbfffffc0u, 0x00000000u, 0x0000007fu, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00004000u, 0x000002b9u, 0x6138cc10u, 0x613a4c10u, 0x613b4c10u, 0x613d4c10u, 0x61414c10u, 0x4c108000u, 0x61398000u, 0x61390000u, 0x0000309cu, 0x0000309cu, 0x00000000u, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x7fffffffu, 0xffffffffu, 0x40000000u, 0x7ffffffeu, 0x80000000u, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0xffffffffu, 0x80000000u, 0x80000000u, 0xffffffffu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x0000093du, 0xffffffffu, 0x66868695u, 0x80000000u, 0x26868695u, 0x80000000u, 0x00000000u, 0xffffffffu, 0xd979796au, 0x80000000u, 0xbfffffffu, 0x00000000u, 0x9979796au, 0x7fffffffu, 0xd979796au, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000035eu, 0x80000000u, 0x7ffffffdu, 0x7ffffffbu, 0x7ffffff7u, 0x7fffffefu, 0xffffffffu, 0x7fffffffu, 0x7fffffffu, 0x7ffffffeu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007feu}, + {0x9e86a671u, 0x617959ceu, 0xffff8000u, 0x61795a0du, 0x9e86a671u, 0x0000003fu, 0x0000003fu, 0x617959ceu, 0x617959ceu, 0x617959f1u, 0x00000031u, 0x9e86a63fu, 0x9e86a600u, 0x9e86a60eu, 0x00000000u, 0x0000119fu, 0x617959ffu, 0xffff8000u, 0x617959f1u, 0xbfff8000u, 0x00000031u, 0x00000000u, 0x9e86a63fu, 0x40007fffu, 0xfffffff1u, 0xbfffffffu, 0x9e86a600u, 0x00007fffu, 0x9e86a60eu, 0x40007fffu, 0x00000000u, 0x0000007fu, 0x000fc000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00010000u, 0xffffc000u, 0xffffc000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00010000u, 0x000002b9u, 0x6179598fu, 0x61795a4cu, 0x61795acau, 0x61795bc6u, 0x61795dbeu, 0x59ce003fu, 0x6179003fu, 0x61790000u, 0x00000017u, 0x00000017u, 0x00000000u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x3bfa6abbu, 0xc4069545u, 0x80000000u, 0xc4079545u, 0x3bfa6abbu, 0xc4069545u, 0x00010000u, 0x00010000u, 0xc4069545u, 0xc4079545u, 0x00010000u, 0x3bf96abau, 0x3bf86abau, 0x3bf86abau, 0x00000000u, 0x000010dfu, 0xc4079545u, 0x80000040u, 0xc4079545u, 0x80000040u, 0x00010000u, 0x00000040u, 0x3bf96abau, 0x7fffffffu, 0xffffffffu, 0xffffffffu, 0x3bf86abau, 0x7fffffbfu, 0x3bf86abau, 0x7fffffbfu, 0x00000000u, 0x0000007fu, 0x00200000u, 0x00000800u, 0x00000800u, 0x00000800u, 0x00000002u, 0x00000800u, 0x00000800u, 0x00000002u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x3bfa6abbu, 0xc4089545u, 0xc40a9545u, 0xc40e9545u, 0xc4169545u, 0x95450000u, 0xc4060000u, 0xc4060001u, 0x0000c406u, 0xffffc406u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu}, + {0x10d822c6u, 0x015c33b2u, 0x00010000u, 0x13908a2au, 0x10d822c6u, 0x015c33b2u, 0x015c33b2u, 0x12345678u, 0x12345678u, 0x136865cau, 0x12204448u, 0xfeb7de7du, 0xec838805u, 0xec979a35u, 0x00000000u, 0x0000007fu, 0x137c77fau, 0x00010000u, 0x136865cau, 0x00010000u, 0x12204448u, 0x00000000u, 0xfeb7de7du, 0xfffeffffu, 0xffebedcfu, 0xffffffffu, 0xec838805u, 0xfffeffffu, 0xec979a35u, 0xfffeffffu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x59e00000u, 0x0000048du, 0x00000000u, 0x00000000u, 0x0000048du, 0x00000000u, 0x00000000u, 0xfffc0000u, 0x00000000u, 0xfffc0000u, 0x0000048du, 0x0000048du, 0x00000000u, 0x00000000u, 0x0000035eu, 0x10d822c6u, 0x25c4e0a2u, 0x4a2d8d92u, 0x92fee772u, 0x24a19b32u, 0x33b25678u, 0x015c5678u, 0x015c1234u, 0x0018c2d2u, 0x0018c2d2u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000047eu}, + {0x2a74915au, 0x0000ffffu, 0x00010000u, 0x2a769158u, 0x2a74915au, 0x0000ffffu, 0x0000ffffu, 0x2a759159u, 0x2a759159u, 0x2a756ea6u, 0x2a750000u, 0xffff9159u, 0xd58a0000u, 0xd58a9159u, 0x00000000u, 0x0000007fu, 0x2a75ffffu, 0x4a0fe75du, 0x2a756ea6u, 0x4a0ee75du, 0x2a750000u, 0x4a0ee75du, 0xffff9159u, 0xffffffffu, 0xffff6ea6u, 0xfffeffffu, 0xd58a0000u, 0xb5f018a2u, 0xd58a9159u, 0xb5f118a2u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000218u, 0x2a74915au, 0x54ec22b1u, 0xa9d74563u, 0x53ad8ac7u, 0xa75a158fu, 0xffff9159u, 0x00009159u, 0x00002a75u, 0x00002a75u, 0x00002a75u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000004feu}, + {0x0000fffdu, 0x0000ffffu, 0xe4cc4132u, 0x00010000u, 0x0000fffeu, 0x00000001u, 0x00000001u, 0x0000ffffu, 0x0000ffffu, 0x0000fffeu, 0x0000fffeu, 0xffffffffu, 0xffff0000u, 0xffff0001u, 0x00000000u, 0x0000087fu, 0x0000ffffu, 0xe4cc413fu, 0x0000fffeu, 0xe4cc410du, 0x0000fffeu, 0xe4cc4100u, 0xffffffffu, 0xfffffff2u, 0xfffffffeu, 0xffffffcdu, 0xffff0000u, 0x1b33bec0u, 0xffff0001u, 0x1b33bef2u, 0x00000000u, 0x0000007fu, 0x0001fffeu, 0xc9988264u, 0x00007fffu, 0x00007fffu, 0x72662099u, 0x00007fffu, 0x00007fffu, 0xf2662099u, 0xfffffffeu, 0xfffffffeu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x0000fffeu, 0x0001ffffu, 0x0003fffdu, 0x0007fff9u, 0x000ffff1u, 0x0001ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu}, + {0xad7bd36cu, 0x43892dfcu, 0x00000005u, 0xd996888bu, 0xad7bd36du, 0x960d5a8fu, 0x43892dfcu, 0x43892dfcu, 0x960d5a8fu, 0xd5847773u, 0x41802570u, 0x6bfbadfcu, 0x28728000u, 0x2a7b888cu, 0x00000000u, 0x00001adfu, 0xd78d7fffu, 0x0021001fu, 0xd5847773u, 0x0021001au, 0x41802570u, 0x00000000u, 0x6bfbadfcu, 0xffdeffe5u, 0xfdf6f773u, 0xfffffffau, 0x28728000u, 0xffdeffe0u, 0x2a7b888cu, 0xffdeffe5u, 0x00000000u, 0x0000007fu, 0x96fe0000u, 0x0002a1c4u, 0x00008712u, 0x000a8712u, 0x00000000u, 0x00008712u, 0x000a8712u, 0x00000000u, 0xffff8000u, 0xffff8000u, 0xffffffffu, 0x00000712u, 0x00000712u, 0x00000712u, 0x00000000u, 0x000003ffu, 0x52842c93u, 0x1d1fb687u, 0xa432127fu, 0xb256ca6fu, 0xcea03a4fu, 0x5a8f2dfcu, 0x960d2dfcu, 0x960d4389u, 0x2795e6ccu, 0xe40cb8d0u, 0x00000712u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xd0d2b3b9u, 0x2f4d4c86u, 0x00000040u, 0x2f6d4cc5u, 0xd0d2b3b9u, 0x0020003fu, 0x0020003fu, 0x2f4d4c86u, 0x2f4d4c86u, 0x2f6d4cb9u, 0x00200039u, 0xd0b2b37fu, 0xd092b340u, 0xd092b346u, 0x00000000u, 0x0000119fu, 0x2f6d4cbfu, 0x7b121dc5u, 0x2f6d4cb9u, 0x7b121d85u, 0x00200039u, 0x7b121d85u, 0xd0b2b37fu, 0xffffffffu, 0xfffffff9u, 0xffffffbfu, 0xd092b340u, 0x84ede23au, 0xd092b346u, 0x84ede27au, 0x00000000u, 0x0000007fu, 0x08000fc0u, 0xc4877140u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0xffffffc0u, 0xffffffc0u, 0xffffffffu, 0x00008000u, 0x00008000u, 0x14008000u, 0x01ec4877u, 0x000003ffu, 0x2f2d4c47u, 0x2f8d4d04u, 0x2fcd4d82u, 0x304d4e7eu, 0x314d5076u, 0x4c86003fu, 0x2f4d003fu, 0x2f4d0020u, 0x0005e9b5u, 0x0005e9b5u, 0x14008000u, 0x01ec4877u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0xfb5fed18u, 0x00000001u, 0x5b8e8fb2u, 0x04a012e9u, 0xfb5fed19u, 0x00000001u, 0x00000001u, 0x04a012e8u, 0x04a012e8u, 0x04a012e9u, 0x00000001u, 0xfb5fed17u, 0xfb5fed16u, 0xfb5fed16u, 0x00000000u, 0x0000199fu, 0x04a012e9u, 0x5bcf8fb3u, 0x04a012e9u, 0x5bcf8fb1u, 0x00000001u, 0x5b8e8fb0u, 0xfb5fed17u, 0xffbefffeu, 0xffffffffu, 0xfffffffdu, 0xfb5fed16u, 0xa430704cu, 0xfb5fed16u, 0xa430704eu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000100u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000100u, 0x00000000u, 0x00000100u, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x000002b9u, 0x04a012e7u, 0x04a012eau, 0x04a012ecu, 0x04a012f0u, 0x04a012f8u, 0x12e80001u, 0x04a00001u, 0x04a00000u, 0x00000000u, 0x00000000u, 0x005b8e8fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x0baccdb2u, 0xf453324eu, 0xffff8000u, 0xf453324eu, 0x0baccdb2u, 0xf453324eu, 0x00000000u, 0x00000000u, 0xf453324eu, 0xf453324eu, 0x00000000u, 0x0baccdb1u, 0x0baccdb1u, 0x0baccdb1u, 0x00000000u, 0x000010d7u, 0xf453324eu, 0xffff8040u, 0xf453324eu, 0xffff8040u, 0x00000000u, 0x00000040u, 0x0baccdb1u, 0x00007fffu, 0xffffffffu, 0xffffffffu, 0x0baccdb1u, 0x00007fbfu, 0x0baccdb1u, 0x00007fbfu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00100000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x000002b9u, 0x0baccdb2u, 0xf453324eu, 0xf453324eu, 0xf453324eu, 0xf453324eu, 0x324e0000u, 0xf4530000u, 0xf4530000u, 0x00000000u, 0x00000000u, 0x01000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0xf121c8c2u, 0x00080004u, 0x1ba95a54u, 0xf131c8cau, 0xf121c8c2u, 0xf129c8c6u, 0x00080004u, 0x00080004u, 0xf129c8c6u, 0xf121c8c2u, 0xf121c8c2u, 0xffffffffu, 0x0ed63739u, 0x0ede373du, 0x00000000u, 0x0000013fu, 0xf129c8c6u, 0x1ba95a54u, 0xf121c8c2u, 0x0ba95a54u, 0xf121c8c2u, 0x00000000u, 0xffffffffu, 0xf456a5abu, 0xfff7fffbu, 0xefffffffu, 0x0ed63739u, 0xe456a5abu, 0x0ede373du, 0xf456a5abu, 0x00000000u, 0x0000007fu, 0x129c8c60u, 0x0000000fu, 0x0f129c8cu, 0x0f129c8cu, 0x01000000u, 0xff129c8cu, 0x0f129c8cu, 0x01000000u, 0x000003f0u, 0x000003f0u, 0x00000000u, 0xffffff8cu, 0x0000008cu, 0x0000008cu, 0x00000000u, 0x000003ffu, 0x0ede373eu, 0xe25b9190u, 0xc4af231cu, 0x89564634u, 0x12a48c64u, 0x0004c8c6u, 0x0008c8c6u, 0x0008f129u, 0x00078952u, 0xffff894eu, 0xffffff8cu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0x80010020u, 0x7fffffffu, 0x10000000u, 0x8001001eu, 0x80010020u, 0x0001001fu, 0x0001001fu, 0x7fffffffu, 0x7fffffffu, 0x7ffeffe0u, 0x00000000u, 0x8001001fu, 0x80000000u, 0x8001001fu, 0x00000000u, 0x00001597u, 0x7fffffffu, 0x1960afe9u, 0x7ffeffe0u, 0x1960afe9u, 0x00000000u, 0x0960afe9u, 0x8001001fu, 0xefffffffu, 0xfffeffe0u, 0xffffffffu, 0x80000000u, 0xe69f5016u, 0x8001001fu, 0xe69f5016u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000218u, 0x7ffeffe0u, 0x8002003du, 0x8004007bu, 0x800800f7u, 0x801001efu, 0xffff001fu, 0x7fff001fu, 0x7fff0001u, 0x0000800fu, 0x0000800fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu}, + {0x5f7933bfu, 0x8b8e8f4eu, 0x00400000u, 0x7696525bu, 0x5f7933bfu, 0x8b8e8f4eu, 0x8b8e8f4eu, 0xeb07c30du, 0xeb07c30du, 0x60894c43u, 0x60014001u, 0xff77f3bdu, 0x147030b0u, 0x9f76b3bcu, 0x00000000u, 0x0000047fu, 0xeb8fcf4fu, 0x0050001cu, 0x60894c43u, 0x0050001cu, 0x60014001u, 0x0010001cu, 0xff77f3bdu, 0xffbfffffu, 0x74f97cf3u, 0xffffffffu, 0x147030b0u, 0xffafffe3u, 0x9f76b3bcu, 0xffafffe3u, 0x00000000u, 0x0000007fu, 0xf0c34000u, 0x00073ac1u, 0x0003ac1fu, 0x0073ac1fu, 0x00000040u, 0xffffac1fu, 0x0073ac1fu, 0x00000040u, 0x07ffc000u, 0x07ffc000u, 0x00000000u, 0xffffec1fu, 0x00002c1fu, 0x00002c1fu, 0x00000000u, 0x000003ffu, 0x5f7933bfu, 0x619e1568u, 0x37ad9b82u, 0xe3cca7b6u, 0x3c0ac01eu, 0x8f4ec30du, 0x8b8ec30du, 0x8b8eeb07u, 0x802018c7u, 0x0989c66cu, 0xffffec1fu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0x80d682f0u, 0x7fffffffu, 0xb89c4e56u, 0x7f297d0du, 0x80d682f1u, 0xff297d0eu, 0x7fffffffu, 0x7fffffffu, 0xff297d0eu, 0x80d682f1u, 0x00d682f1u, 0x7fffffffu, 0x00000000u, 0x7f297d0eu, 0x00000000u, 0x00001addu, 0xffffffffu, 0xffffffffu, 0x80d682f1u, 0x4763b1a9u, 0x00d682f1u, 0x00000000u, 0x7fffffffu, 0xb89c4e56u, 0x80d682f1u, 0x4763b1a9u, 0x00000000u, 0x00000000u, 0x7f297d0eu, 0xb89c4e56u, 0x00000000u, 0x0000007du, 0xffffc000u, 0x13959fffu, 0x0001ffffu, 0x3959ffffu, 0x0002e271u, 0x0001ffffu, 0x3959ffffu, 0xfffee271u, 0xffffc000u, 0xffffc000u, 0xffffffffu, 0x0001ffffu, 0x0001ffffu, 0x3959ffffu, 0x00000071u, 0x000003ffu, 0x7f297d0fu, 0xff297d0cu, 0xff297d0au, 0xff297d06u, 0xff297cfeu, 0x7d0effffu, 0xff29ffffu, 0xff297fffu, 0x7f94be86u, 0xff94be87u, 0x3959ffffu, 0x00000071u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0x8bd8883au, 0x74a677c6u, 0x2547f19cu, 0x752577c6u, 0x8bd8883au, 0x007f0000u, 0x007f0000u, 0x74a677c6u, 0x74a677c6u, 0x74d977c6u, 0x00590000u, 0x8b7f8839u, 0x8b008839u, 0x8b268839u, 0x00000000u, 0x0000119fu, 0x74ff77c6u, 0x2567f1bfu, 0x74d977c6u, 0x2567f1a3u, 0x00590000u, 0x00200023u, 0x8b7f8839u, 0xdab80e7fu, 0xffd9ffffu, 0xffffffe3u, 0x8b008839u, 0xda980e40u, 0x8b268839u, 0xda980e5cu, 0x00000000u, 0x0000007fu, 0x1fc00000u, 0x08000fc0u, 0x0001fc00u, 0xfc01fc00u, 0x00008000u, 0x0001fc00u, 0xfc01fc00u, 0x00008000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0001fc00u, 0x0001fc00u, 0xfc01fc00u, 0x00000000u, 0x000003ffu, 0x742777c6u, 0x75a477c6u, 0x76a277c6u, 0x789e77c6u, 0x7c9677c6u, 0x77c60000u, 0x74a60000u, 0x74a6007fu, 0x0039de95u, 0x0039de95u, 0xfc01fc00u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0xde92d85fu, 0x00000002u, 0x558d2adbu, 0x216d27a4u, 0xde92d860u, 0x00000002u, 0x00000002u, 0x216d27a2u, 0x216d27a2u, 0x216d27a0u, 0x00000000u, 0xde92d85fu, 0xde92d85du, 0xde92d85fu, 0x00000000u, 0x00001997u, 0x216d27a2u, 0xd5ed6edfu, 0x216d27a0u, 0xc5e84654u, 0x00000000u, 0x45880250u, 0xde92d85fu, 0x7f9fbbfbu, 0xfffffffdu, 0xeffad774u, 0xde92d85du, 0x2a129120u, 0xde92d85fu, 0x3a17b9abu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x00000008u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x0000000cu, 0x00000000u, 0x0000000cu, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x000002b9u, 0x216d27a0u, 0x216d27a6u, 0x216d27aau, 0x216d27b2u, 0x216d27c2u, 0x27a20002u, 0x216d0002u, 0x216d0000u, 0x00000000u, 0x00000000u, 0x15634ab6u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0xa121d8c9u, 0xa161d909u, 0x69f0441eu, 0xa1a1d948u, 0xa121d8cau, 0xa161d909u, 0x0040003fu, 0x0040003fu, 0xa161d909u, 0xa121d936u, 0xa121d900u, 0xffffffc9u, 0x5e9e26c0u, 0x5ede26c9u, 0x00000000u, 0x0000093fu, 0xa161d93fu, 0xffffffffu, 0xa121d936u, 0x960fbbe1u, 0xa121d900u, 0x00000000u, 0xffffffc9u, 0x69f0441eu, 0xffbffff6u, 0x960fbbe1u, 0x5e9e26c0u, 0x00000000u, 0x5ede26c9u, 0x69f0441eu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x80000000u, 0xffffffffu, 0x00000001u, 0x00000000u, 0x00000000u, 0x0000035eu, 0x5ede2736u, 0x4303b251u, 0x85c76463u, 0x0b4ec887u, 0x165d90cfu, 0x003fd909u, 0x0040d909u, 0x0040a161u, 0x0028589du, 0xffe8585eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007feu}, + {0x7aa2868bu, 0x00000021u, 0x00000040u, 0x7aa286cdu, 0x7aa2868bu, 0x00000021u, 0x00000021u, 0x7aa286acu, 0x7aa286acu, 0x7aa2868du, 0x7aa2868cu, 0xfffffffeu, 0x855d7952u, 0x855d7972u, 0x00000000u, 0x0000007fu, 0x7aa286adu, 0x40000040u, 0x7aa2868du, 0x40000040u, 0x7aa2868cu, 0x40000000u, 0xfffffffeu, 0xffffffbfu, 0xffffffdfu, 0xffffffffu, 0x855d7952u, 0xbfffffbfu, 0x855d7972u, 0xbfffffbfu, 0x00000000u, 0x0000007fu, 0x00000000u, 0xf5450d58u, 0x3d514356u, 0x20000000u, 0x00000000u, 0x3d514356u, 0x20000000u, 0x00000000u, 0x00001ffeu, 0x00000000u, 0xfffffffeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x7aa2868bu, 0xf5450d79u, 0xea8a1ad1u, 0xd5143581u, 0xaa286ae1u, 0x002186acu, 0x000086acu, 0x00007aa2u, 0x0000000fu, 0x0000000fu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000005feu}, + {0x7fc3987cu, 0x0010001cu, 0x10000000u, 0x7fe398b4u, 0x7fc3987cu, 0x0010001cu, 0x0010001cu, 0x7fd39898u, 0x7fd39898u, 0x7fc39884u, 0x7fc39880u, 0xfffffffbu, 0x802c6763u, 0x803c677bu, 0x00000000u, 0x0000007fu, 0x7fd3989cu, 0x3e1c5d3au, 0x7fc39884u, 0x3e1c5d3au, 0x7fc39880u, 0x2e1c5d3au, 0xfffffffbu, 0xefffffffu, 0xffefffe7u, 0xffffffffu, 0x802c6763u, 0xc1e3a2c5u, 0x803c677bu, 0xc1e3a2c5u, 0x00000000u, 0x0000007fu, 0x80000000u, 0xa7fd3989u, 0x00000007u, 0xe1c5d3a7u, 0x00000002u, 0x00000007u, 0xe1c5d3a7u, 0x00000002u, 0xf0000000u, 0xf0000000u, 0x000fffffu, 0x00000007u, 0x00000007u, 0x0000d3a7u, 0x00000000u, 0x000003ffu, 0x7fc3987cu, 0xffb7314cu, 0xff5e627cu, 0xfeacc4dcu, 0xfd49899cu, 0x001c9898u, 0x00109898u, 0x00107fd3u, 0x0007fd47u, 0x0007fd47u, 0xffffd3a7u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000005ffu}, + {0x0020003au, 0x00000005u, 0x0010001cu, 0x00200044u, 0x0020003au, 0x00000005u, 0x00000005u, 0x0020003fu, 0x0020003fu, 0x0020003au, 0x0020003au, 0xffffffffu, 0xffdfffc0u, 0xffdfffc5u, 0x00000000u, 0x0000007fu, 0x0020003fu, 0xa9fd1cfdu, 0x0020003au, 0xa9ed1cf5u, 0x0020003au, 0xa9ed1ce1u, 0xffffffffu, 0xffffffebu, 0xfffffffau, 0xffeffff7u, 0xffdfffc0u, 0x5602e302u, 0xffdfffc5u, 0x5612e30au, 0x00000000u, 0x0000007fu, 0x040007e0u, 0x3fa39d20u, 0x00010001u, 0x48010001u, 0x054fe8e7u, 0x00010001u, 0x48010001u, 0xfd4fe8e7u, 0xffffffe0u, 0xffffffe0u, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000003f8u, 0x0020003au, 0x00400083u, 0x00800101u, 0x010001fdu, 0x020003f5u, 0x0005003fu, 0x0000003fu, 0x00000020u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu}, + {0x4ed751d9u, 0x43cdc356u, 0xb0ddb48bu, 0x38c434d2u, 0x4ed751dau, 0xf4f6717cu, 0x43cdc356u, 0x43cdc356u, 0xf4f6717cu, 0xb73bb22au, 0x03098202u, 0x4bcdcfd7u, 0x08000c81u, 0x48c44dd5u, 0x00000000u, 0x000018dfu, 0xf7fff37eu, 0xb6dfbd9fu, 0xb73bb22au, 0x168f8d1eu, 0x03098202u, 0x108d840au, 0x4bcdcfd7u, 0xf9fdf6ebu, 0xbf3bbeabu, 0x5fafcf7eu, 0x08000c81u, 0x49204260u, 0x48c44dd5u, 0xe97072e1u, 0x00000000u, 0x0000007fu, 0x00000000u, 0x60000000u, 0x00000004u, 0x0000000bu, 0x00000000u, 0x00000004u, 0xfffffffbu, 0xffffffffu, 0xf0000000u, 0x00000000u, 0xf0000000u, 0x00000004u, 0x00000004u, 0x0000000bu, 0x00000000u, 0x000003ffu, 0x4ed751dau, 0x7c91f828u, 0x042d7ed4u, 0x13648c2cu, 0x31d2a6dcu, 0x717cc356u, 0xf4f6c356u, 0xf4f643cdu, 0x40e163f6u, 0xfd13a0a0u, 0xfffffffbu, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xffefffe4u, 0x0010001cu, 0x10000000u, 0x0010001cu, 0xffefffe4u, 0x00000000u, 0x00000000u, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x00000000u, 0xffefffe3u, 0xffefffe3u, 0xffefffe3u, 0x00000000u, 0x00001197u, 0x0010001cu, 0x10080004u, 0x0010001cu, 0x10080004u, 0x00000000u, 0x00080004u, 0xffefffe3u, 0xefffffffu, 0xffffffffu, 0xffffffffu, 0xffefffe3u, 0xeff7fffbu, 0xffefffe3u, 0xeff7fffbu, 0x00000000u, 0x0000007fu, 0x00000000u, 0x40000000u, 0x00000000u, 0x00800040u, 0x00000000u, 0x00000000u, 0x00800040u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000040u, 0x00000000u, 0x000002b9u, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x0010001cu, 0x001c0000u, 0x00100000u, 0x00100000u, 0x00000000u, 0x00000000u, 0x00000040u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043fu}, + {0x0f4ef612u, 0xbce240c6u, 0x68916d08u, 0x8913779eu, 0x0f4ef612u, 0xbce240c6u, 0xbce240c6u, 0xcc3136d8u, 0xcc3136d8u, 0x70d3761eu, 0x40113618u, 0xcf3dbff9u, 0x030c8921u, 0x8f2c89e1u, 0x00000000u, 0x0000007fu, 0xfcf376deu, 0x68916d1cu, 0x70d3761eu, 0x68816d14u, 0x40113618u, 0x00000014u, 0xcf3dbff9u, 0x977e92ffu, 0x73dfff3fu, 0xffeffff7u, 0x030c8921u, 0x976e92e3u, 0x8f2c89e1u, 0x977e92ebu, 0x00000000u, 0x0000007fu, 0x0c4db600u, 0x04000733u, 0x0330c4dbu, 0x7330c4dbu, 0x00004000u, 0xff30c4dbu, 0x7330c4dbu, 0x00004000u, 0x3fffffc0u, 0x3fffffc0u, 0x00000000u, 0xff30c4dbu, 0x0330c4dbu, 0x7330c4dbu, 0x00004000u, 0x000003ffu, 0x0f4ef612u, 0x5544ae76u, 0xeda71c26u, 0x1e6bf786u, 0x7ff5ae46u, 0x40c636d8u, 0xbce236d8u, 0xbce2cc31u, 0x96a89b63u, 0x0d9523c5u, 0x7330c4dbu, 0x00004000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000007ffu}, + {0xffff0020u, 0x00000040u, 0xb036d9ddu, 0x0001005fu, 0xffff0021u, 0x00000040u, 0x00000040u, 0x0001001fu, 0x0001001fu, 0x0001005fu, 0x00000040u, 0xfffeffe0u, 0xfffeffa0u, 0xfffeffa0u, 0x00000000u, 0x0000199fu, 0x0001005fu, 0xb4b6d9ffu, 0x0001005fu, 0x349210e2u, 0x00000040u, 0x301210c0u, 0xfffeffe0u, 0xfb7fffddu, 0xffffffffu, 0x7fdb36e2u, 0xfffeffa0u, 0x4b492600u, 0xfffeffa0u, 0xcb6def1du, 0x00000000u, 0x0000007fu, 0x00000000u, 0x80000020u, 0x00000000u, 0x606db3bau, 0x00000001u, 0x00000000u, 0x606db3bau, 0xffffffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x000002b8u, 0x0000ffdfu, 0x0001009fu, 0x0001011fu, 0x0001021fu, 0x0001041fu, 0x001f0040u, 0x00010040u, 0x00010000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000043eu} }; -constexpr const char* Names[64] = {"subb_u32", "cselect_b64 lo", "cselect_b64 hi", "add_i32", "sub_i32", "min_i32", "min_u32", "max_i32", "max_u32", "xor_b32", "andn2_b32", "orn2_b32", "nor_b32", "xnor_b32", nullptr, "scc subb_u32,cselect_b64,add_i32,sub_i32,min_i32,min_u32,max_i32,max_u32,xor_b32,andn2_b32,orn2_b32,nor_b32,xnor_b32", "or_b64 lo", "or_b64 hi", "xor_b64 lo", "xor_b64 hi", "andn2_b64 lo", "andn2_b64 hi", "orn2_b64 lo", "orn2_b64 hi", "nand_b64 lo", "nand_b64 hi", "nor_b64 lo", "nor_b64 hi", "xnor_b64 lo", "xnor_b64 hi", nullptr, "scc or_b64,xor_b64,andn2_b64,orn2_b64,nand_b64,nor_b64,xnor_b64", "lshl_b64 lo", "lshl_b64 hi", "lshr_b32", "lshr_b64 lo", "lshr_b64 hi", "ashr_i32", "ashr_i64 lo", "ashr_i64 hi", "bfm_b32", "bfm_b64 lo", "bfm_b64 hi", "bfe_i32", "bfe_u32", "bfe_u64 lo", "bfe_u64 hi", "scc lshl_b64,lshr_b32,lshr_b64,ashr_i32,ashr_i64,bfm_b32,bfm_b64,bfe_i32,bfe_u32,bfe_u64", "absdiff_i32", "lshl1_add_u32", "lshl2_add_u32", "lshl3_add_u32", "lshl4_add_u32", "pack_ll_b32_b16", "pack_lh_b32_b16", "pack_hh_b32_b16", "mul_hi_u32", "mul_hi_i32", nullptr, nullptr, nullptr, nullptr, nullptr, "scc absdiff_i32,lshl1_add_u32,lshl2_add_u32,lshl3_add_u32,lshl4_add_u32,pack_ll_b32_b16,pack_lh_b32_b16,pack_hh_b32_b16,mul_hi_u32,mul_hi_i32"}; +constexpr const char* Names[64] = {"subb_u32", "cselect_b64 lo", "cselect_b64 hi", "add_i32", "sub_i32", "min_i32", "min_u32", "max_i32", "max_u32", "xor_b32", "andn2_b32", "orn2_b32", "nor_b32", "xnor_b32", nullptr, "scc subb_u32,cselect_b64,add_i32,sub_i32,min_i32,min_u32,max_i32,max_u32,xor_b32,andn2_b32,orn2_b32,nor_b32,xnor_b32", "or_b64 lo", "or_b64 hi", "xor_b64 lo", "xor_b64 hi", "andn2_b64 lo", "andn2_b64 hi", "orn2_b64 lo", "orn2_b64 hi", "nand_b64 lo", "nand_b64 hi", "nor_b64 lo", "nor_b64 hi", "xnor_b64 lo", "xnor_b64 hi", nullptr, "scc or_b64,xor_b64,andn2_b64,orn2_b64,nand_b64,nor_b64,xnor_b64", "lshl_b64 lo", "lshl_b64 hi", "lshr_b32", "lshr_b64 lo", "lshr_b64 hi", "ashr_i32", "ashr_i64 lo", "ashr_i64 hi", "bfm_b32", "bfm_b64 lo", "bfm_b64 hi", "bfe_i32", "bfe_u32", "bfe_u64 lo", "bfe_u64 hi", "scc lshl_b64,lshr_b32,lshr_b64,ashr_i32,ashr_i64,bfm_b32,bfm_b64,bfe_i32,bfe_u32,bfe_u64", "absdiff_i32", "lshl1_add_u32", "lshl2_add_u32", "lshl3_add_u32", "lshl4_add_u32", "pack_ll_b32_b16", "pack_lh_b32_b16", "pack_hh_b32_b16", "mul_hi_u32", "mul_hi_i32", "bfe_i64 lo", "bfe_i64 hi", nullptr, nullptr, nullptr, "scc absdiff_i32,lshl1_add_u32,lshl2_add_u32,lshl3_add_u32,lshl4_add_u32,pack_ll_b32_b16,pack_lh_b32_b16,pack_hh_b32_b16,mul_hi_u32,mul_hi_i32,bfe_i64"}; void Fill(std::uint32_t tid, std::uint32_t* words) { std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Vop3IntegerAlu.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3IntegerAlu.cpp new file mode 100644 index 000000000..38c609632 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Vop3IntegerAlu.cpp @@ -0,0 +1,181 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" +#include "Recompiler.hpp" +#include "VulkanTestDevice.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 = 64; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060088, 0xe0381000, 0x80000401, 0xbf8c3f70, 0xd76d000a, 0x041a0b04, 0xd747000b, + 0x041a0b04, 0xd771000c, 0x041a0b04, 0xd772000d, 0x041a0b04, 0xd578000e, 0x041a0b04, 0xd745000f, + 0x041a0b04, 0xd7460010, 0x041a0b04, 0xd76f0011, 0x041a0b04, 0xd5480012, 0x041a0b04, 0xd5490013, + 0x041a0b04, 0xd54a0014, 0x041a0b04, 0xd7630015, 0x02020b04, 0xd7440016, 0x041a0b04, 0xd54e0017, + 0x041a0b04, 0xd54f0018, 0x041a0b04, 0xd5690019, 0x02020b04, 0xd56a001a, 0x02020b04, 0xd56c001b, + 0x02020b04, 0xd542001c, 0x041a0b04, 0xd764001d, 0x02020b04, 0xd6ff001e, 0x02020b04, 0xd7000020, + 0x02020b04, 0xd7010022, 0x02020b04, 0xd5520024, 0x041a0b04, 0xd5550025, 0x041a0b04, 0xd5580026, + 0x041a0b04, 0xd5530027, 0x041a0b04, 0xd5560028, 0x041a0b04, 0xd5590029, 0x041a0b04, 0xd76a002a, + 0x02020b04, 0xd76b002b, 0x02020b04, 0x7e580307, 0xd703002c, 0x02020b04, 0x7e5a0307, 0xd704002d, + 0x02020b04, 0x7e5c0307, 0xd70d002e, 0x02020b04, 0x7e5e0307, 0xd70e002f, 0x02020b04, 0x7e600307, + 0xd7140030, 0x02020b04, 0x7e620307, 0xd7070031, 0x02020b04, 0x7e640307, 0xd7080032, 0x02020b04, + 0x7e660307, 0xd70b0033, 0x02020b04, 0x7e680307, 0xd7090034, 0x02020b04, 0x7e6a0307, 0xd70c0035, + 0x02020b04, 0x7e6c0307, 0xd70a0036, 0x02020b04, 0xd70f0837, 0x02020b04, 0xd5010038, 0x00210280, + 0xd7100839, 0x02020b04, 0xd501003a, 0x00210280, 0xd719083b, 0x02020b04, 0xd501003c, 0x00210280, + 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, + 0xe0781040, 0x80011a03, 0xe0781050, 0x80011e03, 0xe0781060, 0x80012203, 0xe0781070, 0x80012603, + 0xe0781080, 0x80012a03, 0xe0781090, 0x80012e03, 0xe07810a0, 0x80013203, 0xe07810b0, 0x80013603, + 0xe07810c0, 0x80013a03, 0xbf810000, +}; + +constexpr std::uint32_t Rows[32][4] = { + {0xf0000000u, 0x0000001cu, 0x00000008u, 0x12345678u}, + {0x80000000u, 0x0000001fu, 0x00000002u, 0xdeadbeefu}, + {0xc0000000u, 0x0000001eu, 0x0000001fu, 0x00000000u}, + {0xabcdef01u, 0x00000000u, 0x00000000u, 0xffffffffu}, + {0x8000ffffu, 0x00000010u, 0x0000001fu, 0xffff0000u}, + {0x7fffffffu, 0x00000001u, 0x0000001fu, 0x55555555u}, + {0x01020304u, 0x05060708u, 0x0c0b0a09u, 0x00000000u}, + {0x80ff7f01u, 0x81827f80u, 0x03020100u, 0x00000001u}, + {0xffff8000u, 0xffff7fffu, 0x08090a0bu, 0xaaaa5555u}, + {0x0000ffffu, 0x00010000u, 0x0d0e0f10u, 0x00000002u}, + {0x00008000u, 0xffffffffu, 0xffff0000u, 0x00000000u}, + {0x00000000u, 0x00000000u, 0x0000ffffu, 0x25c717e5u}, + {0x00008000u, 0xffff8000u, 0x0000001fu, 0x00000020u}, + {0xdce177fdu, 0xffffffffu, 0x80000000u, 0x000001ffu}, + {0x7fffffffu, 0x00000000u, 0x80000001u, 0x0000001fu}, + {0x80008000u, 0x7fff7fffu, 0xffffffffu, 0x00000040u}, + {0x00000010u, 0xfffffffeu, 0x00000000u, 0x80000001u}, + {0x00010000u, 0xffff0000u, 0x9c015d97u, 0x00000018u}, + {0x00000040u, 0x00000020u, 0x00008000u, 0x4b64789cu}, + {0x00008001u, 0xc7fde805u, 0x040e050fu, 0x309d6b79u}, + {0x0000003fu, 0xffff8000u, 0x07060103u, 0xa66b0d38u}, + {0x00ffffffu, 0x7fff7fffu, 0x84e55160u, 0x7fffffffu}, + {0x75139237u, 0x6822a6b2u, 0xfee5a5b2u, 0x154cd2aau}, + {0x0000003fu, 0x00000001u, 0x0000000fu, 0x7fffffffu}, + {0xd8e94b15u, 0x36a80bdfu, 0x00000003u, 0xffff8000u}, + {0x00000020u, 0x45100358u, 0x80000001u, 0xffff7fffu}, + {0x00000040u, 0x02cdf2afu, 0x0c0c0406u, 0x00008001u}, + {0x00000021u, 0x7fff7fffu, 0x00000001u, 0xffff8000u}, + {0x93b3a3d9u, 0x00000017u, 0xffffffffu, 0x0000001fu}, + {0x00000007u, 0x00000021u, 0x00000040u, 0x00007fffu}, + {0x7fff7fffu, 0x80008000u, 0x00010000u, 0x00000017u}, + {0x0000001fu, 0x00000020u, 0x0000003fu, 0x3253b562u} +}; +constexpr std::uint32_t Expected[32][51] = { + {0xf0000024u, 0x00001c00u, 0x00000008u, 0xf000001cu, 0xf0000014u, 0xf0000024u, 0x00000008u, 0x00000008u, 0x0000000fu, 0xffffffffu, 0x00000008u, 0x00000000u, 0x1c1c1c00u, 0x00000000u, 0x0000001cu, 0x40000000u, 0x0000001au, 0xfffffffeu, 0x00000008u, 0x00000020u, 0x0000001cu, 0x00000008u, 0x0000001cu, 0x00000008u, 0x0000001cu, 0x00000008u, 0xf0000000u, 0x0000001cu, 0x00000008u, 0x00000008u, 0xf0000000u, 0x0000001cu, 0x001cffffu, 0x001c8000u, 0x1234001cu, 0x1234ffe4u, 0x1234001cu, 0x1234ffe4u, 0x1234001cu, 0x1234001cu, 0x1234001cu, 0x12340000u, 0x1234001cu, 0x12340000u, 0x1234001cu, 0xf000001cu, 0x00000000u, 0xefffffe4u, 0x00000000u, 0x1000001cu, 0x00000001u}, + {0x80000021u, 0x0000007cu, 0x00000002u, 0x8000001fu, 0x8000001du, 0x80000021u, 0x00000002u, 0x00000002u, 0x00000001u, 0xffffffffu, 0x00000002u, 0x00000000u, 0x1f1f1f00u, 0x00000007u, 0x00000000u, 0x80000000u, 0x0000000fu, 0xfffffff0u, 0x00000002u, 0x00000020u, 0x0000001fu, 0x00000002u, 0x0000001fu, 0x00000002u, 0x0000001fu, 0x00000002u, 0x80000000u, 0x0000001fu, 0x00000002u, 0x00000002u, 0x80000000u, 0x0000001fu, 0x001fffffu, 0x001f8000u, 0xdead001fu, 0xdeadffe1u, 0xdead001fu, 0xdeadffe1u, 0xdead001fu, 0xdead001fu, 0xdead001fu, 0xdead0000u, 0xdead001fu, 0xdead0000u, 0xdead001fu, 0x8000001fu, 0x00000000u, 0x7fffffe1u, 0x00000000u, 0x8000001fu, 0x00000001u}, + {0xc000003du, 0x00000000u, 0x0000001fu, 0xc000001fu, 0xc0000001u, 0xc000003du, 0x0000001fu, 0x0000001fu, 0x00000003u, 0xffffffffu, 0x0000001fu, 0x00000000u, 0x1e1e1effu, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000016u, 0xfffffff8u, 0x0000001fu, 0x00000020u, 0x0000001eu, 0x0000001fu, 0x0000001eu, 0x0000001fu, 0x0000001eu, 0x0000001fu, 0xc0000000u, 0x0000001fu, 0x0000001eu, 0x0000001eu, 0xc0000000u, 0x0000001fu, 0x001effffu, 0x001e8000u, 0x0000001eu, 0x0000ffe2u, 0x0000001eu, 0x0000ffe2u, 0x0000001eu, 0x0000001eu, 0x0000001eu, 0x00000000u, 0x0000001eu, 0x00000000u, 0x0000001eu, 0xc000001eu, 0x00000000u, 0xbfffffe2u, 0x00000000u, 0x4000001eu, 0x00000001u}, + {0xabcdef01u, 0xabcdef01u, 0x00000000u, 0xabcdef01u, 0xabcdef01u, 0xabcdef01u, 0xabcdef01u, 0xabcdef01u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000012u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xabcdef01u, 0x00000000u, 0x00000000u, 0x00000000u, 0xabcdef01u, 0x00000000u, 0x0000ffffu, 0x00008000u, 0xffffef01u, 0xffffef01u, 0xffffef01u, 0xffffef01u, 0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffffef01u, 0xffffef01u, 0xffff0000u, 0xabcdef01u, 0x00000000u, 0xabcdef01u, 0x00000000u, 0x543210ffu, 0x00000001u}, + {0x8001002eu, 0x80000000u, 0x0000001fu, 0x8000ffffu, 0x8000fff0u, 0x8001000eu, 0xffff001fu, 0xffff001fu, 0x00008000u, 0xffff8000u, 0x00000010u, 0xffff0000u, 0x101010ffu, 0x0001fffeu, 0x00ffff00u, 0x000ffff0u, 0x00000008u, 0xfffffff8u, 0x0010000fu, 0x00000021u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x8000ffffu, 0x0000001fu, 0x00000010u, 0x00000010u, 0x8000ffffu, 0x0000001fu, 0x0010ffffu, 0x00108000u, 0xffff000fu, 0xffffffefu, 0xffff000fu, 0xffffffefu, 0xffff0000u, 0xffff0000u, 0xffff0000u, 0xffff0010u, 0xffffffffu, 0xffffffffu, 0xffff0010u, 0x8001000fu, 0x00000000u, 0x8000ffefu, 0x00000000u, 0x7fff0011u, 0x00000001u}, + {0x8000001fu, 0x00000000u, 0x0000001fu, 0x7fffffffu, 0x7fffffe1u, 0x8000001du, 0x0000001du, 0xffffffffu, 0x3fffffffu, 0x3fffffffu, 0x00000001u, 0xfffffffeu, 0x010101ffu, 0xfffffffeu, 0xffffff00u, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x0000001eu, 0x00000020u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x7fffffffu, 0x0000001fu, 0x00000001u, 0x7fffffffu, 0x0000001fu, 0x0001ffffu, 0x00017fffu, 0x55550000u, 0x5555fffeu, 0x55550000u, 0x5555fffeu, 0x55558000u, 0x55550000u, 0x55550000u, 0x55550001u, 0x5555ffffu, 0x5555ffffu, 0x55550001u, 0x80000000u, 0x00000000u, 0x7ffffffeu, 0x00000000u, 0x80000002u, 0x00000001u}, + {0x12131415u, 0x10141800u, 0x0d0b0b09u, 0x0d0f0f0du, 0x080f0e05u, 0x100f0e15u, 0x0e0e0e09u, 0x0e0b0e09u, 0x00000003u, 0x00000003u, 0x0d0b0b09u, 0x00000f00u, 0x00000000u, 0x82028303u, 0x04050607u, 0x3c3d3420u, 0x00051022u, 0x00051022u, 0x2c483e29u, 0x0506070du, 0x50607080u, 0xc0b0a090u, 0x90506070u, 0x00c0b0a0u, 0x90506070u, 0x00c0b0a0u, 0x01020304u, 0x0c0b0a09u, 0x05060708u, 0x01020304u, 0x0c0b0a09u, 0x05060708u, 0xffffffffu, 0x7fff7fffu, 0x00000a0cu, 0x0000fbfcu, 0x00000a0cu, 0x0000fbfcu, 0x00007080u, 0x00000070u, 0x00000070u, 0x00000304u, 0x00000708u, 0x00000304u, 0x00000708u, 0x06080a0cu, 0x00000000u, 0xfbfbfbfcu, 0x00000001u, 0x04040404u, 0x00000000u}, + {0x0583ff81u, 0x0281fe81u, 0x83827f00u, 0x83ff7f81u, 0x027f0181u, 0x047f0181u, 0x84018001u, 0x83ff7f01u, 0x00000000u, 0x00000000u, 0x83827f00u, 0x00000001u, 0x81827f80u, 0x81827f80u, 0x81827f80u, 0x3f42ff80u, 0x414280fdu, 0x3ec0827cu, 0x40450080u, 0x81827f91u, 0x0304ff00u, 0x06040201u, 0x40c13fc0u, 0x01810080u, 0x40c13fc0u, 0x01810080u, 0x80ff7f01u, 0x03020100u, 0x81827f80u, 0x03020100u, 0x81827f80u, 0x80ff7f01u, 0xffffffffu, 0x80008000u, 0x0000fe81u, 0x0000ff81u, 0x0000fe81u, 0x0000ff81u, 0x0000ff00u, 0x00003fc0u, 0x00003fc0u, 0x00007f01u, 0x00007f80u, 0x00007f01u, 0x00007f80u, 0x0281fe81u, 0x00000001u, 0xff7cff81u, 0x00000001u, 0x0083007fu, 0x00000000u}, + {0x08080a0au, 0xf7fff800u, 0xffff0a0bu, 0xffffffffu, 0x0809f5f4u, 0x080a0a0au, 0x08090a0bu, 0x08090a0bu, 0x00000001u, 0xffffffffu, 0xffff0a0bu, 0x00000000u, 0x00ffffffu, 0x001fffefu, 0xff8000ffu, 0x40008000u, 0xfffeffffu, 0x00000000u, 0x48098a0bu, 0xffff8010u, 0xffff7fffu, 0x08090a0bu, 0xffff7fffu, 0x08090a0bu, 0xffff7fffu, 0x08090a0bu, 0xffff7fffu, 0x08090a0bu, 0xffff8000u, 0x08090a0bu, 0xffff8000u, 0xffff7fffu, 0xffffffffu, 0x80008000u, 0xaaaaffffu, 0xaaaa0001u, 0xaaaaffffu, 0xaaaa0001u, 0xaaaa7fffu, 0xaaaa7fffu, 0xaaaa7fffu, 0xaaaa7fffu, 0xaaaa8000u, 0xaaaa8000u, 0xaaaa7fffu, 0xfffeffffu, 0x00000001u, 0x00000001u, 0x00000000u, 0xffffffffu, 0x00000001u}, + {0x0d100f0fu, 0xffff0000u, 0x0d0e0f10u, 0x0d0fffffu, 0x0d0ff0efu, 0x0d100f0fu, 0x0d0f0f0fu, 0x0d0effffu, 0x0000ffffu, 0xffffffffu, 0x0d0e0000u, 0x7fffffffu, 0xffffffffu, 0xffff0001u, 0x00010000u, 0xffff0000u, 0x00000000u, 0x00000000u, 0x0d0d0f10u, 0x00010010u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x0d0e0f10u, 0x00010000u, 0x0000ffffu, 0x0d0e0f10u, 0x00010000u, 0xffffffffu, 0x7fff7fffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x0001ffffu, 0x00000000u, 0xffffffffu, 0x00000001u, 0x00000001u, 0x00000000u}, + {0xffff7fffu, 0x00007fffu, 0xffff8000u, 0xffffffffu, 0x00007fffu, 0xfffe7fffu, 0xffff0000u, 0xffff0000u, 0x00000000u, 0x00000000u, 0xffff8000u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0xffff8000u, 0x00007fffu, 0xffffffffu, 0xfffe8000u, 0x00000000u, 0xffffffffu, 0xffff0000u, 0xffffffffu, 0xffff0000u, 0xffffffffu, 0xffff0000u, 0xffff0000u, 0x00008000u, 0xffffffffu, 0x00008000u, 0xffffffffu, 0xffff0000u, 0xffff8000u, 0xffff7fffu, 0x00007fffu, 0x00008001u, 0x00007fffu, 0x00008001u, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00008000u, 0x0000ffffu, 0x00008000u, 0x0000ffffu, 0x00007fffu, 0x00000001u, 0x00008001u, 0x00000001u, 0xffff7fffu, 0x00000000u}, + {0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x25c70000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x0000001fu, 0x00000000u, 0x0000801fu, 0xffff801fu, 0xffff001fu, 0xffff001fu, 0x0000801fu, 0x0000801fu, 0x00008000u, 0x00008000u, 0x0000801fu, 0x00000000u, 0x000000ffu, 0x00010001u, 0x008000ffu, 0xc0000000u, 0x00007fffu, 0xffffffffu, 0xc000001fu, 0xffff8001u, 0xffff8000u, 0x0000001fu, 0xffff8000u, 0x0000001fu, 0xffff8000u, 0x0000001fu, 0xffff8000u, 0x00008000u, 0x0000001fu, 0x0000001fu, 0xffff8000u, 0x00008000u, 0xffff8000u, 0x80007fffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00008000u, 0x00000000u, 0x00000001u, 0x00010000u, 0x00000001u, 0xffff0000u, 0x00000000u}, + {0x5ce177fcu, 0xdce177fcu, 0xdce177fdu, 0xffffffffu, 0xa31e8802u, 0xa31e8802u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0xdce177fdu, 0x80000000u, 0xffffffffu, 0xffffffffu, 0xffffffffu, 0x231e8803u, 0xdce177fcu, 0x00000000u, 0x801e8803u, 0x00000015u, 0x00000000u, 0xe0000000u, 0x00000004u, 0x00000000u, 0xfffffffcu, 0xffffffffu, 0x80000000u, 0xffffffffu, 0xdce177fdu, 0x80000000u, 0xffffffffu, 0xdce177fdu, 0xffffffffu, 0xffff8000u, 0x000077fcu, 0x000077feu, 0x000077fcu, 0x000077feu, 0x0000e000u, 0x00000007u, 0x0000ffffu, 0x000077fdu, 0x0000ffffu, 0x0000ffffu, 0x000077fdu, 0xdce177fcu, 0x00000001u, 0xdce177feu, 0x00000001u, 0x231e8802u, 0x00000000u}, + {0x00000000u, 0xfffffffeu, 0x80000001u, 0xffffffffu, 0xfffffffeu, 0x00000000u, 0x00000000u, 0xffffffffu, 0x00000001u, 0xffffffffu, 0x80000000u, 0x7fffffffu, 0xff000000u, 0x80000000u, 0xff000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80000001u, 0x0000001fu, 0x00000000u, 0x00000000u, 0x00000001u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x80000001u, 0x7fffffffu, 0x00000000u, 0x00000000u, 0x80000001u, 0x7fffffffu, 0x0000ffffu, 0x00007fffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x7fffffffu, 0x00000000u, 0x80000001u, 0x00000001u}, + {0xfffffffeu, 0x80000000u, 0xffffffffu, 0xffffffffu, 0x00000000u, 0xfffffffeu, 0xffffffffu, 0xffffffffu, 0x00000001u, 0xffffffffu, 0x7fff7fffu, 0x00000000u, 0xffffffffu, 0x00010000u, 0x0080007fu, 0x3fff8000u, 0x3fffffffu, 0xc0008000u, 0xbfff7fffu, 0x7fff8001u, 0x7fff7fffu, 0xffffffffu, 0x7fff7fffu, 0xffffffffu, 0x7fff7fffu, 0xffffffffu, 0x80008000u, 0x7fff7fffu, 0xffffffffu, 0x7fff7fffu, 0xffffffffu, 0x80008000u, 0xffffffffu, 0x7fff8000u, 0x0000ffffu, 0x00000001u, 0x0000ffffu, 0x00000001u, 0x00007fffu, 0x00007fffu, 0x00007fffu, 0x00007fffu, 0x00008000u, 0x00008000u, 0x00007fffu, 0xffffffffu, 0x00000000u, 0x00010001u, 0x00000000u, 0xfffeffffu, 0x00000001u}, + {0x0000000eu, 0x0000000eu, 0x00000010u, 0xfffffffeu, 0xffffffeeu, 0xffffffeeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000010u, 0xc0000000u, 0xfefefefeu, 0xfffffffeu, 0xfffffffeu, 0xffffffe0u, 0x0000000fu, 0xffffffffu, 0xffffffe0u, 0xffffffffu, 0xfffe0000u, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x0000ffffu, 0x00000000u, 0xfffffffeu, 0x00000010u, 0x00000000u, 0x00000000u, 0xfffffffeu, 0x00000010u, 0xffff0010u, 0xfffe0010u, 0x8000000eu, 0x80000012u, 0x8000000eu, 0x80000012u, 0x8000fffeu, 0x8000fffeu, 0x8000fffeu, 0x80000010u, 0x8000fffeu, 0x8000fffeu, 0x80000010u, 0x0000000eu, 0x00000001u, 0x00000012u, 0x00000001u, 0xffffffeeu, 0x00000000u}, + {0x9c015d97u, 0x00000000u, 0x9c015d97u, 0xffff5d97u, 0x63ff5d97u, 0x9bff5d97u, 0x9c025d97u, 0x9c015d97u, 0x00010000u, 0x00010000u, 0x9c015d97u, 0x00000000u, 0xff00ffffu, 0x020001ffu, 0x010000ffu, 0x00000000u, 0x0000ffffu, 0xffffffffu, 0x9c015d97u, 0xffff0001u, 0xffff0000u, 0x9c015d97u, 0xffff0000u, 0x9c015d97u, 0xffff0000u, 0x9c015d97u, 0x9c015d97u, 0x00010000u, 0xffff0000u, 0x00010000u, 0xffff0000u, 0x9c015d97u, 0xffffffffu, 0x80007fffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x00020000u, 0x00000001u, 0xfffe0000u, 0x00000000u}, + {0x00008060u, 0x00000060u, 0x00008000u, 0x00008060u, 0x00008060u, 0x00008060u, 0x00008040u, 0x00008040u, 0x00000000u, 0x00000000u, 0x00008000u, 0x00000000u, 0x2020ff20u, 0x00000020u, 0x00000020u, 0x00000800u, 0x00000000u, 0x00000000u, 0x00008800u, 0x00000021u, 0x00000020u, 0x00008000u, 0x00000020u, 0x00008000u, 0x00000020u, 0x00008000u, 0x00000020u, 0x00008000u, 0x00000040u, 0x00000020u, 0x00008000u, 0x00000040u, 0x00200040u, 0x00200040u, 0x4b640060u, 0x4b640020u, 0x4b640060u, 0x4b640020u, 0x4b640020u, 0x4b640020u, 0x4b640020u, 0x4b640020u, 0x4b640040u, 0x4b640020u, 0x4b640040u, 0x00000060u, 0x00000000u, 0x00000020u, 0x00000000u, 0xffffffe0u, 0x00000001u}, + {0xcc0c6d15u, 0x34030000u, 0x040e850fu, 0xc7ffed0fu, 0xc3f36d0bu, 0xcc0b6d13u, 0x041e052fu, 0x041e052fu, 0x00000400u, 0x00000400u, 0x040e850fu, 0x00000020u, 0x01ff80ffu, 0x00038ffbu, 0x008001c7u, 0xbc006805u, 0x000063ffu, 0xffffe3feu, 0xf80e6d14u, 0xc7fde807u, 0x8ffbd00au, 0x081c0a1fu, 0xe3fef402u, 0x02070287u, 0xe3fef402u, 0x02070287u, 0xc7fde805u, 0x040e050fu, 0x00008001u, 0x00008001u, 0xc7fde805u, 0x040e050fu, 0xffff8001u, 0x80007fffu, 0x309d6806u, 0x309d97fcu, 0x309d6806u, 0x309d97fcu, 0x309dd00au, 0x309d7402u, 0x309df402u, 0x309d8001u, 0x309de805u, 0x309d8001u, 0x309de805u, 0xc7fe6806u, 0x00000000u, 0x380297fcu, 0x00000001u, 0xc7fd6804u, 0x00000000u}, + {0x07058142u, 0xfffc01f8u, 0x07060103u, 0xffff813fu, 0xf8f9813cu, 0x07058142u, 0x07060142u, 0x0706013fu, 0x00000007u, 0xffffffffu, 0x07060100u, 0x7fffffffu, 0x000080ffu, 0xfffff000u, 0x00003fffu, 0xffe08000u, 0x0000003eu, 0xffffffffu, 0x06e68103u, 0xffff8006u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0xffff8000u, 0x07060103u, 0x0000003fu, 0x0000003fu, 0xffff8000u, 0x07060103u, 0xffff003fu, 0x8000003fu, 0xa66b803fu, 0xa66b803fu, 0xa66b803fu, 0xa66b803fu, 0xa66b0000u, 0xa66b0001u, 0xa66bffffu, 0xa66b003fu, 0xa66b8000u, 0xa66b8000u, 0xa66b003fu, 0xffff803fu, 0x00000000u, 0x0000803fu, 0x00000001u, 0xffff7fc1u, 0x00000000u}, + {0x05e4d15eu, 0x80ff7ffeu, 0x84ff7fffu, 0xffffffffu, 0xfbe5d160u, 0x03e5d160u, 0x04e55160u, 0x84e55160u, 0x00000000u, 0x00000000u, 0x84ff7fffu, 0x80000000u, 0xffffffffu, 0x7fff7fffu, 0x7fff7fffu, 0x7f008001u, 0x007fff7fu, 0x007fff7fu, 0x84e5d161u, 0x7fff8017u, 0x00000000u, 0x80000000u, 0x00000001u, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x84e55160u, 0x7fff7fffu, 0x00ffffffu, 0x00ffffffu, 0x84e55160u, 0x7fff7fffu, 0xffffffffu, 0x7fff7fffu, 0x7fff7ffeu, 0x7fff8000u, 0x7fff7ffeu, 0x7fff8000u, 0x7fff8000u, 0x7fff0000u, 0x7fff0000u, 0x7fff7fffu, 0x7fffffffu, 0x7fffffffu, 0x7fff7fffu, 0x80ff7ffeu, 0x00000000u, 0x81008000u, 0x00000001u, 0x7eff8000u, 0x00000000u}, + {0xdc1bde9bu, 0xe3a40000u, 0xfee7a7b2u, 0xfff7b7b7u, 0xe3d49137u, 0x1c16da37u, 0x47c1a5b2u, 0xfefda5b2u, 0x00001d44u, 0x00001d44u, 0xeae6a7b2u, 0xfffc0000u, 0xffffffffu, 0xe48dda08u, 0x92376822u, 0xdbb9543eu, 0x2f9fcc3bu, 0x2f9fcc3bu, 0x289ef9f0u, 0x6822a6c2u, 0x00000000u, 0x59000000u, 0x000001fdu, 0x00000000u, 0xfffffffdu, 0xffffffffu, 0xfee5a5b2u, 0x75139237u, 0x6822a6b2u, 0x6822a6b2u, 0xfee5a5b2u, 0x75139237u, 0xffffffffu, 0x7fff7fffu, 0x154c38e9u, 0x154ceb85u, 0x154c38e9u, 0x154ceb85u, 0x154c5900u, 0x154c014du, 0x154cff4du, 0x154c9237u, 0x154ca6b2u, 0x154c9237u, 0x154ca6b2u, 0xdd3638e9u, 0x00000000u, 0x0cf0eb85u, 0x00000000u, 0xf30f147bu, 0x00000001u}, + {0x0000004fu, 0x00200000u, 0x0000000fu, 0x0000003fu, 0x00000031u, 0x0000004du, 0x0000008du, 0x0000007fu, 0x0000001fu, 0x0000001fu, 0x00000001u, 0xfffffffeu, 0x010101ffu, 0x007e0000u, 0x00003f00u, 0x0000003fu, 0x00000000u, 0x00000000u, 0x0000004eu, 0x00000007u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x0000003fu, 0x0000000fu, 0x00000001u, 0x0000003fu, 0x0000000fu, 0x0001003fu, 0x0001003fu, 0x7fff0040u, 0x7fff003eu, 0x7fff0040u, 0x7fff003eu, 0x7fff8000u, 0x7fff0000u, 0x7fff0000u, 0x7fff0001u, 0x7fff003fu, 0x7fff0001u, 0x7fff003fu, 0x00000040u, 0x00000000u, 0x0000003eu, 0x00000000u, 0xffffffc2u, 0x00000001u}, + {0x0f9156f7u, 0x7c8ab7a0u, 0x10a80b17u, 0xfee94bdfu, 0xee4140c9u, 0xee4140cdu, 0x80000003u, 0x80000003u, 0x00000001u, 0xffffffffu, 0x10a80b17u, 0x80000000u, 0xdfdfdf36u, 0xa6d5017bu, 0xe94b1536u, 0xad3a4e4bu, 0x2e4f98feu, 0xf7a78d1fu, 0x233a4e4eu, 0x36a80befu, 0x7be00000u, 0x0066d501u, 0x000019b5u, 0x00000000u, 0x000019b5u, 0x00000000u, 0xd8e94b15u, 0x36a80bdfu, 0x00000003u, 0x00000003u, 0xd8e94b15u, 0x36a80bdfu, 0xffffffffu, 0x7fff8000u, 0xffff56f4u, 0xffff3f36u, 0xffff56f4u, 0xffff3f36u, 0xffff7be0u, 0xffff005eu, 0xffff005eu, 0xffff0bdfu, 0xffff4b15u, 0xffff0bdfu, 0xffff4b15u, 0x0f9156f4u, 0x00000001u, 0xa2413f36u, 0x00000000u, 0x5dbec0cau, 0x00000001u}, + {0xc5100379u, 0x8a2006f0u, 0x80000001u, 0xc5100379u, 0xc5100379u, 0xc5100379u, 0xa0000001u, 0xa0000001u, 0x00000000u, 0x00000000u, 0x80000001u, 0x00000000u, 0xff585803u, 0x228801acu, 0x20451003u, 0xa2006b00u, 0x00000008u, 0x00000008u, 0x82006b01u, 0x45100359u, 0x00000000u, 0x45100358u, 0x80000001u, 0x00000000u, 0x80000001u, 0xffffffffu, 0x80000001u, 0x45100358u, 0x00000020u, 0x00000020u, 0x80000001u, 0x45100358u, 0xffff0020u, 0x7fff0020u, 0xffff0378u, 0xfffffcc8u, 0xffff0378u, 0xfffffcc8u, 0xffff0358u, 0xffff0358u, 0xffff0358u, 0xffff0020u, 0xffff0358u, 0xffff0020u, 0xffff0358u, 0x45100378u, 0x00000000u, 0xbaeffcc8u, 0x00000001u, 0x45100338u, 0x00000000u}, + {0x0ed9f6f5u, 0xb37cbbc0u, 0x0c0c0406u, 0x0ecdf6efu, 0x0ec1f6e9u, 0x0ed9f6f5u, 0x0c2c0406u, 0x0c2c0406u, 0x00000000u, 0x00000000u, 0x0c0c0406u, 0x00000000u, 0x00004000u, 0x000b37cau, 0x004002cdu, 0xb37cabc0u, 0x00000000u, 0x00000000u, 0xff88afc6u, 0x02cdf2b0u, 0x02cdf2afu, 0x0c0c0406u, 0x02cdf2afu, 0x0c0c0406u, 0x02cdf2afu, 0x0c0c0406u, 0x00000040u, 0x0c0c0406u, 0x02cdf2afu, 0x00000040u, 0x0c0c0406u, 0x02cdf2afu, 0xffff0040u, 0x7fff0040u, 0x0000f2efu, 0x00000d91u, 0x0000f2efu, 0x00000d91u, 0x0000f2afu, 0x0000f2afu, 0x0000f2afu, 0x00000040u, 0x0000f2afu, 0x0000f2afu, 0x00000040u, 0x02cdf2efu, 0x00000000u, 0xfd320d91u, 0x00000001u, 0x02cdf26fu, 0x00000000u}, + {0x7fff8021u, 0xffff0040u, 0x00000021u, 0x7fff7fffu, 0x7fff7fdfu, 0x7fff7fdfu, 0x80000001u, 0x80000001u, 0x00000000u, 0x00000000u, 0x00000021u, 0x80000000u, 0xffffff7fu, 0xbfffbfffu, 0x217fff7fu, 0x7fef7fdfu, 0x00000010u, 0x00000010u, 0xffef7fe0u, 0x7fff8001u, 0x00000000u, 0xfffefffeu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000001u, 0x7fff7fffu, 0x00000021u, 0x00000001u, 0x7fff7fffu, 0x00000021u, 0xffff0021u, 0x7fff0021u, 0xffff8020u, 0xffff8022u, 0xffff8020u, 0xffff8022u, 0xfffffffeu, 0xffff3fffu, 0xffff3fffu, 0xffff0021u, 0xffff7fffu, 0xffff0021u, 0xffff7fffu, 0x7fff8020u, 0x00000000u, 0x80008022u, 0x00000001u, 0x7fff7fdeu, 0x00000000u}, + {0x93b3a3efu, 0x00000000u, 0xffffffffu, 0xffffffffu, 0x6c4c5c31u, 0x93b3a3cdu, 0xec7fffffu, 0xffffffffu, 0x00000127u, 0xffffff27u, 0x6c4c5c37u, 0xff800000u, 0xffffffffu, 0x276747b2u, 0xb3a3d900u, 0x4523b87fu, 0x0000000du, 0xfffffff6u, 0xf923b87eu, 0x00000029u, 0x2e000000u, 0xfe000000u, 0xffffff80u, 0x0000007fu, 0xffffff80u, 0xffffffffu, 0x93b3a3d9u, 0x00000017u, 0xffffffffu, 0x00000017u, 0xffffffffu, 0x93b3a3d9u, 0x0017ffffu, 0x00178000u, 0x0000a3f0u, 0x0000a3c2u, 0x0000a3f0u, 0x0000a3c2u, 0x00002e00u, 0x00000000u, 0x00000000u, 0x00000017u, 0x0000a3d9u, 0x0000a3d9u, 0x00000017u, 0x93b3a3f0u, 0x00000000u, 0x93b3a3c2u, 0x00000000u, 0x6c4c5c3eu, 0x00000001u}, + {0x00000068u, 0x00000028u, 0x00000041u, 0x00000067u, 0x00000066u, 0x00000066u, 0x0000004eu, 0x0000004eu, 0x00000000u, 0x00000000u, 0x00000041u, 0x000000feu, 0x212121ffu, 0x00000021u, 0x00000021u, 0x000000e7u, 0x00000000u, 0x00000000u, 0x00000127u, 0x00000024u, 0x00001080u, 0x00002000u, 0x80000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0x00000007u, 0x00000040u, 0x00000021u, 0x00000007u, 0x00000040u, 0x00000021u, 0x00210007u, 0x00210007u, 0x00000028u, 0x0000ffe6u, 0x00000028u, 0x0000ffe6u, 0x00001080u, 0x00000000u, 0x00000000u, 0x00000007u, 0x00000021u, 0x00000007u, 0x00000021u, 0x00000028u, 0x00000000u, 0xffffffe6u, 0x00000001u, 0x0000001au, 0x00000000u}, + {0x0000ffffu, 0xffffffffu, 0x00010000u, 0xffffffffu, 0xfffeffffu, 0x0000ffffu, 0x80007fffu, 0x7fff7fffu, 0x00000000u, 0x00000000u, 0x00000000u, 0x7fffffffu, 0x00800000u, 0x80008000u, 0x80008000u, 0x3fff8000u, 0x3fffffffu, 0xc0008000u, 0xc0008000u, 0x8000801eu, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x80008000u, 0x7fff7fffu, 0x00010000u, 0x00010000u, 0x80008000u, 0x7fff7fffu, 0xffffffffu, 0x80007fffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x0000ffffu, 0x00000000u, 0x00000001u, 0x0000ffffu, 0x00007fffu, 0x00008000u, 0x00008000u, 0x00007fffu, 0xffffffffu, 0x00000000u, 0xfffeffffu, 0x00000001u, 0x00010001u, 0x00000000u}, + {0x0000007eu, 0x80000000u, 0x0000003fu, 0x0000003fu, 0x00000000u, 0x0000007eu, 0x0000005eu, 0x0000003fu, 0x0000001fu, 0x0000001fu, 0x00000020u, 0x7fffffffu, 0x202020ffu, 0x0000003eu, 0x00001f00u, 0x000003e0u, 0x00000000u, 0x00000000u, 0x0000041fu, 0x00000025u, 0x00000000u, 0x80000010u, 0x0000007eu, 0x00000000u, 0x0000007eu, 0x00000000u, 0x0000001fu, 0x0000003fu, 0x00000020u, 0x0000001fu, 0x0000003fu, 0x00000020u, 0x0020001fu, 0x0020001fu, 0x3253003fu, 0x3253ffffu, 0x3253003fu, 0x3253ffffu, 0x32530000u, 0x32530000u, 0x32530000u, 0x3253001fu, 0x32530020u, 0x3253001fu, 0x32530020u, 0x0000003fu, 0x00000000u, 0xffffffffu, 0x00000001u, 0x00000001u, 0x00000000u} +}; +constexpr const char* Names[51] = {"add3_u32", "add_lshl_u32", "and_or_b32", "or3_b32", "xor3_b32", "xad_u32", "lshl_add_u32", "lshl_or_b32", "bfe_u32", "bfe_i32", "bfi_b32", "bfm_b32", "perm_b32", "alignbit_b32", "alignbyte_b32", "mul_lo_u32", "mul_hi_u32", "mul_hi_i32", "mad_i32_i24", "bcnt_u32_b32", "lshlrev_b64 lo", "lshlrev_b64 hi", "lshrrev_b64 lo", "lshrrev_b64 hi", "ashrrev_i64 lo", "ashrrev_i64 hi", "min3_i32", "max3_i32", "med3_i32", "min3_u32", "max3_u32", "med3_u32", "cvt_pk_u16_u32", "cvt_pk_i16_i32", "add_nc_u16", "sub_nc_u16", "add_nc_i16", "sub_nc_i16", "lshlrev_b16", "lshrrev_b16", "ashrrev_i16", "min_u16", "max_u16", "min_i16", "max_i16", "add_co_u32", "add_co_u32 carry", "sub_co_u32", "sub_co_u32 carry", "subrev_co_u32", "subrev_co_u32 carry"}; + +void Fill(std::uint32_t tid, std::uint32_t* words) { + std::copy(std::begin(Rows[tid]), std::end(Rows[tid]), words); +} + +std::array BufferDescriptor(const void* data, std::uint32_t bytes) { + const auto address = reinterpret_cast(data); + return {static_cast(address), static_cast((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; +} + +void Expect(std::uint32_t tid, std::uint32_t actual, std::uint32_t expected, const char* name) { + Require(actual == expected, std::string("vop3 integer alu: 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) Fill(tid, &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + std::vector userData(8, 0u); + const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); + const auto output = BufferDescriptor(Output.data(), static_cast(Output.size() * 4u)); + std::copy(input.begin(), input.end(), userData.begin()); + std::copy(output.begin(), output.end(), userData.begin() + 4); + const std::span code(Code); + const std::array memory{{{reinterpret_cast(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(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(code.data())); + device.WaitIdle(); +} + +void Check() { + 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 i = 0; i < 51; ++i) Expect(tid, out[i], Expected[tid][i], Names[i]); + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device); + Check(); + std::puts("vop3 integer alu tests passed"); + return 0; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/libs/prx/libSceAjm.native/Export.cpp b/core/libs/prx/libSceAjm.native/Export.cpp index 62c0bc44e..fcab9e41a 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -14,18 +14,6 @@ static void AjmStub(const char* name) { extern "C" { -int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size) { - (void)info; - (void)instance; - (void)flags; - (void)sideband_input; - (void)sideband_input_size; - (void)sideband_output; - (void)sideband_output_size; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobDecodeSplit(AjmBatchInfo* info, uint32_t instance, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* result) { (void)info; (void)instance; @@ -50,32 +38,6 @@ int APS5_VABI sceAjmBatchJobEncode(AjmBatchInfo* info, uint32_t instance, const return 0; } -int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo* info, uint32_t instance, void* result) { - (void)info; - (void)instance; - (void)result; - AjmStub(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { - (void)info; - (void)instance; - (void)result; - AjmStub(__func__); - return 0; -} - -int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { - (void)info; - (void)instance; - (void)ratio; - (void)flags; - (void)result; - AjmStub(__func__); - return 0; -} - int APS5_VABI sceAjmBatchJobSetResampleParametersEx(AjmBatchInfo* info, uint32_t instance, float ratio_start, float ratio_change_per_sample, uint32_t flags, void* result) { (void)info; (void)instance; @@ -93,4 +55,16 @@ const char* APS5_VABI sceAjmStrError(int error) { return nullptr; } +APS5_EXPORT("Z4HDpe1ZEa4", sceAjmWVorbisUnknown00); +int APS5_VABI sceAjmWVorbisUnknown00(void) { + NotImplemented_nid_no_patch("Z4HDpe1ZEa4"); + return 0; +} + +APS5_EXPORT("oXShHcqMcX0", sceAjmWVorbisUnknown01); +int APS5_VABI sceAjmWVorbisUnknown01(void) { + NotImplemented_nid_no_patch("oXShHcqMcX0"); + return 0; +} + } diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 9b5542631..bff995503 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -56,6 +56,8 @@ bool TraceEnabled() { constexpr std::uint64_t RUN_GET_CODEC_INFO = 1ull << 11; constexpr std::uint64_t RUN_MULTIPLE_FRAMES = 1ull << 12; +constexpr std::uint64_t CONTROL_RESET = 1ull << 13; +constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45; constexpr std::uint64_t SIDEBAND_FORMAT = 1ull << 46; constexpr std::uint64_t SIDEBAND_STREAM = 1ull << 47; @@ -98,6 +100,18 @@ struct SidebandAt9CodecInfo { std::uint32_t frameSamples; }; +struct SidebandResampleInfo { + float ratio; + std::int32_t samples; + std::uint32_t reserved[8]; +}; + +struct Resampler { + float ratio = 1.0f; + std::vector frames; + double position = 1.0; +}; + struct Instance { std::uint32_t codec = 0; std::uint64_t flags = 0; @@ -117,6 +131,7 @@ struct Instance { std::uint32_t opusChannels = 0; std::uint32_t opusSampleRate = 0; std::vector opusPending; + Resampler resampler; ~Instance() { if (decoder) Atrac9ReleaseHandle(decoder); @@ -139,6 +154,9 @@ enum class JobKind : std::uint32_t { SetGaplessDecode = 3, Run = 4, GetStatistics = 5, + Control = 6, + SetResampleParameters = 7, + GetResampleInfo = 8, }; struct JobHeader { @@ -233,7 +251,13 @@ bool ResetDecoder(Instance& instance) { void OpenOpus(Instance& instance); +void ClearResampler(Instance& instance) { + instance.resampler.frames.clear(); + instance.resampler.position = 1.0; +} + std::int32_t InitializeInstance(Instance& instance, const std::uint8_t* parameters, std::uint64_t size) { + ClearResampler(instance); if (instance.codec == CODEC_OPUS) { if (size < 8) return AJM_RESULT_INVALID_PARAMETER; std::uint32_t channels = 0; @@ -333,6 +357,82 @@ std::uint32_t PcmEncoding(const Instance& instance) { return static_cast((instance.flags >> 7u) & 7u); } +double PcmValue(const std::uint8_t* at, std::uint32_t encoding) { + if (encoding == 0) { + std::int16_t value = 0; + std::memcpy(&value, at, sizeof(value)); + return value / 32768.0; + } + if (encoding == 1) { + std::int32_t value = 0; + std::memcpy(&value, at, sizeof(value)); + return value / 2147483648.0; + } + float value = 0; + std::memcpy(&value, at, sizeof(value)); + return value; +} + +void StorePcm(double value, std::uint8_t* out, std::uint32_t encoding) { + if (encoding == 0) { + const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); + std::memcpy(out, &converted, sizeof(converted)); + } else if (encoding == 1) { + const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); + std::memcpy(out, &converted, sizeof(converted)); + } else { + const auto converted = static_cast(value); + std::memcpy(out, &converted, sizeof(converted)); + } +} + +bool Resampling(const Instance& instance) { + return instance.resampler.ratio != 1.0f || !instance.resampler.frames.empty(); +} + +std::size_t ResamplerHeld(const Instance& instance, std::size_t channels) { + const auto& resampler = instance.resampler; + if (resampler.frames.empty() || channels == 0) return 0; + const std::size_t frames = resampler.frames.size() / channels; + const auto next = static_cast(resampler.position); + return frames > next ? frames - next : 0; +} + +void ResamplerPush(Instance& instance, const double* samples, std::size_t frames, std::size_t channels) { + auto& stored = instance.resampler.frames; + if (frames == 0) return; + if (stored.empty()) stored.insert(stored.end(), samples, samples + channels); + stored.insert(stored.end(), samples, samples + frames * channels); +} + +void ResamplerProduce(Instance& instance, PcmOutputs& outputs, std::size_t channels, std::uint32_t encoding, std::size_t sampleBytes) { + auto& resampler = instance.resampler; + if (resampler.frames.empty() || channels == 0) return; + const std::size_t frameBytes = channels * sampleBytes; + const std::size_t available = resampler.frames.size() / channels; + std::vector frame(frameBytes); + while (outputs.Room() >= frameBytes) { + const auto index = static_cast(resampler.position); + if (index + 2 >= available) break; + const double t = resampler.position - static_cast(index); + for (std::size_t channel = 0; channel < channels; ++channel) { + const double p0 = resampler.frames[(index - 1) * channels + channel]; + const double p1 = resampler.frames[index * channels + channel]; + const double p2 = resampler.frames[(index + 1) * channels + channel]; + const double p3 = resampler.frames[(index + 2) * channels + channel]; + const double value = p1 + 0.5 * t * (p2 - p0 + t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3 + t * (3.0 * (p1 - p2) + p3 - p0))); + StorePcm(value, frame.data() + channel * sampleBytes, encoding); + } + outputs.Emit(frame.data(), frameBytes); + resampler.position += resampler.ratio; + } + const auto index = std::min(static_cast(resampler.position), available); + if (index > 1) { + resampler.frames.erase(resampler.frames.begin(), resampler.frames.begin() + static_cast((index - 1) * channels)); + resampler.position -= static_cast(index - 1); + } +} + void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, std::size_t samples, std::size_t channels, std::size_t sampleBytes) { std::size_t first = 0; std::size_t count = samples; @@ -346,8 +446,16 @@ void EmitFrame(Instance& instance, PcmOutputs& outputs, const std::uint8_t* pcm, const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; count = static_cast(std::min(count, remaining)); } - outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); instance.totalDecodedSamples += count; + if (!Resampling(instance)) { + outputs.Emit(pcm + first * channels * sampleBytes, count * channels * sampleBytes); + return; + } + const auto encoding = PcmEncoding(instance); + std::vector values(count * channels); + for (std::size_t index = 0; index < values.size(); ++index) values[index] = PcmValue(pcm + (first * channels + index) * sampleBytes, encoding); + ResamplerPush(instance, values.data(), count, channels); + ResamplerProduce(instance, outputs, channels, encoding, sampleBytes); } SidebandFormat CurrentFormat(const Instance& instance) { @@ -416,6 +524,7 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c // reported as partial input, and the title resubmits it from the consumed offset. const auto superframeSize = static_cast(instance.info.superframeSize); const auto framesInSuperframe = static_cast(std::max(1, instance.info.framesInSuperframe)); + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); for (;;) { if (instance.superframeRemaining == 0) consumed += RiffDataOffset(input.data() + consumed, input.size() - consumed); const std::size_t needed = instance.superframeRemaining == 0 ? superframeSize : instance.superframeRemaining; @@ -423,8 +532,8 @@ void RunAt9(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c if (input.size() != consumed) result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (pcmOutputs.Room() < frameBytes) { - if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + if (Resampling(instance) ? pcmOutputs.Room() < channels * sampleBytes : pcmOutputs.Room() < frameBytes) { + if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } if (instance.superframeRemaining == 0) { @@ -644,6 +753,7 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c std::int32_t result = 0; std::size_t consumed = 0; std::uint32_t frames = 0; + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, instance.mp3Channels, encoding, sampleBytes); while (consumed < input.size()) { Mp3Frame frame{}; if (!ParseMp3Frame(input.data() + consumed, input.size() - consumed, frame)) { @@ -654,8 +764,8 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c result |= AJM_RESULT_PARTIAL_INPUT; break; } - if (pcmOutputs.Room() < frame.samples * frame.channels * sampleBytes) { - if (frames == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; + if (pcmOutputs.Room() < (Resampling(instance) ? 1 : frame.samples) * frame.channels * sampleBytes) { + if (frames == 0 && pcmOutputs.produced == 0) result |= AJM_RESULT_NOT_ENOUGH_ROOM; break; } packet->data = const_cast(input.data() + consumed); @@ -701,7 +811,7 @@ void RunMp3(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, c } if ((job.flags & RUN_MULTIPLE_FRAMES) == 0) break; } - if (consumed == input.size() && frames == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; + if (consumed == input.size() && frames == 0 && pcmOutputs.produced == 0 && result == 0) result |= AJM_RESULT_PARTIAL_INPUT; AJM_TRACE("[ajm] instance %u mp3 run flags 0x%llx: %zu input bytes, %zu output bytes -> result 0x%x, %u frames, consumed %zu, produced %zu, %u channels, %u Hz, %u bps, total samples %llu\n", job.instance, static_cast(job.flags), input.size(), pcmOutputs.capacity, static_cast(result), frames, consumed, pcmOutputs.produced, instance.mp3Channels, instance.mp3SampleRate, instance.mp3Bitrate, static_cast(instance.totalDecodedSamples)); @@ -754,12 +864,19 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, std::int32_t result = 0; std::size_t consumed = 0; std::uint32_t frames = 0; + const std::size_t frameBytes = channels * sampleBytes; + if (Resampling(instance) && !instance.opusPending.empty()) { + std::vector values(instance.opusPending.size() / sampleBytes); + for (std::size_t index = 0; index < values.size(); ++index) values[index] = PcmValue(instance.opusPending.data() + index * sampleBytes, encoding); + ResamplerPush(instance, values.data(), values.size() / channels, channels); + instance.opusPending.clear(); + } + if (Resampling(instance)) ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); if (!instance.opusPending.empty()) { const std::size_t bytes = std::min(instance.opusPending.size(), pcmOutputs.Room()); pcmOutputs.Emit(instance.opusPending.data(), bytes); instance.opusPending.erase(instance.opusPending.begin(), instance.opusPending.begin() + static_cast(bytes)); } - const std::size_t frameBytes = channels * sampleBytes; while (consumed < input.size() && instance.opusPending.empty()) { if (input.size() - consumed < 2) { result |= AJM_RESULT_PARTIAL_INPUT; @@ -794,24 +911,11 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, if (format != AV_SAMPLE_FMT_FLTP && format != AV_SAMPLE_FMT_FLT) throw std::runtime_error("AJM Opus: FFmpeg sample format " + std::to_string(decoded->format) + " is not converted"); const auto frameChannels = static_cast(decoded->ch_layout.nb_channels); const auto frameSamples = static_cast(decoded->nb_samples); - pcm.assign(frameSamples * channels * sampleBytes, 0); - for (std::size_t sample = 0; sample < frameSamples; ++sample) { - for (std::size_t channel = 0; channel < channels; ++channel) { - const std::size_t sourceChannel = std::min(channel, frameChannels - 1); - const float value = format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[sourceChannel])[sample] - : reinterpret_cast(decoded->data[0])[sample * frameChannels + sourceChannel]; - auto* out = pcm.data() + (sample * channels + channel) * sampleBytes; - if (encoding == 0) { - const auto converted = static_cast(std::clamp(std::lrint(value * 32768.0), -32768L, 32767L)); - std::memcpy(out, &converted, sizeof(converted)); - } else if (encoding == 1) { - const auto converted = static_cast(std::clamp(std::llrint(value * 2147483648.0), -2147483648LL, 2147483647LL)); - std::memcpy(out, &converted, sizeof(converted)); - } else { - std::memcpy(out, &value, sizeof(value)); - } - } - } + const auto value = [&](std::size_t sample, std::size_t channel) { + const std::size_t sourceChannel = std::min(channel, frameChannels - 1); + return format == AV_SAMPLE_FMT_FLTP ? reinterpret_cast(decoded->extended_data[sourceChannel])[sample] + : reinterpret_cast(decoded->data[0])[sample * frameChannels + sourceChannel]; + }; std::size_t first = 0; std::size_t count = frameSamples; if (instance.gapless.skippedSamples < instance.gapless.skipSamples) { @@ -824,12 +928,23 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, const std::uint64_t remaining = instance.gapless.totalSamples > instance.totalDecodedSamples ? instance.gapless.totalSamples - instance.totalDecodedSamples : 0; count = static_cast(std::min(count, remaining)); } - const auto* start = pcm.data() + first * frameBytes; - const std::size_t total = count * frameBytes; - const std::size_t fits = std::min(total, pcmOutputs.Room() / frameBytes * frameBytes); - pcmOutputs.Emit(start, fits); - instance.opusPending.insert(instance.opusPending.end(), start + fits, start + total); instance.totalDecodedSamples += count; + if (Resampling(instance)) { + std::vector values(count * channels); + for (std::size_t sample = 0; sample < count; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) values[sample * channels + channel] = value(first + sample, channel); + } + ResamplerPush(instance, values.data(), count, channels); + ResamplerProduce(instance, pcmOutputs, channels, encoding, sampleBytes); + } else { + pcm.assign(count * frameBytes, 0); + for (std::size_t sample = 0; sample < count; ++sample) { + for (std::size_t channel = 0; channel < channels; ++channel) StorePcm(value(first + sample, channel), pcm.data() + (sample * channels + channel) * sampleBytes, encoding); + } + const std::size_t fits = std::min(pcm.size(), pcmOutputs.Room() / frameBytes * frameBytes); + pcmOutputs.Emit(pcm.data(), fits); + instance.opusPending.insert(instance.opusPending.end(), pcm.begin() + static_cast(fits), pcm.end()); + } av_frame_unref(decoded.get()); } if (status != AVERROR(EAGAIN)) { @@ -844,6 +959,51 @@ void RunOpus(Instance& instance, const JobHeader& job, const AjmBuffer* inputs, WriteRunSideband(job, instance, result, consumed, pcmOutputs.produced, frames, CurrentFormat(instance)); } +void ClearContext(Instance& instance) { + instance.totalDecodedSamples = 0; + instance.gapless.skippedSamples = 0; + if (instance.decoder && instance.initialized) ResetDecoder(instance); + if (instance.mp3) avcodec_flush_buffers(instance.mp3); + if (instance.opus) avcodec_flush_buffers(instance.opus); + instance.opusPending.clear(); + ClearResampler(instance); +} + +std::size_t ControlInitializeSize(std::uint32_t codec) { + switch (codec) { + case CODEC_MP3: return 0; + case CODEC_AT9: return 8; + case CODEC_OPUS: return 12; + default: NotImplemented_nid_no_patch("sceAjmBatchJobControl (INITIALIZE for a codec other than MP3, ATRAC9 and Opus)"); return 0; + } +} + +void Control(Instance& instance, const JobHeader& job, const AjmBuffer* inputs) { + const auto* input = job.inputCount ? static_cast(inputs[0].ptr) : nullptr; + const std::size_t inputSize = job.inputCount ? inputs[0].size : 0; + const std::size_t gaplessSize = (job.flags & SIDEBAND_GAPLESS_DECODE) ? sizeof(SidebandGaplessDecode) : 0; + const std::size_t initializeSize = (job.flags & CONTROL_INITIALIZE) ? ControlInitializeSize(instance.codec) : 0; + if (inputSize != gaplessSize + initializeSize) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband input size other than the gapless decode and the codec's initialize parameters)"); + std::int32_t result = 0; + if (job.flags & CONTROL_RESET) ClearContext(instance); + if (job.flags & CONTROL_INITIALIZE) result = InitializeInstance(instance, input + gaplessSize, initializeSize); + if (gaplessSize) { + SidebandGaplessDecode gapless{}; + std::memcpy(&gapless, input, sizeof(gapless)); + instance.gapless.totalSamples = gapless.totalSamples; + instance.gapless.skipSamples = gapless.skipSamples; + } + AJM_TRACE("[ajm] instance %u control flags 0x%llx: %zu sideband input bytes -> result 0x%x, gapless total %u skip %u skipped %u\n", job.instance, static_cast(job.flags), inputSize, static_cast(result), instance.gapless.totalSamples, instance.gapless.skipSamples, instance.gapless.skippedSamples); + WriteResult(job.sideband, job.sidebandSize, result); +} + +std::size_t HeldSamples(const Instance& instance) { + const std::size_t channels = instance.codec == CODEC_OPUS ? instance.opusChannels : instance.codec == CODEC_MP3 ? instance.mp3Channels : static_cast(std::max(0, instance.info.channels)); + if (Resampling(instance)) return ResamplerHeld(instance, channels); + const std::size_t sampleBytes = PcmEncoding(instance) == 0 ? sizeof(std::int16_t) : sizeof(std::int32_t); + return channels == 0 ? 0 : instance.opusPending.size() / (channels * sampleBytes); +} + void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* outputs) { if (job.kind == JobKind::GetStatistics) { AJM_TRACE("[ajm] statistics job: sideband %llu bytes\n", static_cast(job.sidebandSize)); @@ -866,12 +1026,7 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out } case JobKind::ClearContext: AJM_TRACE("[ajm] instance %u clear context (sideband %llu bytes)\n", job.instance, static_cast(job.sidebandSize)); - instance->totalDecodedSamples = 0; - instance->gapless.skippedSamples = 0; - if (instance->decoder && instance->initialized) ResetDecoder(*instance); - if (instance->mp3) avcodec_flush_buffers(instance->mp3); - if (instance->opus) avcodec_flush_buffers(instance->opus); - instance->opusPending.clear(); + ClearContext(*instance); WriteResult(job.sideband, job.sidebandSize, 0); break; case JobKind::SetGaplessDecode: { @@ -884,6 +1039,20 @@ void Execute(const JobHeader& job, const AjmBuffer* inputs, const AjmBuffer* out WriteResult(job.sideband, job.sidebandSize, 0); break; } + case JobKind::Control: + Control(*instance, job, inputs); + break; + case JobKind::SetResampleParameters: + std::memcpy(&instance->resampler.ratio, job.parameters, sizeof(instance->resampler.ratio)); + AJM_TRACE("[ajm] instance %u set resample ratio %f\n", job.instance, static_cast(instance->resampler.ratio)); + WriteResult(job.sideband, job.sidebandSize, 0); + break; + case JobKind::GetResampleInfo: { + const SidebandResampleInfo resample{instance->resampler.ratio, static_cast(HeldSamples(*instance)), {}}; + WriteResult(job.sideband, job.sidebandSize, 0); + if (job.sideband && job.sidebandSize >= sizeof(SidebandResult) + sizeof(resample)) std::memcpy(static_cast(job.sideband) + sizeof(SidebandResult), &resample, sizeof(resample)); + break; + } case JobKind::Run: if (!instance->initialized) { AJM_TRACE("[ajm] instance %u run before initialize\n", job.instance); @@ -1029,6 +1198,19 @@ int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo* info, uint32_t instan return Append(info, header, nullptr, nullptr); } +int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const void* sideband_input, size_t sideband_input_size, void* sideband_output, size_t sideband_output_size) { + constexpr std::uint64_t supported = CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; + if (flags == 0 || (flags & ~supported) != 0) NotImplemented_nid_no_patch("sceAjmBatchJobControl (flags other than a combination of RESET, INITIALIZE and SIDEBAND_GAPLESS_DECODE)"); + if ((flags & SIDEBAND_GAPLESS_DECODE) != 0 && (flags & CONTROL_RESET) == 0) NotImplemented_nid_no_patch("sceAjmBatchJobControl (SIDEBAND_GAPLESS_DECODE without RESET)"); + if (sideband_output_size != sizeof(SidebandResult)) NotImplemented_nid_no_patch("sceAjmBatchJobControl (sideband output other than the 8-byte result)"); + if (sideband_input_size != 0 && !sideband_input) NotImplemented_nid_no_patch("sceAjmBatchJobControl (null sideband input)"); + auto header = MakeHeader(JobKind::Control, instance, sideband_output, sideband_output_size); + header.flags = flags; + header.inputCount = sideband_input_size != 0 ? 1 : 0; + const AjmBuffer input{const_cast(sideband_input), sideband_input_size}; + return Append(info, header, &input, nullptr); +} + int APS5_VABI sceAjmBatchJobRunSplit(AjmBatchInfo* info, uint32_t instance, uint64_t flags, const AjmBuffer* input_buffers, size_t input_buffers_num, const AjmBuffer* output_buffers, size_t output_buffers_num, void* sideband_output, size_t sideband_output_size) { auto header = MakeHeader(JobKind::Run, instance, sideband_output, sideband_output_size); header.flags = flags; @@ -1043,6 +1225,19 @@ int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo* info, uint32_t instance, uint64_t return sceAjmBatchJobRunSplit(info, instance, flags, &input, 1, &output, 1, sideband_output, sideband_output_size); } +int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo* info, uint32_t instance, float ratio, uint32_t flags, void* result) { + (void)flags; + if (!std::isfinite(ratio) || ratio <= 0.0f) return SCE_AJM_ERROR_INVALID_PARAMETER; + auto header = MakeHeader(JobKind::SetResampleParameters, instance, result, sizeof(SidebandResult)); + std::memcpy(header.parameters, &ratio, sizeof(ratio)); + header.parameterSize = sizeof(ratio); + return Append(info, header, nullptr, nullptr); +} + +int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo* info, uint32_t instance, void* result) { + return Append(info, MakeHeader(JobKind::GetResampleInfo, instance, result, sizeof(SidebandResult) + sizeof(SidebandResampleInfo)), nullptr, nullptr); +} + int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo* info, uint32_t instance, const void* bitstream_input, size_t bitstream_input_size, void* pcm_output, size_t pcm_output_size, void* result) { return sceAjmBatchJobRun(info, instance, SIDEBAND_STREAM, bitstream_input, bitstream_input_size, pcm_output, pcm_output_size, result, sizeof(SidebandResult) + sizeof(SidebandStream)); } @@ -1055,6 +1250,10 @@ int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo* info, uint32_t instan return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_GAPLESS_DECODE, nullptr, 0, nullptr, 0, result, sizeof(SidebandResult) + sizeof(SidebandGaplessDecode)); } +int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo* info, uint32_t instance, void* result) { + return sceAjmBatchJobRunSplit(info, instance, SIDEBAND_FORMAT, nullptr, 0, nullptr, 0, result, sizeof(SidebandResult) + sizeof(SidebandFormat)); +} + int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo* info, uint32_t instance, void* result, size_t result_size) { return sceAjmBatchJobRunSplit(info, instance, RUN_GET_CODEC_INFO, nullptr, 0, nullptr, 0, result, result_size); } diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index dfc8300ad..3a025d498 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -11,6 +11,7 @@ static constexpr int SCE_APP_CONTENT_ERROR_PARAMETER = static_cast(0x80D90002); static constexpr int SCE_APP_CONTENT_ERROR_NOT_FOUND = static_cast(0x80D90005); +static constexpr int SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT = static_cast(0x80D90007); static constexpr uint32_t APPPARAM_ID_SKU_FLAG = 1; static constexpr int32_t SKU_FLAG_FULL = 3; @@ -18,6 +19,11 @@ static constexpr char TEMPORARY_MOUNT_POINT[] = "/temp0"; static constexpr char DOWNLOAD_MOUNT_POINT[] = "/download0"; static constexpr uint32_t TEMPORARY_DATA_OPTION_FORMAT = 1; +struct AppContentAddcontInfo { + NpUnifiedEntitlementLabel entitlement_label; + uint32_t status; +}; + static std::filesystem::path TemporaryDirectory(const AppContentMountPoint* mount_point) { if (!mount_point || std::strncmp(mount_point->data, TEMPORARY_MOUNT_POINT, sizeof(mount_point->data)) != 0) APS5_INVALID_ARG_EX; return ResolvePath_nid_no_patch(TEMPORARY_MOUNT_POINT); @@ -40,6 +46,19 @@ int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint* mount_poin return SCE_APP_CONTENT_ERROR_NOT_FOUND; } +int APS5_VABI sceAppContentGetAddcontInfo(uint32_t service_label, const NpUnifiedEntitlementLabel* entitlement_label, AppContentAddcontInfo* info) { + (void)service_label; + if (!entitlement_label || !info) return SCE_APP_CONTENT_ERROR_PARAMETER; + return SCE_APP_CONTENT_ERROR_DRM_NO_ENTITLEMENT; +} + +int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t service_label, AppContentAddcontInfo* list, uint32_t list_num, uint32_t* hit_num) { + (void)service_label; + if ((!list || list_num == 0) && !hit_num) return SCE_APP_CONTENT_ERROR_PARAMETER; + if (hit_num) *hit_num = 0; + return 0; +} + int APS5_VABI sceAppContentAppParamGetInt(uint32_t param_id, int32_t* value) { if (!value) return SCE_APP_CONTENT_ERROR_PARAMETER; switch (param_id) { diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index 705f81119..5254ad3c7 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -13,12 +14,14 @@ namespace { constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); +constexpr int AUDIO3D_ERROR_INVALID_OBJECT = static_cast(0x80EA0003); constexpr int AUDIO3D_ERROR_INVALID_PARAMETER = static_cast(0x80EA0004); constexpr int AUDIO3D_ERROR_OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int AUDIO3D_ERROR_NOT_READY = static_cast(0x80EA0007); constexpr int AUDIO3D_USER_ID_SYSTEM = 0xFF; constexpr std::uint32_t AUDIO3D_PORT_ID = 0; +constexpr std::uint32_t AUDIO3D_OBJECT_INVALID = 0xFFFFFFFF; constexpr std::uint32_t AUDIO3D_RATE_48000 = 0; constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000; constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2; @@ -36,6 +39,9 @@ struct Audio3dPort { Clock::duration frame_duration{}; std::uint32_t advanced = 0; std::deque playing; + std::uint32_t max_objects = 0; + std::uint32_t next_object_id = 0; + std::set objects; }; std::mutex g_mutex; @@ -74,6 +80,13 @@ int APS5_VABI sceAudio3dInitialize(int64_t reserved) { return 0; } +int APS5_VABI sceAudio3dTerminate() { + std::lock_guard lock(g_mutex); + if (!g_initialized || g_port.open) return AUDIO3D_ERROR_NOT_READY; + g_initialized = false; + return 0; +} + int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { std::lock_guard lock(g_mutex); if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; @@ -129,6 +142,7 @@ int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* param g_port = Audio3dPort{}; g_port.open = true; g_port.queue_depth = parameters->queue_depth; + g_port.max_objects = parameters->max_objects; g_port.frame_duration = std::chrono::duration_cast( std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); *id = AUDIO3D_PORT_ID; @@ -172,4 +186,25 @@ int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id } } +int APS5_VABI sceAudio3dObjectReserve(uint32_t port_id, uint32_t* object_id) { + if (object_id == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + *object_id = AUDIO3D_OBJECT_INVALID; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (g_port.objects.size() >= g_port.max_objects) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + do { + ++g_port.next_object_id; + } while (g_port.next_object_id == 0 || g_port.next_object_id == AUDIO3D_OBJECT_INVALID || g_port.objects.contains(g_port.next_object_id)); + g_port.objects.insert(g_port.next_object_id); + *object_id = g_port.next_object_id; + return 0; +} + +int APS5_VABI sceAudio3dObjectUnreserve(uint32_t port_id, uint32_t object_id) { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (g_port.objects.erase(object_id) == 0) return AUDIO3D_ERROR_INVALID_OBJECT; + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp index f658ddf10..67d66b44b 100644 --- a/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp +++ b/core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp @@ -3,7 +3,8 @@ #include "prx/libc/include/General.hpp" static constexpr int USER_ID_SYSTEM = 0xFF; -static constexpr std::uint32_t SUPPORTED_3D_LATENCY = 2; +static constexpr std::uint32_t MIN_SUPPORTED_3D_LATENCY = 1; +static constexpr std::uint32_t MAX_SUPPORTED_3D_LATENCY = 2; extern "C" { @@ -12,7 +13,7 @@ int APS5_VABI sceAudioOut2Initialize(void) { } int APS5_VABI sceAudioOut2Set3DLatency(int userId, std::uint32_t latency) { - if (userId != USER_ID_SYSTEM || latency != SUPPORTED_3D_LATENCY) NotImplemented_nid_no_patch(__func__); + if (userId != USER_ID_SYSTEM || latency < MIN_SUPPORTED_3D_LATENCY || latency > MAX_SUPPORTED_3D_LATENCY) NotImplemented_nid_no_patch(__func__); return 0; } @@ -28,4 +29,9 @@ int APS5_VABI sceAudioOut2SetSystemDebugState(const AudioOut2SystemDebugStatePar return 0; } +int APS5_VABI sceAudioOut2UserGetSupportedAttributes(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp index 7f3404f2a..c0882d45c 100644 --- a/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp +++ b/core/libs/prx/libSceAudioOut/tests/AudioOut2Latency.cpp @@ -35,7 +35,8 @@ constexpr int user = 0x10000000; void TestSet3DLatency() { Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "latency 2 for the system user must be accepted"); Require(sceAudioOut2Set3DLatency(systemUser, 2) == 0, "latency 2 must be accepted again"); - Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(systemUser, 1); }), "latency 1 must throw"); + Require(sceAudioOut2Set3DLatency(systemUser, 1) == 0, "latency 1 for the system user must be accepted"); + Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(systemUser, 0); }), "latency 0 must throw"); Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(systemUser, 3); }), "latency 3 must throw"); Require(ThrowsRuntimeError([] { sceAudioOut2Set3DLatency(user, 2); }), "a user other than the system user must throw"); } diff --git a/core/libs/prx/libSceAudiodec.native/CMakeLists.txt b/core/libs/prx/libSceAudiodec.native/CMakeLists.txt new file mode 100644 index 000000000..a4e07a5c0 --- /dev/null +++ b/core/libs/prx/libSceAudiodec.native/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAudiodec/AudiodecLibrary.cmake) +add_sce_audiodec_library(libSceAudiodec.native) diff --git a/core/libs/prx/libSceAudiodec.native/Export.cpp b/core/libs/prx/libSceAudiodec.native/Export.cpp deleted file mode 100644 index fb34206c2..000000000 --- a/core/libs/prx/libSceAudiodec.native/Export.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -APS5_DUMMY_FUN - -} diff --git a/core/libs/prx/libSceAudiodec/AudiodecLibrary.cmake b/core/libs/prx/libSceAudiodec/AudiodecLibrary.cmake new file mode 100644 index 000000000..b806bb8c0 --- /dev/null +++ b/core/libs/prx/libSceAudiodec/AudiodecLibrary.cmake @@ -0,0 +1,14 @@ +function(add_sce_audiodec_library target) + set(audiodecDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${audiodecDir}/Export.cpp + ${audiodecDir}/src/Codecs.cpp + ) + target_include_directories(${target} PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src + ) + target_link_libraries(${target} PRIVATE atrac9 ffmpeg libc libkernel) + set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceAudiodec/CMakeLists.txt b/core/libs/prx/libSceAudiodec/CMakeLists.txt index 0283a14a3..b851e4298 100644 --- a/core/libs/prx/libSceAudiodec/CMakeLists.txt +++ b/core/libs/prx/libSceAudiodec/CMakeLists.txt @@ -1,14 +1,5 @@ -add_library(libSceAudiodec SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Codecs.cpp -) -target_include_directories(libSceAudiodec PRIVATE - ${LIBS_INCLUDE_DIR} - ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src -) -target_link_libraries(libSceAudiodec PRIVATE atrac9 ffmpeg libc libkernel) -set_target_properties(libSceAudiodec PROPERTIES CXX_EXTENSIONS OFF) -configure_windows_unwind(libSceAudiodec) +include(${CMAKE_CURRENT_SOURCE_DIR}/AudiodecLibrary.cmake) +add_sce_audiodec_library(libSceAudiodec) if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) diff --git a/core/libs/prx/libSceAvPlayer.native/Export.cpp b/core/libs/prx/libSceAvPlayer.native/Export.cpp deleted file mode 100644 index 25dfc65c5..000000000 --- a/core/libs/prx/libSceAvPlayer.native/Export.cpp +++ /dev/null @@ -1,183 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libSceAvPlayer/include/AvPlayer.hpp" - -using namespace AvPlayer; - -namespace { - -Player* ToPlayer(AvPlayerInternal* h) { - return static_cast(h); -} - -std::uint32_t VideoBufferCount(std::int32_t requested) { - return static_cast(std::clamp(requested, 2, 16)); -} - -} - -#pragma GCC visibility push(default) -extern "C" { - -int APS5_VABI sceAvPlayerAddSource(AvPlayerInternal* h, const char* filename) { - if (!h || !filename) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->AddSource(filename, SourceTypeUnknown); -} - -int APS5_VABI sceAvPlayerAddSourceEx(AvPlayerInternal* h, uint32_t uri_type, const AvPlayerSourceDetails* source_details) { - if (!h || uri_type != 0 || !source_details || !source_details->uri.name) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->AddSource(std::string_view(source_details->uri.name, source_details->uri.length), source_details->source_type); -} - -int APS5_VABI sceAvPlayerChangeStream(AvPlayerInternal* h, uint32_t old_stream_id, uint32_t new_stream_id) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->ChangeStream(old_stream_id, new_stream_id); -} - -int APS5_VABI sceAvPlayerClose(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - delete ToPlayer(h); - return SCE_OK; -} - -uint64_t APS5_VABI sceAvPlayerCurrentTime(AvPlayerInternal* h) { - if (!h) return static_cast(static_cast(SCE_AVPLAYER_ERROR_INVALID_PARAMS)); - return ToPlayer(h)->CurrentTime(); -} - -int APS5_VABI sceAvPlayerDisableStream(AvPlayerInternal* h, uint32_t stream_id) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->DisableStream(stream_id); -} - -int APS5_VABI sceAvPlayerEnableStream(AvPlayerInternal* h, uint32_t stream_id) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->EnableStream(stream_id); -} - -Bool APS5_VABI sceAvPlayerGetAudioData(AvPlayerInternal* h, AvPlayerFrameInfo* audio_info) { - if (!h || !audio_info) return false; - return ToPlayer(h)->GetAudioData(*audio_info); -} - -int APS5_VABI sceAvPlayerGetStreamInfo(AvPlayerInternal* h, uint32_t stream_id, AvPlayerStreamInfo* info) { - if (!h || !info) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->GetStreamInfo(stream_id, *info); -} - -int APS5_VABI sceAvPlayerGetStreamInfoEx(AvPlayerInternal* h, uint32_t stream_id, AvPlayerStreamInfoEx* info) { - if (!h || !info) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->GetStreamInfoEx(stream_id, *info); -} - -Bool APS5_VABI sceAvPlayerGetVideoData(AvPlayerInternal* h, AvPlayerFrameInfo* video_info) { - if (!h || !video_info) return false; - return ToPlayer(h)->GetVideoData(*video_info); -} - -Bool APS5_VABI sceAvPlayerGetVideoDataEx(AvPlayerInternal* h, AvPlayerFrameInfoEx* video_info) { - if (!h || !video_info) return false; - return ToPlayer(h)->GetVideoData(*video_info); -} - -AvPlayerInternal* APS5_VABI sceAvPlayerInit(AvPlayerInitData* init) { - if (!init) return nullptr; - return new Player(*init, VideoBufferCount(init->num_output_video_framebuffers)); -} - -int APS5_VABI sceAvPlayerInitEx(const AvPlayerInitDataEx* init_ex, AvPlayerInternal** handle) { - if (!init_ex || !handle) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - AvPlayerInitData init{}; - init.memory_replacement = init_ex->memory_replacement; - init.file_replacement = init_ex->file_replacement; - init.event_replacement = init_ex->event_replacement; - init.debug_level = init_ex->debug_level; - init.num_output_video_framebuffers = init_ex->num_output_video_framebuffers; - init.auto_start = init_ex->auto_start; - init.default_language = init_ex->default_language; - *handle = new Player(init, VideoBufferCount(init.num_output_video_framebuffers)); - return SCE_OK; -} - -Bool APS5_VABI sceAvPlayerIsActive(AvPlayerInternal* h) { - if (!h) return false; - return ToPlayer(h)->IsActive(); -} - -int APS5_VABI sceAvPlayerJumpToTime(AvPlayerInternal* h, uint64_t time_ms) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->JumpToTime(time_ms); -} - -int APS5_VABI sceAvPlayerPause(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Pause(); -} - -int APS5_VABI sceAvPlayerPostInit(AvPlayerInternal* h, const AvPlayerPostInitData* post_init) { - if (!h || !post_init) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->PostInit(*post_init); -} - -int APS5_VABI sceAvPlayerResume(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Resume(); -} - -int APS5_VABI sceAvPlayerSetAvSyncMode(AvPlayerInternal* h, uint32_t sync_mode) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->SetAvSyncMode(sync_mode); -} - -int APS5_VABI sceAvPlayerSetAvailableBandwidth(AvPlayerInternal* h, uint32_t start_bandwidth, uint32_t minimum_bandwidth, uint32_t maximum_bandwidth) { - (void)start_bandwidth; - (void)minimum_bandwidth; - (void)maximum_bandwidth; - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return SCE_OK; -} - -int APS5_VABI sceAvPlayerSetLogCallback(void* callback, void* user_data) { - (void)callback; - (void)user_data; - return SCE_OK; -} - -int APS5_VABI sceAvPlayerSetLooping(AvPlayerInternal* h, Bool loop) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->SetLooping(loop != 0); -} - -int APS5_VABI sceAvPlayerSetTrickSpeed(AvPlayerInternal* h, int32_t trick_speed) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->SetTrickSpeed(trick_speed); -} - -int APS5_VABI sceAvPlayerStart(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Start(); -} - -int APS5_VABI sceAvPlayerStartEx(AvPlayerInternal* h, const void* start_info_ex) { - (void)start_info_ex; - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Start(); -} - -int APS5_VABI sceAvPlayerStop(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->Stop(); -} - -int APS5_VABI sceAvPlayerStreamCount(AvPlayerInternal* h) { - if (!h) return SCE_AVPLAYER_ERROR_INVALID_PARAMS; - return ToPlayer(h)->StreamCount(); -} - -} -#pragma GCC visibility pop diff --git a/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake b/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake index 8facef190..86a5d13d3 100644 --- a/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake +++ b/core/libs/prx/libSceAvPlayer/AvPlayerLibrary.cmake @@ -1,7 +1,7 @@ function(add_sce_avplayer_library target) set(avPlayerDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) add_library(${target} SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp + ${avPlayerDir}/Export.cpp ${avPlayerDir}/src/Player.cpp ${avPlayerDir}/src/Source.cpp ) diff --git a/core/libs/prx/libSceAvPlayer/src/Player.cpp b/core/libs/prx/libSceAvPlayer/src/Player.cpp index cc55f6eff..1e265d395 100644 --- a/core/libs/prx/libSceAvPlayer/src/Player.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Player.cpp @@ -327,7 +327,7 @@ int Player::SetAvSyncMode(std::uint32_t mode) { bool Player::GetVideoData(AvPlayerFrameInfoEx& info) { std::lock_guard lock(mutex); - if (!source || state != State::Play) return false; + if (!source || (state != State::Play && state != State::Pause)) return false; return source->GetVideoData(info); } diff --git a/core/libs/prx/libSceAvPlayer/src/Source.cpp b/core/libs/prx/libSceAvPlayer/src/Source.cpp index a59c07a47..8990d5d57 100644 --- a/core/libs/prx/libSceAvPlayer/src/Source.cpp +++ b/core/libs/prx/libSceAvPlayer/src/Source.cpp @@ -494,7 +494,7 @@ private: } bool takeVideo(AvPlayerFrameInfoEx& info) { - if (!started || paused || video.stream < 0 || speed < 0) return false; + if (!started || video.stream < 0 || speed < 0) return false; if (speed != NormalSpeed) dropLateAudio(); auto& frames = video.frames; const bool synced = syncMode == SyncModeDefault; @@ -502,6 +502,7 @@ private: while (!frames.empty() && frames.front().epoch < stale) recycleFront(video); if (frames.empty()) return false; const auto& front = frames.front(); + if (paused && front.epoch <= clockEpoch) return false; if (front.epoch > clockEpoch) { if (synced && audioDriving && speed == NormalSpeed) return false; if (synced && front.seamless && presented && presentationClock.Now() < static_cast(lastPresented + frameDuration())) return false; diff --git a/core/libs/prx/libSceConvertKeycode/Export.cpp b/core/libs/prx/libSceConvertKeycode/Export.cpp index b643fcbb6..2ac60a0cd 100644 --- a/core/libs/prx/libSceConvertKeycode/Export.cpp +++ b/core/libs/prx/libSceConvertKeycode/Export.cpp @@ -21,4 +21,10 @@ int APS5_VABI sceConvertKeycodeGetVirtualKeycode(std::uint64_t a0, std::uint64_t static_cast(a0), static_cast(a1), static_cast(a2), static_cast(a3)); return kErrInvalidAddress; } + +int APS5_VABI sceConvertKeycodeGetCharacterFromKeyboardData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index 3ed2eaa05..ae87bf31b 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -230,6 +230,24 @@ void CopyStyleToCache(FontHandleNative* font) { std::memcpy(&font->cached_style.state, &font->style, sizeof(StyleStateBlock)); } +template +int CallOpenFontObject(FontHandle fontHandle, TCall call) { + const auto* font = GetNativeFont(fontHandle); + if (!font || font->magic != HANDLE_MAGIC) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + auto* library = static_cast(font->library); + if (!library || library->magic != LIBRARY_MAGIC) return SCE_FONT_ERROR_INVALID_FONT_HANDLE; + if (!library->sys_driver) return SCE_FONT_ERROR_FATAL; + const std::uint32_t modeLow = font->flags & 0x0Fu; + FontObj* head = nullptr; + std::uint32_t lockWord = 0; + auto* entry = AcquireFontCtxEntry(FontContext(library, font), font->open_info.ctx_entry_index, modeLow, &head, &lockWord); + if (!entry) return SCE_FONT_ERROR_FATAL; + FontObj* obj = FindSubFont(head, font->open_info.sub_font_index); + const int rc = (lockWord & OPEN_BIT) != 0 && (lockWord & COUNT_MASK) != 0 && obj ? call(*library->sys_driver, obj) : SCE_FONT_ERROR_FATAL; + ReleaseFontCtxEntryLock(entry, modeLow, lockWord); + return rc; +} + } #pragma GCC visibility push(default) @@ -453,6 +471,46 @@ int APS5_VABI sceFontGetLibrary(FontHandle fontHandle, FontLibrary* pLibrary) { return SCE_FONT_OK; } +int APS5_VABI sceFontGetFontGlyphsCount(FontHandle fontHandle, std::uint32_t* glyphsCount) { + if (!glyphsCount) return SCE_FONT_ERROR_INVALID_PARAMETER; + *glyphsCount = 0; + const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { + if (!driver.glyphs_count) return SCE_FONT_ERROR_FATAL; + return driver.glyphs_count(obj, glyphsCount); + }); + if (rc != SCE_FONT_OK) *glyphsCount = 0; + return rc; +} + +int APS5_VABI sceFontGetCharGlyphCode(FontHandle fontHandle, std::uint32_t code, std::uint32_t* glyphCode) { + if (!glyphCode) return SCE_FONT_ERROR_INVALID_PARAMETER; + *glyphCode = 0; + const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { + if (!driver.glyph_index) return SCE_FONT_ERROR_FATAL; + if (code == 0) return SCE_FONT_ERROR_NO_SUPPORT_CODE; + return driver.glyph_index(obj, code, glyphCode); + }); + if (rc != SCE_FONT_OK) *glyphCode = 0; + return rc; +} + +int APS5_VABI sceFontGetFontResolution(FontHandle fontHandle, std::uint32_t* pResolution, float* pScalePixel) { + std::uint16_t unitsPerEm = 0; + float scalePixel = 0.0f; + const int rc = CallOpenFontObject(fontHandle, [&](const SysDriver& driver, FontObj* obj) { + if (!pResolution && !pScalePixel) return SCE_FONT_ERROR_INVALID_PARAMETER; + if (!driver.scale) return SCE_FONT_ERROR_FATAL; + return driver.scale(obj, &unitsPerEm, &scalePixel); + }); + if (rc != SCE_FONT_OK) { + unitsPerEm = 0; + scalePixel = 0.0f; + } + if (pResolution) *pResolution = unitsPerEm; + if (pScalePixel) *pScalePixel = scalePixel; + return rc; +} + int APS5_VABI sceFontBindRenderer(FontHandle fontHandle, FontRenderer renderer) { auto* font = GetNativeFont(fontHandle); std::uint32_t fontLock = 0; diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index e7a004051..e8fcd5a5a 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -72,16 +72,6 @@ int APS5_VABI sceFontGetAttribute() { return 0; } -int APS5_VABI sceFontGetCharGlyphCode() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceFontGetFontGlyphsCount() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetFontGlyphsOutlineProfile() { NotImplemented_nid_no_patch(__func__); return 0; @@ -92,11 +82,6 @@ int APS5_VABI sceFontGetFontMetrics() { return 0; } -int APS5_VABI sceFontGetFontResolution() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGetFontStyleInformation() { NotImplemented_nid_no_patch(__func__); return 0; @@ -422,11 +407,6 @@ int APS5_VABI sceFontWordsFindWordCharacters() { return 0; } -int APS5_VABI sceFontWritingLineGetOrderingSpace() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceFont/src/WritingLine.cpp b/core/libs/prx/libSceFont/src/WritingLine.cpp index 5a313c994..216a03deb 100644 --- a/core/libs/prx/libSceFont/src/WritingLine.cpp +++ b/core/libs/prx/libSceFont/src/WritingLine.cpp @@ -124,6 +124,16 @@ int APS5_VABI sceFontWritingLineGetRenderMetrics(void* writingLine, FontWritingM return SCE_FONT_OK; } +int APS5_VABI sceFontWritingLineGetOrderingSpace(void* writingLine, float* headSpace, float* inlineSpace, float* tailSpace, float* advanceSpace) { + GetLine(__func__, writingLine); + if (!headSpace || !inlineSpace || !tailSpace || !advanceSpace) Unsupported(__func__, "null space pointer (error code not verified)"); + *headSpace = 0.0f; + *inlineSpace = 0.0f; + *tailSpace = 0.0f; + *advanceSpace = 0.0f; + return SCE_FONT_OK; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index ad09bd5a2..7f49954e0 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -295,6 +295,14 @@ int APS5_VABI LibraryGetFaceMetric(FontObj* obj, std::uint32_t metricId, std::ui return SCE_FONT_OK; } +int APS5_VABI LibraryGetGlyphsCount(FontObj* obj, std::uint32_t* outCount) { + if (!obj || !outCount) return SCE_FONT_ERROR_FATAL; + const FT_Face face = FaceOf(obj); + if (!face || face->num_glyphs < 0) return SCE_FONT_ERROR_FATAL; + *outCount = static_cast(face->num_glyphs); + return SCE_FONT_OK; +} + int APS5_VABI LibraryGetGlyphIndex(FontObj* obj, std::uint32_t codepoint, std::uint32_t* outGlyphIndex) { if (!obj || !outGlyphIndex) return SCE_FONT_ERROR_FATAL; const FT_Face face = FaceOf(obj); @@ -735,6 +743,7 @@ SysDriver MakeDriverTable() { driver.close = &LibraryCloseFont; driver.scale = &LibraryGetFaceScale; driver.metric = &LibraryGetFaceMetric; + driver.glyphs_count = &LibraryGetGlyphsCount; driver.glyph_index = &LibraryGetGlyphIndex; driver.set_char_with_dpi = &LibrarySetCharSizeWithDpi; driver.set_char_default_dpi = &LibrarySetCharSizeDefaultDpi; diff --git a/core/libs/prx/libSceGameLiveStreaming/Export.cpp b/core/libs/prx/libSceGameLiveStreaming/Export.cpp index be247e398..11d59f501 100644 --- a/core/libs/prx/libSceGameLiveStreaming/Export.cpp +++ b/core/libs/prx/libSceGameLiveStreaming/Export.cpp @@ -14,4 +14,14 @@ int APS5_VABI sceGameLiveStreamingTerminate(void) { return 0; } +int APS5_VABI sceGameLiveStreamingGetCurrentStatus2() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameLiveStreamingGetProgramInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index fa00ad2d7..16eca0b4a 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -172,6 +172,12 @@ int APS5_VABI sceHttpSetRecvTimeOut(int id, uint32_t usec) { return 0; } +int APS5_VABI sceHttpSetResponseHeaderMaxSize(int id, uint64_t header_size) { + (void)id; + (void)header_size; + return 0; +} + int APS5_VABI sceHttpSetRequestContentLength(int request_id, uint64_t content_length) { (void)request_id; (void)content_length; @@ -242,6 +248,11 @@ int APS5_VABI sceHttpReadData(int request_id, void* data, size_t size) { return ERROR_NETWORK; } +int APS5_VABI sceHttpRedirectCacheFlush(int http_ctx_id) { + (void)http_ctx_id; + return 0; +} + int APS5_VABI sceHttpSetChunkedTransferEnabled(int id, int enable) { (void)id; (void)enable; @@ -345,6 +356,11 @@ int APS5_VABI sceHttpsLoadCert(int http_ctx_id, int num, void* ca_list, void* ce return 0; } +int APS5_VABI sceHttpsUnloadCert(int http_ctx_id) { + (void)http_ctx_id; + return 0; +} + int APS5_VABI sceHttpGetLastErrno(int request_id, int* errno_out) { (void)request_id; if (errno_out == nullptr) { @@ -353,4 +369,25 @@ int APS5_VABI sceHttpGetLastErrno(int request_id, int* errno_out) { *errno_out = 0; return 0; } + +int APS5_VABI sceHttpsGetSslError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetCookieRecvCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpsSetSslVersion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttpSetRedirectCallback(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceHttp/src/Parse.cpp b/core/libs/prx/libSceHttp/src/Parse.cpp index 93b9d109b..0732f6240 100644 --- a/core/libs/prx/libSceHttp/src/Parse.cpp +++ b/core/libs/prx/libSceHttp/src/Parse.cpp @@ -1,6 +1,7 @@ #include #include #include +#include #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" @@ -56,7 +57,7 @@ int APS5_VABI sceHttpParseStatusLine(const char* statusLine, size_t lineLen, int size_t length = static_cast(static_cast(lineFeed) - phrase); const size_t consumed = index + length + 1; if (consumed > static_cast(INT32_MAX)) { - NotImplemented_nid_no_patch("sceHttpParseStatusLine: status line longer than INT32_MAX"); + throw std::out_of_range("sceHttpParseStatusLine: consumed byte count exceeds int"); } if (length > 0 && phrase[length - 1] == '\r') --length; diff --git a/core/libs/prx/libSceHttp/src/Uri.cpp b/core/libs/prx/libSceHttp/src/Uri.cpp index 4dc4dd3b3..8bee28c3e 100644 --- a/core/libs/prx/libSceHttp/src/Uri.cpp +++ b/core/libs/prx/libSceHttp/src/Uri.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -19,6 +20,7 @@ constexpr uint32_t URI_BUILD_WITH_USERNAME = 0x10; constexpr uint32_t URI_BUILD_WITH_PASSWORD = 0x20; constexpr uint32_t URI_BUILD_WITH_QUERY = 0x40; constexpr uint32_t URI_BUILD_WITH_FRAGMENT = 0x80; +constexpr size_t URI_MAX_LENGTH = 0x3FFF; struct UriParts { bool opaque = false; @@ -72,14 +74,23 @@ std::string removeDotSegments(std::string_view input) { return output; } -int parseUri(std::string_view uri, UriParts& parts) { +size_t schemeLength(std::string_view uri) { size_t schemeEnd = 0; if (!uri.empty() && std::isalpha(static_cast(uri[0]))) { schemeEnd = 1; while (schemeEnd < uri.size() && (std::isalnum(static_cast(uri[schemeEnd])) || uri[schemeEnd] == '+' || uri[schemeEnd] == '-' || uri[schemeEnd] == '.')) ++schemeEnd; } - if (schemeEnd > 0 && schemeEnd < uri.size() && uri[schemeEnd] == ':') { + return schemeEnd > 0 && schemeEnd < uri.size() && uri[schemeEnd] == ':' ? schemeEnd : 0; +} + +bool isOpaque(std::string_view uri) { + const size_t schemeEnd = schemeLength(uri); + return !uri.substr(schemeEnd == 0 ? 0 : schemeEnd + 1).starts_with("//"); +} + +int parseUri(std::string_view uri, UriParts& parts) { + if (const size_t schemeEnd = schemeLength(uri); schemeEnd != 0) { parts.scheme = uri.substr(0, schemeEnd); uri.remove_prefix(schemeEnd + 1); } @@ -140,6 +151,13 @@ int parseUri(std::string_view uri, UriParts& parts) { return 0; } +size_t poolSize(const UriParts& parts) { + const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; + size_t size = 0; + for (const std::string_view field : fields) size += field.size() + 1; + return size; +} + int copyOut(const std::string& value, char* out, size_t* require, size_t prepare) { if (require) *require = value.size() + 1; if (!out) return 0; @@ -218,6 +236,50 @@ int APS5_VABI sceHttpUriUnescape(char* out, size_t* require, size_t prepare, con return copyOut(decoded, out, require, prepare); } +int APS5_VABI sceHttpUriMerge(char* merged_url, const char* url, const char* relative_uri, size_t* require, size_t prepare, uint32_t option) { + if (option != 0 || !url || !relative_uri) return ERROR_INVALID_VALUE; + + UriParts base; + if (const int result = parseUri(url, base); result != 0) return result; + const bool relativeOpaque = isOpaque(relative_uri); + if (UriParts relative; !relativeOpaque) { + if (const int result = parseUri(relative_uri, relative); result != 0) return result; + } + + const size_t urlLength = strnlen(url, URI_MAX_LENGTH); + const size_t relativeLength = strnlen(relative_uri, URI_MAX_LENGTH); + const size_t size = poolSize(base) + 2 + (urlLength + relativeLength) * 2; + if (require) *require = size; + if (!merged_url) return 0; + if (prepare < size) return ERROR_OUT_OF_MEMORY; + + if (!relativeOpaque) { + std::strncpy(merged_url, relative_uri, size); + merged_url[size - 1] = '\0'; + if (require) *require = relativeLength + 1; + return 0; + } + + std::string path = base.path; + const size_t slash = path.rfind('/'); + if (slash == std::string::npos) path.push_back('/'); + else path.erase(slash + 1); + if (relative_uri[0] == '/') path.clear(); + path.append(relative_uri, std::min(relativeLength, URI_MAX_LENGTH - std::min(path.size(), URI_MAX_LENGTH))); + + std::string scheme(base.scheme), username(base.username), password(base.password), hostname(base.hostname); + SceHttpUriElement element; + element.opaque = base.opaque; + element.scheme = scheme.data(); + element.username = username.data(); + element.password = password.data(); + element.hostname = hostname.data(); + element.path = path.data(); + element.port = base.port; + return sceHttpUriBuild(merged_url, nullptr, prepare - (urlLength + relativeLength + 1), &element, + URI_BUILD_WITH_SCHEME | URI_BUILD_WITH_HOSTNAME | URI_BUILD_WITH_PORT | URI_BUILD_WITH_PATH | URI_BUILD_WITH_USERNAME | URI_BUILD_WITH_PASSWORD); +} + int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* pool, size_t* require, size_t prepare) { if (!src_url) return ERROR_INVALID_URL; const bool write = out && pool; @@ -227,8 +289,7 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* if (const int result = parseUri(src_url, parts); result != 0) return result; const std::string_view fields[] = {parts.scheme, parts.username, parts.password, parts.hostname, parts.path, parts.query, parts.fragment}; - size_t size = 0; - for (const std::string_view field : fields) size += field.size() + 1; + const size_t size = poolSize(parts); if (require) *require = size; if (!write) return 0; if (prepare < size) return ERROR_OUT_OF_MEMORY; @@ -254,4 +315,45 @@ int APS5_VABI sceHttpUriParse(SceHttpUriElement* out, const char* src_url, void* return 0; } +int APS5_VABI sceHttpUriSweepPath(char* dst, const char* src, size_t srcSize) { + if (srcSize == 0) return 0; + if (!dst || !src) return ERROR_INVALID_VALUE; + + const size_t length = srcSize - 1; + if (length == 0 || src[0] != '/') { + std::memcpy(dst, src, length); + dst[length] = '\0'; + return 0; + } + + dst[0] = '/'; + dst[1] = '\0'; + size_t end = 0; + size_t pos = 1; + while (pos < length) { + if (src[pos] == '.' && pos + 1 < length && src[pos + 1] == '/') { + pos += 2; + continue; + } + if (src[pos] == '.' && pos + 2 < length && src[pos + 1] == '.' && src[pos + 2] == '/') { + if (end != 0) { + dst[end] = '\0'; + end = static_cast(std::strrchr(dst, '/') - dst); + dst[end + 1] = '\0'; + } + pos += 3; + continue; + } + size_t count = length - pos; + if (const void* slash = std::memchr(src + pos, '/', length - pos)) { + count = static_cast(static_cast(slash) + 1 - (src + pos)); + } + std::memcpy(dst + end + 1, src + pos, count); + dst[end + 1 + count] = '\0'; + end += count; + pos += count; + } + return 0; +} + } diff --git a/core/libs/prx/libSceHttp2/CMakeLists.txt b/core/libs/prx/libSceHttp2/CMakeLists.txt new file mode 100644 index 000000000..08d9a13c8 --- /dev/null +++ b/core/libs/prx/libSceHttp2/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceHttp2 SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceHttp2 PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceHttp2) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_http2_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttp2.cpp) + target_include_directories(guest_http2_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http2_tests PRIVATE libSceHttp2 libc) + configure_windows_unwind(guest_http2_tests) + add_test(NAME guest_http2 COMMAND guest_http2_tests) + if(WIN32) + set_tests_properties(guest_http2 PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index adaa00e78..a90ecb83f 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -214,23 +214,24 @@ int APS5_VABI sceHttp2GetMemoryPoolStats() { return 0; } -int APS5_VABI sceHttp2CookieFlush(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttp2CookieFlush(int id) { + (void)id; return 0; } -int APS5_VABI sceHttp2CreateCookieBox(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttp2CreateCookieBox(int lib_http2_ctx_id) { + (void)lib_http2_ctx_id; + return g_nextHandle.fetch_add(1, std::memory_order_relaxed); +} + +int APS5_VABI sceHttp2SetCookieBox(int id, int cookie_box_id) { + (void)id; + (void)cookie_box_id; return 0; } -int APS5_VABI sceHttp2SetCookieBox(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceHttp2SetRequestNoContentLength(void) { - NotImplemented_nid_no_patch(__func__); +int APS5_VABI sceHttp2SetRequestNoContentLength(int id) { + (void)id; return 0; } diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 480dbbd24..b105f365b 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -11,6 +11,7 @@ namespace { constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorConnectionFailed = static_cast(0x80bc0004u); constexpr int ErrorInvalidUserId = static_cast(0x80bc0010u); constexpr int ErrorInvalidType = static_cast(0x80bc0011u); constexpr int ErrorInvalidOption = static_cast(0x80bc0015u); @@ -58,10 +59,12 @@ int APS5_VABI sceImeKeyboardGetInfo(uint32_t resource_id, KeyboardInfo* info) { } int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArray* resource_ids) { - (void)user_id; - (void)resource_ids; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!resource_ids) return ErrorInvalidAddress; + if (user_id == UserIdInvalid) return ErrorInvalidUserId; + *resource_ids = {}; + resource_ids->user_id = user_id; + std::lock_guard lock(g_keyboardMutex); + return g_openKeyboards.contains(user_id) ? ErrorConnectionFailed : ErrorNotOpened; } int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { diff --git a/core/libs/prx/libSceJpegEnc/Export.cpp b/core/libs/prx/libSceJpegEnc/Export.cpp index d52ad86ac..6de513f53 100644 --- a/core/libs/prx/libSceJpegEnc/Export.cpp +++ b/core/libs/prx/libSceJpegEnc/Export.cpp @@ -38,10 +38,11 @@ constexpr std::uint8_t SAMPLING_TYPE_FULL = 0; constexpr std::uint8_t SAMPLING_TYPE_422 = 1; constexpr std::uint8_t SAMPLING_TYPE_420 = 2; +constexpr std::int32_t RESTART_INTERVAL_PER_ROW = -1; + constexpr std::uint32_t MAX_IMAGE_DIMENSION = 0xFFFF; constexpr std::uint32_t MAX_IMAGE_PITCH = 0xFFFFFFF; constexpr std::uint64_t MAX_IMAGE_SIZE = 0x7FFFFFFF; -constexpr int SUBSAMPLED_MAX_QUALITY = 90; struct Encoder { Encoder* self; @@ -148,9 +149,16 @@ std::vector toPackedPixels(const JpegEncEncodeParam& param, std::u } int toQuality(const JpegEncEncodeParam& param) { - int quality = 100 - param.compression_ratio * 99 / 255; - if (param.sampling_type != SAMPLING_TYPE_FULL) quality = std::min(quality, SUBSAMPLED_MAX_QUALITY); - return quality; + return 100 - param.compression_ratio * 99 / 255; +} + +Decoder::Jpeg::Sampling toSampling(const JpegEncEncodeParam& param) { + switch (param.sampling_type) { + case SAMPLING_TYPE_FULL: return Decoder::Jpeg::Sampling::Yuv444; + case SAMPLING_TYPE_422: return Decoder::Jpeg::Sampling::Yuv422; + case SAMPLING_TYPE_420: return Decoder::Jpeg::Sampling::Yuv420; + } + throw std::invalid_argument("sceJpegEncEncode: invalid sampling type"); } } // namespace @@ -183,11 +191,14 @@ int32_t APS5_VABI sceJpegEncEncode(void* handle, const JpegEncEncodeParam* param const std::int32_t result = validateEncodeParam(param); if (result != 0) return result; if (param->encode_mode == ENCODE_MODE_MJPEG) throw std::runtime_error("sceJpegEncEncode: MJPEG encode mode is not implemented"); - if (param->restart_interval > 0) throw std::runtime_error("sceJpegEncEncode: restart interval is not implemented"); + if (param->restart_interval < RESTART_INTERVAL_PER_ROW) throw std::runtime_error("sceJpegEncEncode: unknown negative restart interval"); const std::uint32_t channels = param->pixel_format == PIXEL_FORMAT_Y8 ? 1 : 3; const std::vector pixels = toPackedPixels(*param, channels); - const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param)); + const std::uint32_t restartBlocks = param->restart_interval > 0 ? static_cast(param->restart_interval) : 0; + const std::uint32_t restartRows = param->restart_interval == RESTART_INTERVAL_PER_ROW ? 1 : 0; + const std::vector jpeg = Decoder::Jpeg::Encode(pixels, param->image_width, param->image_height, channels, toQuality(*param), toSampling(*param), + restartBlocks, restartRows); if (jpeg.size() > param->jpeg_size) return SCE_JPEG_ENC_ERROR_INVALID_SIZE; std::memcpy(param->jpeg, jpeg.data(), jpeg.size()); diff --git a/core/libs/prx/libSceJson2/CMakeLists.txt b/core/libs/prx/libSceJson2/CMakeLists.txt index d1cd90d94..acfda4947 100644 --- a/core/libs/prx/libSceJson2/CMakeLists.txt +++ b/core/libs/prx/libSceJson2/CMakeLists.txt @@ -12,6 +12,7 @@ if(BUILD_TESTING) add_test_executable(guest_json_tests ${LIBS_INCLUDE_DIR}/tests/GuestJson.cpp) target_include_directories(guest_json_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_json_tests PRIVATE libSceJson2 libc) + configure_windows_unwind(guest_json_tests) add_test(NAME guest_json COMMAND guest_json_tests) if(WIN32) set_tests_properties(guest_json PROPERTIES ENVIRONMENT_MODIFICATION diff --git a/core/libs/prx/libSceJson2/Export.cpp b/core/libs/prx/libSceJson2/Export.cpp index a19fc3289..0d7e33f95 100644 --- a/core/libs/prx/libSceJson2/Export.cpp +++ b/core/libs/prx/libSceJson2/Export.cpp @@ -371,8 +371,10 @@ private: } } } + const double real = std::strtod(text.c_str(), nullptr); + if (!std::isfinite(real)) throw std::runtime_error("sce::Json::Parser::parse: number " + text + " overflows a double; the console's behaviour is unverified"); out.type = TypeReal; - out.real = std::strtod(text.c_str(), nullptr); + out.real = real; return true; } bool value(Node& out) { @@ -567,6 +569,7 @@ void APS5_VABI _ZN3sce4Json6Object8iteratorD1Ev(ObjectIterator* self) { (void)se ObjectIterator* APS5_VABI _ZN3sce4Json6Object8iteratorppEv(ObjectIterator* self) { ++self->it; return self; } Pair* APS5_VABI _ZNK3sce4Json6Object8iteratordeEv(const ObjectIterator* self) { return &*self->it; } bool APS5_VABI _ZNK3sce4Json6Object8iteratorneERKS2_(const ObjectIterator* self, const ObjectIterator* other) { return self->it != other->it; } +std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object* self) { return self->items->size(); } void APS5_VABI _ZN3sce4Json5ValueC1Ev(Value* self) { Construct(*self); } void APS5_VABI _ZN3sce4Json5ValueC1Eb(Value* self, bool value) { Construct(*self); self->node->type = TypeBoolean; self->node->boolean = value; } @@ -691,6 +694,25 @@ const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value* self, std::size_t in } return &NullAccess(TypeNull, self); } +void APS5_VABI _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(Value* self, ValueType type) { + Construct(*self); + SetType(*self->node, type); +} +const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value* self, std::size_t index) { return _ZNK3sce4Json5ValueixEm(self, index); } +const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value* self, const String* key) { + const Node& n = NodeOf(*self); + if (key != nullptr && n.type == TypeObject) + for (const auto& pair : *n.object.items) + if (*pair.key.text == *key->text) return &pair.value; + return &NullAccess(TypeNull, self); +} +Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value* self, std::size_t index) { + Node& n = NodeOf(*self); + if (n.type != TypeArray || index >= n.array.items->size()) return nullptr; + auto it = n.array.items->begin(); + std::advance(it, static_cast(index)); + return &*it; +} Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value* self, const String* key) { Node& n = NodeOf(*self); if (key == nullptr || n.type != TypeObject) return nullptr; @@ -732,7 +754,14 @@ int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value* out, const char* if (text == nullptr) throw std::invalid_argument("sce::Json::Parser::parse: null text"); Node parsed{}; Parser parser(text, size); - if (!parser.Parse(parsed)) { + bool parsedOk = false; + try { + parsedOk = parser.Parse(parsed); + } catch (...) { + Clear(parsed); + throw; + } + if (!parsedOk) { Clear(parsed); return JsonErrorParse; } @@ -747,4 +776,41 @@ int APS5_VABI sceJson2Unknown00(void) { NotImplemented_nid_no_patch("6i18OJSvFWk"); return 0; } + +struct InitParameter2 { + void* allocator; + void* userData; + std::size_t fileBufferSize; +}; +static_assert(sizeof(InitParameter2) <= 40); + +void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(InitParameter2* self) { + *self = {}; +} + +void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(InitParameter2* self, void* allocator, void* userData) { + self->allocator = allocator; + self->userData = userData; +} + +void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(InitParameter2* self, std::size_t size) { + self->fileBufferSize = size; +} + +int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void* self, const InitParameter2* parameter) { + (void)self; + if (parameter == nullptr) throw std::invalid_argument("sce::Json::Initializer::initialize: null parameter"); + return 0; +} + +int APS5_VABI _ZN3sce4Json5Value5clearEv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI _ZN3sce4Json12MemAllocator11notifyErrorEimPv(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceKeyboard/include/keyboard_structs.h b/core/libs/prx/libSceKeyboard/include/keyboard_structs.h index 326fa4b84..c219f233d 100644 --- a/core/libs/prx/libSceKeyboard/include/keyboard_structs.h +++ b/core/libs/prx/libSceKeyboard/include/keyboard_structs.h @@ -18,6 +18,7 @@ constexpr std::int32_t KEYBOARD_ARRANGEMENT_106 = 1; constexpr std::uint32_t KEYBOARD_LED_NUM_LOCK = 1; constexpr std::uint32_t KEYBOARD_LED_CAPS_LOCK = 2; constexpr std::uint32_t KEYBOARD_LED_SCROLL_LOCK = 4; +constexpr std::uint32_t KEYBOARD_LED_KANA = 16; constexpr std::uint32_t KEYBOARD_MOD_LEFT_SHIFT = 2; constexpr std::uint32_t KEYBOARD_MOD_RIGHT_SHIFT = 32; diff --git a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp index 7bc369767..2bb6273e7 100644 --- a/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp +++ b/core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp @@ -21,6 +21,8 @@ int keyCount = 0; constexpr char US_NORMAL[] = "1234567890\n\0\b\t -=[]\\\0;'`,./"; constexpr char US_SHIFTED[] = "!@#$%^&*()\n\0\b\t _+{}|\0:\"~<>?"; +constexpr char JIS_NORMAL[] = "1234567890\n\0\b\t -^@[]];:\0,./"; +constexpr char JIS_SHIFTED[] = "!\"#$%&'()\0\n\0\b\t =~`{}}+*\0<>?"; constexpr char KEYPAD_DIGITS[] = "1234567890."; KeyboardData snapshot() { @@ -69,15 +71,18 @@ bool updateKey(std::uint16_t keyCode, bool pressed) { return true; } -std::uint16_t usCharacter(std::uint32_t ledState, std::uint32_t modifierKey, std::uint16_t keyCode) { +std::uint16_t character(bool jis, std::uint32_t ledState, std::uint32_t modifierKey, std::uint16_t keyCode) { const bool shift = (modifierKey & (KEYBOARD_MOD_LEFT_SHIFT | KEYBOARD_MOD_RIGHT_SHIFT)) != 0; if (keyCode >= 0x04 && keyCode <= 0x1d) { const bool upper = shift != ((ledState & KEYBOARD_LED_CAPS_LOCK) != 0); return static_cast((upper ? 'A' : 'a') + keyCode - 0x04); } if (keyCode >= 0x1e && keyCode <= 0x38) { - return static_cast((shift ? US_SHIFTED : US_NORMAL)[keyCode - 0x1e]); + const char* table = jis ? (shift ? JIS_SHIFTED : JIS_NORMAL) : (shift ? US_SHIFTED : US_NORMAL); + return static_cast(table[keyCode - 0x1e]); } + if (jis && keyCode == 0x87) return shift ? '_' : '\\'; + if (jis && keyCode == 0x89) return shift ? '|' : '\\'; switch (keyCode) { case 0x54: return '/'; case 0x55: return '*'; @@ -155,13 +160,13 @@ int GetKey2Char(std::int32_t handle, std::int32_t arrange, std::uint32_t ledStat if (handle != KEYBOARD_HANDLE || !opened) return KEYBOARD_ERROR_INVALID_HANDLE; } if (charData == nullptr) return KEYBOARD_ERROR_INVALID_ARG; - if (arrange == KEYBOARD_ARRANGEMENT_106) { + if (arrange != KEYBOARD_ARRANGEMENT_101 && arrange != KEYBOARD_ARRANGEMENT_106) return KEYBOARD_ERROR_INVALID_ARG; + if (arrange == KEYBOARD_ARRANGEMENT_106 && (ledState & KEYBOARD_LED_KANA) != 0) { NotImplemented_nid_no_patch(__func__); return KEYBOARD_ERROR_INVALID_ARG; } - if (arrange != KEYBOARD_ARRANGEMENT_101) return KEYBOARD_ERROR_INVALID_ARG; *charData = {}; - charData->char_code = usCharacter(ledState, modifierKey, keyCode); + charData->char_code = character(arrange == KEYBOARD_ARRANGEMENT_106, ledState, modifierKey, keyCode); charData->processed = charData->char_code != 0; charData->length = charData->processed ? 1 : 0; return KEYBOARD_OK; diff --git a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp index 3b633176d..802e1da75 100644 --- a/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp +++ b/core/libs/prx/libSceKeyboard/tests/Keyboard.cpp @@ -27,13 +27,17 @@ static SDL_Event KeyEvent(Uint32 type, SDL_Scancode scancode, Uint16 mod = 0, Ui return event; } -static std::uint16_t Char(std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode) { +static std::uint16_t Char(std::uint32_t led, std::uint32_t modifierKey, std::uint16_t keyCode, std::int32_t arrange = KEYBOARD_ARRANGEMENT_101) { KeyboardCharData data{}; - Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, led, modifierKey, keyCode, &data) == KEYBOARD_OK); + Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, arrange, led, modifierKey, keyCode, &data) == KEYBOARD_OK); Require(data.processed == (data.char_code != 0) && data.length == (data.processed ? 1 : 0)); return data.char_code; } +static std::uint16_t Jis(std::uint32_t modifierKey, std::uint16_t keyCode, std::uint32_t led = 0) { + return Char(led, modifierKey, keyCode, KEYBOARD_ARRANGEMENT_106); +} + int main() { KeyboardData data[16]{}; KeyboardCharData charData{}; @@ -115,7 +119,22 @@ int main() { Require(Char(0, 0, 0x59) == 0 && Char(KEYBOARD_LED_NUM_LOCK, 0, 0x59) == '1'); Require(Char(KEYBOARD_LED_NUM_LOCK, 0, 0x62) == '0' && Char(KEYBOARD_LED_NUM_LOCK, 0, 0x63) == '.'); Require(Char(0, 0, 0x54) == '/' && Char(0, 0, 0x57) == '+' && Char(0, 0, 0x58) == '\n'); + Require(Char(0, 0, 0x87) == 0 && Char(0, 0, 0x89) == 0); + + constexpr std::uint32_t shift = KEYBOARD_MOD_LEFT_SHIFT; + Require(Jis(0, 0x04) == 'a' && Jis(shift, 0x04) == 'A' && Jis(KEYBOARD_MOD_RIGHT_SHIFT, 0x04, KEYBOARD_LED_CAPS_LOCK) == 'a'); + Require(Jis(0, 0x1e) == '1' && Jis(shift, 0x1e) == '!' && Jis(shift, 0x1f) == '"' && Jis(shift, 0x23) == '&'); + Require(Jis(shift, 0x24) == '\'' && Jis(shift, 0x25) == '(' && Jis(shift, 0x26) == ')' && Jis(0, 0x27) == '0' && Jis(shift, 0x27) == 0); + Require(Jis(0, 0x28) == '\n' && Jis(0, 0x29) == 0 && Jis(0, 0x2a) == '\b' && Jis(0, 0x2b) == '\t' && Jis(shift, 0x2c) == ' '); + Require(Jis(0, 0x2d) == '-' && Jis(shift, 0x2d) == '=' && Jis(0, 0x2e) == '^' && Jis(shift, 0x2e) == '~'); + Require(Jis(0, 0x2f) == '@' && Jis(shift, 0x2f) == '`' && Jis(0, 0x30) == '[' && Jis(shift, 0x30) == '{'); + Require(Jis(0, 0x31) == ']' && Jis(shift, 0x31) == '}' && Jis(0, 0x32) == ']' && Jis(shift, 0x32) == '}'); + Require(Jis(0, 0x33) == ';' && Jis(shift, 0x33) == '+' && Jis(0, 0x34) == ':' && Jis(shift, 0x34) == '*'); + Require(Jis(0, 0x35) == 0 && Jis(shift, 0x35) == 0 && Jis(0, 0x36) == ',' && Jis(shift, 0x37) == '>' && Jis(shift, 0x38) == '?'); + Require(Jis(0, 0x87) == '\\' && Jis(shift, 0x87) == '_' && Jis(0, 0x89) == '\\' && Jis(shift, 0x89) == '|'); + Require(Jis(0, 0x59, KEYBOARD_LED_NUM_LOCK) == '1' && Jis(0, 0x59) == 0 && Jis(0, 0x55) == '*' && Jis(0, 0xe1) == 0); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_101, 0, 0, 0x04, nullptr) == KEYBOARD_ERROR_INVALID_ARG); + Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, KEYBOARD_ARRANGEMENT_106, 0, 0, 0x04, nullptr) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardGetKey2Char(KEYBOARD_HANDLE, 2, 0, 0, 0x04, &charData) == KEYBOARD_ERROR_INVALID_ARG); Require(sceKeyboardClose(KEYBOARD_HANDLE) == KEYBOARD_OK); diff --git a/core/libs/prx/libSceLoginDialog/CMakeLists.txt b/core/libs/prx/libSceLoginDialog/CMakeLists.txt new file mode 100644 index 000000000..0271b47ba --- /dev/null +++ b/core/libs/prx/libSceLoginDialog/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceLoginDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceLoginDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLoginDialog) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_login_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestLoginDialog.cpp) + target_include_directories(guest_login_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_login_dialog_tests PRIVATE libSceLoginDialog) + add_test(NAME guest_login_dialog COMMAND guest_login_dialog_tests) + if(WIN32) + set_tests_properties(guest_login_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLoginDialog/Export.cpp b/core/libs/prx/libSceLoginDialog/Export.cpp index 66c3b8374..a986655ab 100644 --- a/core/libs/prx/libSceLoginDialog/Export.cpp +++ b/core/libs/prx/libSceLoginDialog/Export.cpp @@ -1,44 +1,69 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_RUNNING = 2; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; +constexpr int COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr int COMMON_DIALOG_ERROR_ALREADY_INITIALIZED = static_cast(0x80B80004u); +constexpr int COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr int COMMON_DIALOG_ERROR_BUSY = static_cast(0x80B80007u); +constexpr int COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); +constexpr int COMMON_DIALOG_RESULT_USER_CANCELED = 1; + +std::atomic g_status{COMMON_DIALOG_STATUS_NONE}; + +} + extern "C" { -int APS5_VABI sceLoginDialogClose(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceLoginDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceLoginDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceLoginDialogInitialize(void) { - NotImplemented_nid_no_patch(__func__); + int expected = COMMON_DIALOG_STATUS_NONE; + if (!g_status.compare_exchange_strong(expected, COMMON_DIALOG_STATUS_INITIALIZED)) return COMMON_DIALOG_ERROR_ALREADY_INITIALIZED; return 0; } int APS5_VABI sceLoginDialogOpen(const void* param) { - (void)param; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceLoginDialogTerminate(void) { - NotImplemented_nid_no_patch(__func__); + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (status == COMMON_DIALOG_STATUS_RUNNING) return COMMON_DIALOG_ERROR_BUSY; + if (param == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + g_status = COMMON_DIALOG_STATUS_FINISHED; return 0; } int APS5_VABI sceLoginDialogUpdateStatus(void) { - NotImplemented_nid_no_patch(__func__); + return g_status.load(); +} + +int APS5_VABI sceLoginDialogGetStatus(void) { + return g_status.load(); +} + +int APS5_VABI sceLoginDialogGetResult(void* result) { + const int status = g_status.load(); + if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (status != COMMON_DIALOG_STATUS_FINISHED) return COMMON_DIALOG_ERROR_NOT_FINISHED; + const std::int32_t canceled = COMMON_DIALOG_RESULT_USER_CANCELED; + std::memcpy(result, &canceled, sizeof(canceled)); + return 0; +} + +int APS5_VABI sceLoginDialogClose(void) { + if (g_status.load() == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + return 0; +} + +int APS5_VABI sceLoginDialogTerminate(void) { + if (g_status.exchange(COMMON_DIALOG_STATUS_NONE) == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; return 0; } diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index 678c37088..edb76df0f 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -83,10 +83,8 @@ int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; } -APS5_EXPORT("CWVW78Qc3fI", sceMsgDialogUnknown00); -int APS5_VABI sceMsgDialogUnknown00(void) { - NotImplemented_nid_no_patch("CWVW78Qc3fI"); - return 0; +int APS5_VABI sceMsgDialogGetStatus(void) { + return g_status.load(); } } diff --git a/core/libs/prx/libSceNet/CMakeLists.txt b/core/libs/prx/libSceNet/CMakeLists.txt index d6a6254c7..4eac17f68 100644 --- a/core/libs/prx/libSceNet/CMakeLists.txt +++ b/core/libs/prx/libSceNet/CMakeLists.txt @@ -11,7 +11,7 @@ endif() if(BUILD_TESTING) add_test_executable(guest_sce_net_tests ${LIBS_INCLUDE_DIR}/tests/GuestSceNet.cpp) target_include_directories(guest_sce_net_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl) + target_link_libraries(guest_sce_net_tests PRIVATE libSceNet libSceNetCtl libc) configure_windows_unwind(guest_sce_net_tests) add_test(NAME guest_sce_net COMMAND guest_sce_net_tests) set_tests_properties(guest_sce_net PROPERTIES TIMEOUT 20) diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 0aa78dcd6..0b279067d 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -37,6 +38,7 @@ namespace { // FreeBSD errno numbers as reported through sceNetErrnoLoc (SCE_NET_ERROR_* is 0x80410100 + errno). constexpr int NET_ENOENT = 2; constexpr int NET_EBADF = 9; +constexpr int NET_EFAULT = 14; constexpr int NET_EINVAL = 22; constexpr int NET_ENOSPC = 28; constexpr int NET_EAGAIN = 35; @@ -62,6 +64,9 @@ constexpr int NET_SOL_SOCKET = 0xFFFF; constexpr int NET_SO_SNDTIMEO = 0x1005; constexpr int NET_SO_RCVTIMEO = 0x1006; constexpr int NET_SO_NBIO = 0x1200; +constexpr int NET_MSG_PEEK = 0x2; +constexpr int NET_MSG_TRUNC = 0x10; +constexpr int NET_UIO_MAXIOV = 1024; #ifdef _WIN32 using NativeSocket = SOCKET; @@ -247,6 +252,37 @@ std::uint64_t swap64(std::uint64_t v) { ((v >> 8) & 0xFF000000u) | ((v >> 24) & 0xFF0000u) | ((v >> 40) & 0xFF00u) | (v >> 56); } +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t name_length; + NetIovec* iov; + int iov_length; + void* control; + std::uint32_t control_length; + int flags; +}; +static_assert(sizeof(NetMsghdr) == 48 && offsetof(NetMsghdr, iov) == 16 && offsetof(NetMsghdr, control) == 32 && + offsetof(NetMsghdr, flags) == 44); + +std::int64_t message_length(const NetMsghdr* message) { + if (!message) return fail(NET_EFAULT); + if (message->iov_length < 0 || message->iov_length > NET_UIO_MAXIOV) return fail(NET_EMSGSIZE); + if (message->iov_length && !message->iov) return fail(NET_EFAULT); + std::uint64_t total = 0; + for (int i = 0; i < message->iov_length; ++i) { + const auto& entry = message->iov[i]; + if (!entry.base && entry.length) return fail(NET_EFAULT); + if (entry.length > INT_MAX - total) return fail(NET_EINVAL); + total += entry.length; + } + return static_cast(total); +} + } // namespace extern "C" { @@ -525,6 +561,70 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co return result >= 0 ? result : fail(native_error()); } +int64_t APS5_VABI sceNetSendmsg(int s, const NetMsghdr* msg, int flags) { + const auto total = message_length(msg); + if (total < 0) return total; + if (msg->control && msg->control_length) throw std::runtime_error("sceNetSendmsg: control data is not supported"); + std::vector buffer; + try { + buffer.resize(static_cast(total)); + } catch (const std::bad_alloc&) { + return fail(55); + } + std::size_t offset = 0; + for (int i = 0; i < msg->iov_length; ++i) { + if (msg->iov[i].length) std::memcpy(buffer.data() + offset, msg->iov[i].base, msg->iov[i].length); + offset += msg->iov[i].length; + } + return sceNetSendto(s, buffer.data(), buffer.size(), flags, msg->name, msg->name ? msg->name_length : 0u); +} + +int64_t APS5_VABI sceNetRecvmsg(int s, NetMsghdr* msg, int flags) { + const auto total = message_length(msg); + if (total < 0) return total; + if (flags != 0 && flags != NET_MSG_PEEK) return fail(NET_EOPNOTSUPP); + Sock socket; + { + std::lock_guard lk(g_mutex); + auto it = g_socks.find(s); + if (it == g_socks.end()) return fail(NET_EBADF); + socket = it->second; + } + const bool datagram = socket.type != NET_SOCK_STREAM; + std::vector buffer; + try { + buffer.resize(static_cast(total) + (datagram ? 1u : 0u)); + } catch (const std::bad_alloc&) { + return fail(55); + } + sockaddr_storage peer{}; + NativeLength peer_length = sizeof(peer); + std::int64_t received = ::recvfrom(socket.native->value, buffer.data(), static_cast(buffer.size()), + flags == NET_MSG_PEEK ? MSG_PEEK : 0, reinterpret_cast(&peer), &peer_length); + if (received < 0) { +#ifdef _WIN32 + if (!datagram || WSAGetLastError() != WSAEMSGSIZE) return fail(native_error()); + received = static_cast(buffer.size()); +#else + return fail(native_error()); +#endif + } + msg->flags = 0; + if (received > total) { + received = total; + msg->flags = NET_MSG_TRUNC; + } + std::size_t offset = 0; + for (int i = 0; i < msg->iov_length && offset < static_cast(received); ++i) { + const auto count = std::min(msg->iov[i].length, static_cast(received) - offset); + std::memcpy(msg->iov[i].base, buffer.data() + offset, count); + offset += count; + } + if (msg->name && !native_to_guest_address(peer, msg->name, &msg->name_length)) msg->name_length = 0; + msg->control_length = 0; + return received; +} + int APS5_VABI sceNetShutdown(int s, int how) { if (how < 0 || how > 2) return fail(NET_EINVAL); Sock socket; @@ -948,4 +1048,14 @@ int APS5_VABI sceNetResolverGetError(int rid, int* status) { return 0; } +int APS5_VABI sceNetResolverAbort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNetResolverStartNtoaMultipleRecords() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNgs2.native/CMakeLists.txt b/core/libs/prx/libSceNgs2.native/CMakeLists.txt index 986014221..b9ed2af05 100644 --- a/core/libs/prx/libSceNgs2.native/CMakeLists.txt +++ b/core/libs/prx/libSceNgs2.native/CMakeLists.txt @@ -1,22 +1,5 @@ -add_library(libSceNgs2.native SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/src/Atrac9.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Rack.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Render.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/System.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Unimplemented.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Voice.cpp -) -target_include_directories(libSceNgs2.native PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) -target_link_libraries(libSceNgs2.native PRIVATE atrac9) -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - target_link_options(libSceNgs2.native PRIVATE LINKER:--exclude-libs,ALL) -endif() -set_target_properties(libSceNgs2.native PROPERTIES - CXX_EXTENSIONS OFF - CXX_VISIBILITY_PRESET hidden - VISIBILITY_INLINES_HIDDEN ON -) -configure_windows_unwind(libSceNgs2.native) +include(${CMAKE_CURRENT_SOURCE_DIR}/Ngs2Library.cmake) +add_sce_ngs2_library(libSceNgs2.native) if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) @@ -25,7 +8,7 @@ if(MINGW) endif() if(BUILD_TESTING) - foreach(ngs2Test IN ITEMS Render Atrac9 Filter) + foreach(ngs2Test IN ITEMS Render Atrac9 Filter Custom) string(TOLOWER ${ngs2Test} testName) add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp) target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) diff --git a/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake new file mode 100644 index 000000000..5f8a9df33 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/Ngs2Library.cmake @@ -0,0 +1,23 @@ +function(add_sce_ngs2_library target) + set(ngs2Dir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${ngs2Dir}/src/Atrac9.cpp + ${ngs2Dir}/src/Custom.cpp + ${ngs2Dir}/src/Rack.cpp + ${ngs2Dir}/src/Render.cpp + ${ngs2Dir}/src/System.cpp + ${ngs2Dir}/src/Unimplemented.cpp + ${ngs2Dir}/src/Voice.cpp + ) + target_include_directories(${target} PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) + target_link_libraries(${target} PRIVATE atrac9) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(${target} PRIVATE LINKER:--exclude-libs,ALL) + endif() + set_target_properties(${target} PROPERTIES + CXX_EXTENSIONS OFF + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + ) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp index 6c99f3f42..a6a92c819 100644 --- a/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp +++ b/core/libs/prx/libSceNgs2.native/include/Ngs2Types.hpp @@ -9,8 +9,8 @@ static constexpr int SCE_NGS2_OK = 0; static constexpr int SCE_NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast(0x804A8010); static constexpr int SCE_NGS2_ERROR_INVALID_OUT_SIZE = static_cast(0x804A8011); -static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A8201); -static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A8202); +static constexpr int SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE = static_cast(0x804A0230); +static constexpr int SCE_NGS2_ERROR_INVALID_RACK_HANDLE = static_cast(0x804A0261); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast(0x804A8430); static constexpr int SCE_NGS2_ERROR_INVALID_WAVEFORM_FORMAT = static_cast(0x804A8431); static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0x804A8432); @@ -18,6 +18,7 @@ static constexpr int SCE_NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT = static_cast(0 static constexpr std::uint32_t SCE_NGS2_RACK_ID_SAMPLER = 0x1000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_SUBMIXER = 0x2000; static constexpr std::uint32_t SCE_NGS2_RACK_ID_MASTERING = 0x3000; +static constexpr std::uint32_t SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER = 0x4002; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_I16L = 0x12; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_TYPE_PCM_F32L = 0x18; @@ -49,7 +50,14 @@ static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH = 0x10000005; static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER = 0x1000000a; static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP = 0x20000000; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000; +static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_GAIN = 0x30000004; static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT = 0x30000005; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP = 0x40020000; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 = 0x40001f00; + +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_MODULES = 24; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MAX_PORTS = 16; +static constexpr std::uint32_t SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2 = 0x1f; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_CONTINUE = 1; static constexpr std::uint32_t SCE_NGS2_WAVEFORM_BLOCKS_FLAG_RESET = 4; @@ -149,6 +157,128 @@ struct Ngs2MasteringRackOption { std::uint32_t num_peak_meter_blocks; }; +struct Ngs2CustomModuleOption { + std::uint32_t size; +}; + +struct Ngs2UserFx2SetupContext { + void* common; + void* param; + void* work; + std::uintptr_t user_data; + std::uint32_t max_voices; + std::uint32_t voice_index; + std::uint64_t reserved[4]; +}; +static_assert(sizeof(Ngs2UserFx2SetupContext) == 72); + +using Ngs2UserFx2CleanupContext = Ngs2UserFx2SetupContext; + +struct Ngs2UserFx2ControlContext { + const void* data; + std::size_t data_size; + void* common; + void* param; + std::uintptr_t user_data; + std::uint64_t reserved[4]; +}; +static_assert(sizeof(Ngs2UserFx2ControlContext) == 72); + +struct Ngs2UserFx2ProcessContext { + float** channel_data; + void* common; + const void* param; + void* work; + void* state; + std::uintptr_t user_data; + std::uint32_t flags; + std::uint32_t num_input_channels; + std::uint32_t num_output_channels; + std::uint32_t num_grain_samples; + std::uint32_t sample_rate; + std::uint32_t reserved; + std::uint64_t reserved2[4]; +}; +static_assert(sizeof(Ngs2UserFx2ProcessContext) == 104); + +using Ngs2UserFx2SetupHandler = std::int32_t (APS5_VABI *)(Ngs2UserFx2SetupContext*); +using Ngs2UserFx2CleanupHandler = std::int32_t (APS5_VABI *)(Ngs2UserFx2CleanupContext*); +using Ngs2UserFx2ControlHandler = std::int32_t (APS5_VABI *)(Ngs2UserFx2ControlContext*); +using Ngs2UserFx2ProcessHandler = std::int32_t (APS5_VABI *)(Ngs2UserFx2ProcessContext*); + +struct Ngs2CustomUserFx2ModuleOption { + Ngs2CustomModuleOption custom_module_option; + Ngs2UserFx2SetupHandler setup_handler; + Ngs2UserFx2CleanupHandler cleanup_handler; + Ngs2UserFx2ControlHandler control_handler; + Ngs2UserFx2ProcessHandler process_handler; + std::size_t common_size; + std::size_t param_size; + std::size_t work_size; + std::uintptr_t user_data; +}; +static_assert(sizeof(Ngs2CustomUserFx2ModuleOption) == 72); + +struct Ngs2CustomRackModuleInfo { + const Ngs2CustomModuleOption* option; + std::uint32_t module_id; + std::uint32_t source_buffer_id; + std::uint32_t extra_buffer_id; + std::uint32_t dest_buffer_id; + std::uint32_t state_offset; + std::uint32_t state_size; + std::uint32_t reserved; + std::uint32_t reserved2; +}; +static_assert(sizeof(Ngs2CustomRackModuleInfo) == 40); + +struct Ngs2CustomRackPortInfo { + std::uint32_t source_buffer_id; + std::uint32_t reserved; +}; + +struct Ngs2CustomRackOption { + Ngs2RackOption rack_option; + std::uint32_t state_size; + std::uint32_t num_buffers; + std::uint32_t num_modules; + std::uint32_t reserved; + Ngs2CustomRackModuleInfo module[SCE_NGS2_CUSTOM_MAX_MODULES]; + Ngs2CustomRackPortInfo port[SCE_NGS2_CUSTOM_MAX_PORTS]; +}; +static_assert(sizeof(Ngs2CustomRackOption) == 1280); + +struct Ngs2CustomSubmixerRackOption { + Ngs2CustomRackOption custom_rack_option; + std::uint32_t max_channels; + std::uint32_t max_inputs; +}; +static_assert(sizeof(Ngs2CustomSubmixerRackOption) == 1288); + +struct Ngs2RackInfo { + char name[64]; + Ngs2Handle rack_handle; + Ngs2ContextBufferInfo buffer_info; + Ngs2Handle owner_system_handle; + std::uint32_t type; + std::uint32_t rack_id; + std::uint32_t uid; + std::uint32_t min_grain_samples; + std::uint32_t max_grain_samples; + std::uint32_t max_voices; + std::uint32_t max_channel_works; + std::uint32_t max_inputs; + std::uint32_t max_matrices; + std::uint32_t max_ports; + std::uint32_t state_flags; + float last_process_ratio; + std::uint64_t last_processed_tick; + std::uint64_t render_count; + std::uint32_t active_voice_count; + std::uint32_t active_channel_work_count; +}; +static_assert(sizeof(Ngs2RackInfo) == 216); + struct Ngs2VoiceParamHeader { std::uint16_t size; std::int16_t next; @@ -275,12 +405,35 @@ struct Ngs2SubmixerVoiceSetupParam { }; static_assert(sizeof(Ngs2SubmixerVoiceSetupParam) == 16); +struct Ngs2CustomSubmixerVoiceSetupParam { + Ngs2VoiceParamHeader header; + std::uint32_t num_input_channels; + std::uint32_t num_output_channels; + std::uint32_t flags; + std::uint32_t reserved; +}; +static_assert(sizeof(Ngs2CustomSubmixerVoiceSetupParam) == 24); + +struct Ngs2CustomVoiceUserFx2Param { + Ngs2VoiceParamHeader header; + const void* data; + std::size_t data_size; +}; +static_assert(sizeof(Ngs2CustomVoiceUserFx2Param) == 24); + struct Ngs2MasteringVoiceSetupParam { Ngs2VoiceParamHeader header; std::uint32_t num_io_channels; std::uint32_t flags; }; +struct Ngs2MasteringVoiceGainParam { + Ngs2VoiceParamHeader header; + float fbw_level; + float lfe_level; +}; +static_assert(sizeof(Ngs2MasteringVoiceGainParam) == 16); + struct Ngs2MasteringVoiceOutputParam { Ngs2VoiceParamHeader header; std::uint32_t output_id; @@ -308,6 +461,23 @@ struct Ngs2RenderBufferInfo { std::uint32_t num_channels; }; +static constexpr std::uint32_t SCE_NGS2_VOICE_INFO_CHANNELS = 0x4001; + +struct Ngs2VoiceChannelsInfo { + std::uint32_t num_channels; + std::uint32_t reserved; +}; +static_assert(sizeof(Ngs2VoiceChannelsInfo) == 8); + +struct Ngs2VoicePortInfo { + std::int32_t matrix_id; + float volume; + std::uint32_t num_delay_samples; + std::uint32_t dest_input_id; + Ngs2Handle dest_handle; +}; +static_assert(sizeof(Ngs2VoicePortInfo) == 24); + struct Ngs2VoiceState { std::uint32_t state_flags; std::int32_t error_code; diff --git a/core/libs/prx/libSceNgs2.native/src/Custom.cpp b/core/libs/prx/libSceNgs2.native/src/Custom.cpp new file mode 100644 index 000000000..d97ae633c --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/src/Custom.cpp @@ -0,0 +1,111 @@ +#include +#include +#include +#include +#include +#include + +#include "prx/libc/include/General.hpp" +#include "Ngs2Internal.hpp" + +static constexpr std::uint32_t USER_FX2_FLAG_FIRST_PROCESS = 1; +static constexpr std::uint32_t USER_FX2_FLAG_PARAM_CHANGED = 2; + +void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option) { + if (option.num_buffers != 1) throw std::runtime_error("NGS2: custom racks with " + std::to_string(option.num_buffers) + " buffers are not implemented"); + if (option.num_modules > SCE_NGS2_CUSTOM_MAX_MODULES || option.rack_option.max_ports > SCE_NGS2_CUSTOM_MAX_PORTS) APS5_INVALID_ARG_EX; + for (std::uint32_t m = 0; m < option.num_modules; m++) { + const auto& info = option.module[m]; + if (info.module_id != SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2) throw std::runtime_error("NGS2: custom module id " + Ngs2Hex(info.module_id) + " is not implemented"); + if (info.option == nullptr || info.option->size != sizeof(Ngs2CustomUserFx2ModuleOption)) APS5_INVALID_ARG_EX; + if (info.source_buffer_id != 0 || info.dest_buffer_id != 0) APS5_INVALID_ARG_EX; + if (reinterpret_cast(info.option)->process_handler == nullptr) APS5_INVALID_ARG_EX; + } + for (std::uint32_t p = 0; p < option.rack_option.max_ports; p++) { + if (option.port[p].source_buffer_id != 0) APS5_INVALID_ARG_EX; + } +} + +static Ngs2UserFx2SetupContext UserFxContext(Ngs2Rack& rack, std::size_t module, std::uint32_t voiceIndex) { + auto& fx = rack.voices[voiceIndex].userFx[module]; + return {rack.userFxCommon[module].data(), fx.param.data(), fx.work.data(), rack.userFx[module].user_data, + static_cast(rack.voices.size()), voiceIndex, {}}; +} + +void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option) { + rack.userFx.resize(option.num_modules); + rack.userFxCommon.resize(option.num_modules); + for (auto& voice : rack.voices) voice.userFx.resize(option.num_modules); + for (std::uint32_t m = 0; m < option.num_modules; m++) { + const auto& module = *reinterpret_cast(option.module[m].option); + rack.userFx[m] = module; + rack.userFxCommon[m].resize(module.common_size); + for (std::uint32_t i = 0; i < rack.voices.size(); i++) { + auto& fx = rack.voices[i].userFx[m]; + fx.param.resize(module.param_size); + fx.work.resize(module.work_size); + fx.state.resize(option.module[m].state_size); + fx.flags = USER_FX2_FLAG_FIRST_PROCESS; + if (module.setup_handler == nullptr) continue; + auto context = UserFxContext(rack, m, i); + const int result = module.setup_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 setup handler failed with " + std::to_string(result)); + } + } +} + +void Ngs2CleanupUserFx(Ngs2Rack& rack) { + for (std::uint32_t i = 0; i < rack.voices.size(); i++) { + for (std::size_t m = 0; m < rack.userFx.size(); m++) { + if (rack.userFx[m].cleanup_handler == nullptr) continue; + auto context = UserFxContext(rack, m, i); + const int result = rack.userFx[m].cleanup_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 cleanup handler failed with " + std::to_string(result)); + } + } +} + +static void SetupCustomSubmixer(Ngs2Voice& voice, const Ngs2CustomSubmixerVoiceSetupParam& setup) { + if (setup.flags != 0) throw std::runtime_error("NGS2: custom submixer setup flags are not implemented"); + if (setup.num_input_channels != setup.num_output_channels) throw std::runtime_error("NGS2: custom submixer channel conversion is not implemented"); + if (setup.num_input_channels == 0 || setup.num_input_channels > voice.rack->maxChannels) APS5_INVALID_ARG_EX; + voice.channels = setup.num_input_channels; +} + +static void SetUserFxParam(Ngs2Voice& voice, std::uint32_t index, const Ngs2CustomVoiceUserFx2Param& param) { + if (index >= voice.userFx.size()) APS5_INVALID_ARG_EX; + const auto& module = voice.rack->userFx[index]; + if (module.control_handler != nullptr) throw std::runtime_error("NGS2: the UserFx2 control handler is not implemented"); + auto& fx = voice.userFx[index]; + if (param.data_size != fx.param.size() || (param.data == nullptr && param.data_size != 0)) APS5_INVALID_ARG_EX; + if (param.data_size != 0) std::memcpy(fx.param.data(), param.data, param.data_size); + fx.flags |= USER_FX2_FLAG_PARAM_CHANGED; +} + +void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { + if (voice.rack->rackId != SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); + if (param.id == SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP) { + SetupCustomSubmixer(voice, ParamAs(param)); + return; + } + if ((param.id & ~0x1fu) == SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2) { + SetUserFxParam(voice, param.id & 0x1f, ParamAs(param)); + return; + } + throw std::runtime_error("NGS2: voice param " + Ngs2Hex(param.id) + " is not implemented"); +} + +void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate) { + if (voice.userFx.empty()) return; + std::vector channels(voice.channels); + for (std::uint32_t c = 0; c < voice.channels; c++) channels[c] = voice.samples.data() + static_cast(c) * grain; + for (std::size_t m = 0; m < voice.userFx.size(); m++) { + auto& fx = voice.userFx[m]; + const auto& module = voice.rack->userFx[m]; + Ngs2UserFx2ProcessContext context{channels.data(), voice.rack->userFxCommon[m].data(), fx.param.data(), fx.work.data(), fx.state.data(), + module.user_data, fx.flags, voice.channels, voice.channels, grain, sampleRate, 0, {}}; + const int result = module.process_handler(&context); + if (result != SCE_NGS2_OK) throw std::runtime_error("NGS2: the UserFx2 process handler failed with " + std::to_string(result)); + fx.flags = 0; + } +} diff --git a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp index 615380f59..8b3965619 100644 --- a/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp +++ b/core/libs/prx/libSceNgs2.native/src/Ngs2Internal.hpp @@ -6,12 +6,14 @@ #include #include #include +#include #include #include #include "prx/libSceNgs2.native/include/Ngs2Types.hpp" static constexpr std::uint32_t NGS2_MAX_CHANNELS = 8; +static constexpr std::uint32_t MIN_GRAIN_SAMPLES = 64; struct Ngs2System; struct Ngs2Rack; @@ -71,6 +73,13 @@ struct Ngs2Port { std::int32_t matrix = -1; }; +struct Ngs2UserFx2 { + std::vector param; + std::vector work; + std::vector state; + std::uint32_t flags = 0; +}; + struct Ngs2Voice { Ngs2Rack* rack = nullptr; Ngs2PlayState state = Ngs2PlayState::Empty; @@ -93,6 +102,9 @@ struct Ngs2Voice { std::vector> matrices; std::vector filters; std::uint32_t outputId = 0; + float fbwLevel = 1.0f; + float lfeLevel = 1.0f; + std::vector userFx; std::vector samples; bool rendering = false; bool rendered = false; @@ -106,11 +118,18 @@ struct Ngs2Voice { struct Ngs2Rack { Ngs2System* system = nullptr; std::uint32_t rackId = 0; + std::uint32_t uid = 0; + char name[64] = {}; std::uint32_t maxChannels = 0; std::uint32_t maxFilters = 0; + std::uint32_t maxGrainSamples = 0; + std::uint32_t maxChannelWorks = 0; + std::uint32_t maxInputs = 0; Ngs2ContextBufferInfo bufferInfo{}; Ngs2BufferAllocator allocator{}; std::vector voices; + std::vector userFx; + std::vector> userFxCommon; }; struct Ngs2System { @@ -119,12 +138,20 @@ struct Ngs2System { Ngs2BufferAllocator allocator{}; std::uint32_t uid = 0; std::int64_t renderCount = 0; + std::uintptr_t userData = 0; std::vector racks; }; std::string Ngs2Hex(std::uint32_t value); std::recursive_mutex& Ngs2Mutex(); Ngs2System* Ngs2FindSystem(Ngs2Handle handle); + +template +const TParam& ParamAs(const Ngs2VoiceParamHeader& param) { + if (param.size < sizeof(TParam)) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " is too small"); + return reinterpret_cast(param); +} + Ngs2Rack* Ngs2FindRack(Ngs2Handle handle); Ngs2Voice* Ngs2FindVoice(Ngs2Handle handle); void* Ngs2Place(const Ngs2ContextBufferInfo* bufferInfo, std::size_t size, std::size_t alignment); @@ -134,6 +161,11 @@ void Ngs2SetupAtrac9(Ngs2Voice& voice, const Ngs2WaveformFormat& format); std::size_t Ngs2Atrac9BlockBytes(const Ngs2Voice& voice, const Ngs2WaveformBlock& block); void Ngs2RestartAtrac9(Ngs2Voice& voice); const float* Ngs2Atrac9Frame(Ngs2Voice& voice, Ngs2Block& block, std::uint32_t frame); +void Ngs2CheckCustomRack(const Ngs2CustomRackOption& option); +void Ngs2SetupUserFx(Ngs2Rack& rack, const Ngs2CustomRackOption& option); +void Ngs2CleanupUserFx(Ngs2Rack& rack); +void Ngs2ApplyCustomParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param); +void Ngs2ProcessUserFx(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t sampleRate); void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo); #endif diff --git a/core/libs/prx/libSceNgs2.native/src/Rack.cpp b/core/libs/prx/libSceNgs2.native/src/Rack.cpp index b98f15b96..04d922208 100644 --- a/core/libs/prx/libSceNgs2.native/src/Rack.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Rack.cpp @@ -16,6 +16,7 @@ union RackOptions { Ngs2SamplerRackOption sampler; Ngs2SubmixerRackOption submixer; Ngs2MasteringRackOption mastering; + Ngs2CustomSubmixerRackOption customSubmixer; }; static std::size_t RackOptionSize(std::uint32_t rackId) { @@ -23,11 +24,13 @@ static std::size_t RackOptionSize(std::uint32_t rackId) { case SCE_NGS2_RACK_ID_SAMPLER: return sizeof(Ngs2SamplerRackOption); case SCE_NGS2_RACK_ID_SUBMIXER: return sizeof(Ngs2SubmixerRackOption); case SCE_NGS2_RACK_ID_MASTERING: return sizeof(Ngs2MasteringRackOption); + case SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER: return sizeof(Ngs2CustomSubmixerRackOption); default: throw std::runtime_error("NGS2: rack id " + Ngs2Hex(rackId) + " is not implemented"); } } static RackOptions DefaultRackOption(std::uint32_t rackId) { + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) throw std::runtime_error("NGS2: default custom rack options are not implemented"); RackOptions options{}; auto& common = options.common; common.size = RackOptionSize(rackId); @@ -69,6 +72,7 @@ static RackOptions DefaultRackOption(std::uint32_t rackId) { static std::uint32_t RackMaxChannels(std::uint32_t rackId, const RackOptions& options) { if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) return options.submixer.max_channels; if (rackId == SCE_NGS2_RACK_ID_MASTERING) return options.mastering.max_channels; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) return options.customSubmixer.max_channels; return NGS2_MAX_CHANNELS; } @@ -82,11 +86,13 @@ static RackOptions CheckedRackOption(std::uint32_t rackId, const Ngs2RackOption* if (options.common.max_voices == 0 || maxChannels == 0 || maxChannels > NGS2_MAX_CHANNELS) { throw std::invalid_argument("NGS2: invalid rack voice or channel count"); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2CheckCustomRack(options.customSubmixer.custom_rack_option); return options; } static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackOptions& options, const Ngs2ContextBufferInfo& bufferInfo, const Ngs2BufferAllocator& allocator, Ngs2Handle* handle) { + static std::uint32_t nextUid = 1; auto* system = Ngs2FindSystem(systemHandle); if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; auto* rack = new (Ngs2Place(&bufferInfo, sizeof(Ngs2Rack), alignof(Ngs2Rack))) Ngs2Rack{}; @@ -94,6 +100,13 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO rack->rackId = rackId; rack->maxChannels = RackMaxChannels(rackId, options); rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters : 0; + rack->uid = nextUid++; + std::memcpy(rack->name, options.common.name, sizeof(rack->name)); + rack->name[sizeof(rack->name) - 1] = '\0'; + rack->maxGrainSamples = options.common.max_grain_samples; + if (rackId == SCE_NGS2_RACK_ID_SAMPLER) rack->maxChannelWorks = options.sampler.max_channel_works; + if (rackId == SCE_NGS2_RACK_ID_SUBMIXER) rack->maxInputs = options.submixer.max_inputs; + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) rack->maxInputs = options.customSubmixer.max_inputs; rack->bufferInfo = bufferInfo; rack->allocator = allocator; rack->voices.resize(options.common.max_voices); @@ -102,12 +115,14 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO voice.ports.resize(options.common.max_ports); voice.matrices.resize(options.common.max_matrices); } + if (rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) Ngs2SetupUserFx(*rack, options.customSubmixer.custom_rack_option); system->racks.push_back(rack); *handle = reinterpret_cast(rack); return SCE_NGS2_OK; } int Ngs2DestroyRack(Ngs2Rack& rack, Ngs2ContextBufferInfo* outBufferInfo) { + Ngs2CleanupUserFx(rack); auto& racks = rack.system->racks; std::erase(racks, &rack); for (auto* other : racks) { @@ -175,6 +190,34 @@ int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t rack_handle, uint32_t voice_id return SCE_NGS2_OK; } +int APS5_VABI sceNgs2RackGetInfo(uintptr_t rack_handle, Ngs2RackInfo* info, size_t info_size) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (info_size != sizeof(Ngs2RackInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + std::lock_guard lock(Ngs2Mutex()); + const auto* rack = Ngs2FindRack(rack_handle); + if (rack == nullptr) return SCE_NGS2_ERROR_INVALID_RACK_HANDLE; + *info = {}; + std::memcpy(info->name, rack->name, sizeof(info->name)); + info->rack_handle = rack_handle; + info->buffer_info = rack->bufferInfo; + info->owner_system_handle = reinterpret_cast(rack->system); + info->type = rack->rackId >> 12; + info->rack_id = rack->rackId; + info->uid = rack->uid; + info->min_grain_samples = MIN_GRAIN_SAMPLES; + info->max_grain_samples = rack->maxGrainSamples; + info->max_voices = static_cast(rack->voices.size()); + info->max_channel_works = rack->maxChannelWorks; + info->max_inputs = rack->maxInputs; + info->max_matrices = static_cast(rack->voices.front().matrices.size()); + info->max_ports = static_cast(rack->voices.front().ports.size()); + info->state_flags = 1; + info->render_count = static_cast(rack->system->renderCount); + info->active_voice_count = static_cast(std::count_if(rack->voices.begin(), rack->voices.end(), + [](const Ngs2Voice& voice) { return voice.state != Ngs2PlayState::Empty; })); + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2RackLock(uintptr_t rack_handle) { Ngs2Mutex().lock(); if (Ngs2FindRack(rack_handle) != nullptr) return SCE_NGS2_OK; diff --git a/core/libs/prx/libSceNgs2.native/src/Render.cpp b/core/libs/prx/libSceNgs2.native/src/Render.cpp index ba5b520b3..1e81d0d6b 100644 --- a/core/libs/prx/libSceNgs2.native/src/Render.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Render.cpp @@ -176,6 +176,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector& voices, if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) { if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) RenderSampler(voice, grain, systemRate); else MixInputs(voice, voices, grain, systemRate); + if (voice.hasSamples) Ngs2ProcessUserFx(voice, grain, systemRate); } voice.rendering = false; voice.rendered = true; @@ -187,12 +188,16 @@ static std::size_t SampleBytes(std::uint32_t waveformType) { throw std::runtime_error("NGS2: render waveform type " + Ngs2Hex(waveformType) + " is not implemented"); } +static bool IsStereoIntoSurround(const Ngs2Voice& voice, const Ngs2RenderBufferInfo& output) { + return voice.channels == 2 && (output.num_channels == 6 || output.num_channels == 8); +} + static std::vector& OutputMix(std::vector>& mixes, const Ngs2RenderBufferInfo* bufferInfo, std::uint32_t numBufferInfo, const Ngs2Voice& voice, std::uint32_t grain) { if (voice.outputId >= numBufferInfo) throw std::invalid_argument("NGS2: mastering output " + std::to_string(voice.outputId) + " has no render buffer"); const auto& output = bufferInfo[voice.outputId]; const auto sampleBytes = SampleBytes(output.waveform_type); - if (output.num_channels != voice.channels) throw std::runtime_error("NGS2: mixing " + std::to_string(voice.channels) + " mastering channels into " + std::to_string(output.num_channels) + " is not implemented"); + if (output.num_channels != voice.channels && !IsStereoIntoSurround(voice, output)) throw std::runtime_error("NGS2: mixing " + std::to_string(voice.channels) + " mastering channels into " + std::to_string(output.num_channels) + " is not implemented"); if (output.buffer == nullptr || output.buffer_size < static_cast(grain) * output.num_channels * sampleBytes) { throw std::invalid_argument("NGS2: render buffer " + std::to_string(voice.outputId) + " is missing or too small"); } @@ -226,8 +231,11 @@ void Ngs2RenderSystem(Ngs2System& system, const Ngs2RenderBufferInfo* bufferInfo RenderVoice(*voice, voices, grain, system.option.sample_rate); if (voice->rack->rackId != SCE_NGS2_RACK_ID_MASTERING || !voice->hasSamples) continue; auto& mix = OutputMix(mixes, bufferInfo, numBufferInfo, *voice, grain); + const std::uint32_t outputChannels = bufferInfo[voice->outputId].num_channels; for (std::uint32_t channel = 0; channel < voice->channels; channel++) { - for (std::uint32_t i = 0; i < grain; i++) mix[i * voice->channels + channel] += voice->samples[channel * grain + i]; + const bool lfe = channel == 3 && (voice->channels == 6 || voice->channels == 8); + const float level = lfe ? voice->lfeLevel : voice->fbwLevel; + for (std::uint32_t i = 0; i < grain; i++) mix[i * outputChannels + channel] += voice->samples[channel * grain + i] * level; } } for (std::uint32_t i = 0; i < numBufferInfo; i++) { diff --git a/core/libs/prx/libSceNgs2.native/src/System.cpp b/core/libs/prx/libSceNgs2.native/src/System.cpp index 1bb850606..f03e5c7c2 100644 --- a/core/libs/prx/libSceNgs2.native/src/System.cpp +++ b/core/libs/prx/libSceNgs2.native/src/System.cpp @@ -13,7 +13,6 @@ #include "prx/libc/include/General.hpp" #include "Ngs2Internal.hpp" -static constexpr std::uint32_t MIN_GRAIN_SAMPLES = 64; static std::vector& Systems() { static std::vector systems; @@ -188,6 +187,55 @@ int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t system_handle, uint32_t num return SCE_NGS2_OK; } +int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t system_handle, uint32_t sample_rate) { + std::lock_guard lock(Ngs2Mutex()); + auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (sample_rate == 0) APS5_INVALID_ARG_EX; + for (const auto* rack : system->racks) { + for (const auto& voice : rack->voices) { + for (const auto& filter : voice.filters) { + if (filter.enabled && sample_rate != system->option.sample_rate) { + throw std::runtime_error("NGS2: changing the sample rate under an enabled sampler filter is not implemented"); + } + } + } + } + system->option.sample_rate = sample_rate; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemSetUserData(uintptr_t system_handle, uintptr_t user_data) { + std::lock_guard lock(Ngs2Mutex()); + auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + system->userData = user_data; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemGetUserData(uintptr_t system_handle, uintptr_t* user_data) { + std::lock_guard lock(Ngs2Mutex()); + const auto* system = Ngs2FindSystem(system_handle); + if (system == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + if (user_data == nullptr) APS5_INVALID_ARG_EX; + *user_data = system->userData; + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2SystemLock(uintptr_t system_handle) { + Ngs2Mutex().lock(); + if (Ngs2FindSystem(system_handle) != nullptr) return SCE_NGS2_OK; + Ngs2Mutex().unlock(); + return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; +} + +int APS5_VABI sceNgs2SystemUnlock(uintptr_t system_handle) { + std::lock_guard lock(Ngs2Mutex()); + if (Ngs2FindSystem(system_handle) == nullptr) return SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE; + Ngs2Mutex().unlock(); + return SCE_NGS2_OK; +} + int APS5_VABI sceNgs2SystemRender(uintptr_t system_handle, const Ngs2RenderBufferInfo* buffer_info, uint32_t num_buffer_info) { std::lock_guard lock(Ngs2Mutex()); auto* system = Ngs2FindSystem(system_handle); diff --git a/core/libs/prx/libSceNgs2.native/src/Voice.cpp b/core/libs/prx/libSceNgs2.native/src/Voice.cpp index b92ece2f4..af69ff6fa 100644 --- a/core/libs/prx/libSceNgs2.native/src/Voice.cpp +++ b/core/libs/prx/libSceNgs2.native/src/Voice.cpp @@ -49,6 +49,8 @@ void Ngs2Voice::ResetSetup() { std::fill(ports.begin(), ports.end(), Ngs2Port{}); for (auto& matrix : matrices) matrix.clear(); filters.clear(); + fbwLevel = 1.0f; + lfeLevel = 1.0f; channels = 0; sampleRate = 0; waveformType = 0; @@ -69,12 +71,6 @@ const std::uint8_t* Ngs2Voice::WaveformData() const { return block.data + (static_cast(block.info.num_skip_samples) + block.cursor) * channels * sizeof(std::int16_t); } -template -static const TParam& ParamAs(const Ngs2VoiceParamHeader& param) { - if (param.size < sizeof(TParam)) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " is too small"); - return reinterpret_cast(param); -} - static Ngs2Port& PortAt(Ngs2Voice& voice, std::uint32_t port) { if (port >= voice.ports.size()) throw std::invalid_argument("NGS2: voice port " + std::to_string(port) + " is out of range"); return voice.ports[port]; @@ -246,6 +242,10 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { ApplyCommonParam(voice, param); return; } + if (rackId == (SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 >> 16) || rackId == SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER) { + Ngs2ApplyCustomParam(voice, param); + return; + } if (rackId != voice.rack->rackId) throw std::invalid_argument("NGS2: voice param " + Ngs2Hex(param.id) + " belongs to another rack"); switch (rackId) { case SCE_NGS2_RACK_ID_SAMPLER: ApplySamplerParam(voice, param); return; @@ -259,6 +259,13 @@ static void ApplyParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) { SetupMixer(voice, ParamAs(param).num_io_channels); return; } + if (param.id == SCE_NGS2_MASTERING_VOICE_PARAM_GAIN) { + const auto& gain = ParamAs(param); + if (!std::isfinite(gain.fbw_level) || !std::isfinite(gain.lfe_level)) APS5_INVALID_ARG_EX; + voice.fbwLevel = gain.fbw_level; + voice.lfeLevel = gain.lfe_level; + return; + } if (param.id == SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT) { voice.outputId = ParamAs(param).output_id; return; @@ -340,6 +347,33 @@ int APS5_VABI sceNgs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state } } +int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t voice_handle, uint32_t port, Ngs2VoicePortInfo* info, size_t info_size) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + if (info_size != sizeof(Ngs2VoicePortInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + std::lock_guard lock(Ngs2Mutex()); + auto& voice = CheckedVoice(voice_handle); + const auto& source = PortAt(voice, port); + *info = {}; + info->matrix_id = source.matrix; + info->volume = source.volume; + info->dest_handle = reinterpret_cast(source.dest); + return SCE_NGS2_OK; +} + +int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t voice_handle, uint32_t info_id, void* info, size_t info_size) { + if (info == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; + std::lock_guard lock(Ngs2Mutex()); + const auto& voice = CheckedVoice(voice_handle); + switch (info_id) { + case SCE_NGS2_VOICE_INFO_CHANNELS: { + if (info_size != sizeof(Ngs2VoiceChannelsInfo)) return SCE_NGS2_ERROR_INVALID_OUT_SIZE; + *static_cast(info) = {voice.channels, 0}; + return SCE_NGS2_OK; + } + default: throw std::runtime_error("NGS2: voice info " + Ngs2Hex(info_id) + " is not implemented"); + } +} + int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t voice_handle, uint32_t* state_flags) { if (state_flags == nullptr) return SCE_NGS2_ERROR_INVALID_OUT_ADDRESS; std::lock_guard lock(Ngs2Mutex()); diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp new file mode 100644 index 000000000..b5872f457 --- /dev/null +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Custom.cpp @@ -0,0 +1,231 @@ +#include "Ngs2Test.hpp" + +#include +#include +#include +#include + +struct Calls { + std::vector setupVoices; + std::vector cleanupVoices; + std::vector processFlags; + std::vector order; + int processResult = SCE_NGS2_OK; +}; +static Calls calls; + +static std::int32_t APS5_VABI Setup(Ngs2UserFx2SetupContext* context) { + Require(context->common != nullptr && context->param != nullptr && context->work != nullptr); + Require(context->user_data == 0x77 && context->max_voices == 2); + std::memset(context->work, 0x5a, 8); + *static_cast(context->param) = 1.0f; + *static_cast(context->common) = 0.25f; + calls.setupVoices.push_back(context->voice_index); + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI Cleanup(Ngs2UserFx2CleanupContext* context) { + Require(context->user_data == 0x77 && context->max_voices == 2 && static_cast(context->work)[7] == 0x5a); + calls.cleanupVoices.push_back(context->voice_index); + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI Gain(Ngs2UserFx2ProcessContext* context) { + Require(context->user_data == 0x77 && context->num_input_channels == 2 && context->num_output_channels == 2); + Require(context->num_grain_samples == Grain && context->sample_rate == 48000 && context->state != nullptr); + Require(static_cast(context->work)[0] == 0x5a); + const float gain = *static_cast(context->param); + for (std::uint32_t c = 0; c < 2; c++) { + for (std::uint32_t i = 0; i < Grain; i++) context->channel_data[c][i] *= gain; + } + *static_cast(context->common) = 0.125f; + calls.processFlags.push_back(context->flags); + calls.order.push_back(0); + return calls.processResult; +} + +static std::int32_t APS5_VABI Offset(Ngs2UserFx2ProcessContext* context) { + const float offset = *static_cast(context->common); + for (std::uint32_t i = 0; i < Grain; i++) context->channel_data[1][i] += offset; + calls.order.push_back(1); + return SCE_NGS2_OK; +} + +static std::int32_t APS5_VABI ControlHandler(Ngs2UserFx2ControlContext*) { + return SCE_NGS2_OK; +} + +static Ngs2CustomUserFx2ModuleOption Module(Ngs2UserFx2ProcessHandler process) { + return {{sizeof(Ngs2CustomUserFx2ModuleOption)}, Setup, Cleanup, nullptr, process, sizeof(float), sizeof(float), 8, 0x77}; +} + +static Ngs2CustomSubmixerRackOption RackOption(const Ngs2CustomUserFx2ModuleOption* modules, std::uint32_t count) { + Ngs2CustomSubmixerRackOption option{}; + auto& common = option.custom_rack_option.rack_option; + common.size = sizeof(option); + common.max_grain_samples = 512; + common.max_voices = 2; + common.max_matrices = 1; + common.max_ports = 1; + option.custom_rack_option.num_buffers = 1; + option.custom_rack_option.num_modules = count; + for (std::uint32_t m = 0; m < count; m++) { + option.custom_rack_option.module[m].option = &modules[m].custom_module_option; + option.custom_rack_option.module[m].module_id = SCE_NGS2_CUSTOM_MODULE_ID_USER_FX2; + option.custom_rack_option.module[m].state_size = 16; + } + option.max_channels = 2; + option.max_inputs = 1; + return option; +} + +template +static bool CreateThrows(const Ngs2CustomSubmixerRackOption& option) { + Ngs2ContextBufferInfo query{}; + try { + sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &query); + } catch (const TException&) { + return true; + } + return false; +} + +template +static bool ControlThrows(uintptr_t voice, std::uint32_t id, TParam param) { + param.header = {static_cast(sizeof(TParam)), 0, id}; + try { + sceNgs2VoiceControl(voice, ¶m.header); + } catch (const TException&) { + return true; + } + return false; +} + +static uintptr_t CustomRack(uintptr_t system, const Ngs2CustomSubmixerRackOption& option) { + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + const auto info = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &option.custom_rack_option.rack_option, &info, &rack) == SCE_NGS2_OK); + return rack; +} + +static uintptr_t Sampler(uintptr_t system, const std::vector& pcm) { + const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER)); + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 2, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size() / 2), 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + return voice; +} + +static std::vector Render(uintptr_t system) { + std::vector out(Grain * 2, -1.0f); + const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}; + Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK); + return out; +} + +static void TestUserFxChain() { + calls = {}; + const auto system = CreateSystem(); + const auto master = Mastering(system, 2); + const Ngs2CustomUserFx2ModuleOption modules[2] = {Module(Gain), Module(Offset)}; + const auto rack = CustomRack(system, RackOption(modules, 2)); + Require(calls.setupVoices == (std::vector{0, 1, 0, 1})); + + const auto custom = Voice(rack); + Control(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 0, 0}); + Patch(custom, master); + Event(custom, SCE_NGS2_VOICE_EVENT_PLAY); + + std::vector pcm(Grain * 2 * 8, 16384); + const auto sampler = Sampler(system, pcm); + Patch(sampler, custom); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + + auto out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.5f && out[i * 2 + 1] == 0.75f); + Require(calls.processFlags == (std::vector{1}) && calls.order == (std::vector{0, 1})); + + const float gain = 0.5f; + Control(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)}); + out = Render(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i * 2] == 0.25f && out[i * 2 + 1] == 0.5f); + Require(calls.processFlags == (std::vector{1, 2})); + out = Render(system); + Require(calls.processFlags == (std::vector{1, 2, 0})); + + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain) - 1})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 2, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 1, 0, 0})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 2, 2, 1, 0})); + Require(ControlThrows(custom, SCE_NGS2_CUSTOM_SUBMIXER_VOICE_PARAM_SETUP, Ngs2CustomSubmixerVoiceSetupParam{{}, 3, 3, 0, 0})); + Require(ControlThrows(sampler, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)})); + + calls.processResult = -1; + bool failed = false; + try { + Render(system); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed); + calls.processResult = SCE_NGS2_OK; + + Require(sceNgs2RackDestroy(rack, nullptr) == SCE_NGS2_OK); + Require(calls.cleanupVoices == (std::vector{0, 0, 1, 1})); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestRackOptions() { + calls = {}; + const auto system = CreateSystem(); + const Ngs2CustomUserFx2ModuleOption modules[1] = {Module(Gain)}; + const auto good = RackOption(modules, 1); + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, &good.custom_rack_option.rack_option, &query) == SCE_NGS2_OK); + + auto option = good; + option.custom_rack_option.num_buffers = 2; + Require(CreateThrows(option)); + option = good; + option.custom_rack_option.module[0].module_id = 0x10; + Require(CreateThrows(option)); + option = good; + option.custom_rack_option.module[0].dest_buffer_id = 1; + Require(CreateThrows(option)); + option = good; + option.custom_rack_option.port[0].source_buffer_id = 1; + Require(CreateThrows(option)); + option = good; + option.custom_rack_option.num_modules = SCE_NGS2_CUSTOM_MAX_MODULES + 1; + Require(CreateThrows(option)); + auto noProcess = Module(nullptr); + option = RackOption(&noProcess, 1); + Require(CreateThrows(option)); + auto badSize = Module(Gain); + badSize.custom_module_option.size = 8; + option = RackOption(&badSize, 1); + Require(CreateThrows(option)); + bool failed = false; + try { + sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_CUSTOM_SUBMIXER, nullptr, &query); + } catch (const std::runtime_error&) { + failed = true; + } + Require(failed && calls.setupVoices.empty()); + + auto withControl = Module(Gain); + withControl.control_handler = ControlHandler; + const auto voice = Voice(CustomRack(system, RackOption(&withControl, 1))); + const float gain = 0.5f; + Require(ControlThrows(voice, SCE_NGS2_CUSTOM_VOICE_PARAM_USER_FX2 | 0, Ngs2CustomVoiceUserFx2Param{{}, &gain, sizeof(gain)})); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + Require(calls.cleanupVoices == (std::vector{0, 1})); +} + +int main() { + TestUserFxChain(); + TestRackOptions(); + return 0; +} diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp index 0c7f193c4..cf47e314b 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Filter.cpp @@ -173,10 +173,28 @@ static void TestRejectedParams() { Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); } +static void TestRateChangeUnderFilter() { + const auto system = CreateSystem(); + const auto sampler = Sampler(system, std::vector(Grain, 0), 1, 48000); + SetFilter(sampler, Filter(0, 1, 1, 0, 1000.0f, 1.0f, 1.0f)); + Require(sceNgs2SystemSetSampleRate(system, 48000) == SCE_NGS2_OK); + bool rejected = false; + try { + sceNgs2SystemSetSampleRate(system, 96000); + } catch (const std::runtime_error&) { + rejected = true; + } + Require(rejected); + SetFilter(sampler, Filter(0, 1, 0, 0, 0.0f, 1.0f, 1.0f)); + Require(sceNgs2SystemSetSampleRate(system, 96000) == SCE_NGS2_OK); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + int main() { TestLowPassAndTail(); TestBypassMaskAndGain(); TestSystemRateAndZeroCutoff(); TestRejectedParams(); + TestRateChangeUnderFilter(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp index ea0f7c9eb..c26310763 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Render.cpp @@ -1,7 +1,12 @@ #include "Ngs2Test.hpp" +#include +#include +#include #include #include +#include +#include #include static uintptr_t Sampler(uintptr_t system, const std::vector& pcm, std::uint32_t repeats) { @@ -33,15 +38,50 @@ static void TestErrorsAndInfo() { Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_SAMPLER, nullptr, nullptr) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); Ngs2SystemInfo info{}; - Require(sceNgs2SystemGetInfo(0x1234, &info, sizeof(info)) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); - Require(sceNgs2RackDestroy(0x1234, nullptr) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2SystemGetInfo(0x1234, &info, sizeof(info)) == static_cast(0x804A0230u)); + Require(sceNgs2RackDestroy(0x1234, nullptr) == static_cast(0x804A0261u)); const auto system = CreateSystem(); - CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + const auto rack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + Ngs2RackInfo rackInfo{}; + Require(sceNgs2RackGetInfo(0x1234, &rackInfo, sizeof(rackInfo)) == SCE_NGS2_ERROR_INVALID_RACK_HANDLE); + Require(sceNgs2RackGetInfo(rack, nullptr, sizeof(rackInfo)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2RackGetInfo(rack, &rackInfo, sizeof(rackInfo) - 1) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); + Require(sceNgs2RackGetInfo(rack, &rackInfo, sizeof(rackInfo)) == SCE_NGS2_OK); + Require(rackInfo.rack_handle == rack && rackInfo.owner_system_handle == system && rackInfo.rack_id == SCE_NGS2_RACK_ID_SAMPLER && rackInfo.type == 1); + Require(rackInfo.uid != 0 && rackInfo.max_voices == 256 && rackInfo.max_channel_works == 256 && rackInfo.max_grain_samples == 512); + Require(rackInfo.max_ports == 8 && rackInfo.max_matrices == 1 && rackInfo.active_voice_count == 0 && rackInfo.name[0] == '\0'); + Ngs2SubmixerRackOption named{}; + named.rack_option.size = sizeof(named); + for (auto& c : named.rack_option.name) c = 'n'; + named.rack_option.max_grain_samples = 256; + named.rack_option.max_voices = 2; + named.rack_option.max_matrices = 1; + named.rack_option.max_ports = 2; + named.max_channels = 2; + named.max_inputs = 3; + Ngs2ContextBufferInfo namedQuery{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_SUBMIXER, &named.rack_option, &namedQuery) == SCE_NGS2_OK); + const auto namedBuffer = Buffer(namedQuery); + uintptr_t namedRack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_SUBMIXER, &named.rack_option, &namedBuffer, &namedRack) == SCE_NGS2_OK); + Ngs2RackInfo namedInfo{}; + Require(sceNgs2RackGetInfo(namedRack, &namedInfo, sizeof(namedInfo)) == SCE_NGS2_OK); + Require(namedInfo.name[0] == 'n' && namedInfo.name[62] == 'n' && namedInfo.name[63] == '\0'); + Require(namedInfo.type == 2 && namedInfo.rack_id == SCE_NGS2_RACK_ID_SUBMIXER && namedInfo.uid != rackInfo.uid && namedInfo.max_grain_samples == 256); + Require(namedInfo.max_voices == 2 && namedInfo.max_ports == 2 && namedInfo.max_inputs == 3 && namedInfo.max_channel_works == 0); + Require(namedInfo.buffer_info.host_buffer == namedBuffer.host_buffer && namedInfo.render_count == 0); + uintptr_t named0 = 0; + Require(sceNgs2RackGetVoiceHandle(namedRack, 0, &named0) == SCE_NGS2_OK); + Control(named0, SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP, Ngs2SubmixerVoiceSetupParam{{}, 2, 0}); + Event(named0, SCE_NGS2_VOICE_EVENT_PLAY); + Require(sceNgs2RackGetInfo(namedRack, &namedInfo, sizeof(namedInfo)) == SCE_NGS2_OK && namedInfo.active_voice_count == 1); + Ngs2VoiceChannelsInfo stereoInfo{}; + Require(sceNgs2VoiceQueryInfo(named0, SCE_NGS2_VOICE_INFO_CHANNELS, &stereoInfo, sizeof(stereoInfo)) == SCE_NGS2_OK && stereoInfo.num_channels == 2); Require(sceNgs2SystemGetInfo(system, nullptr, sizeof(info)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); Require(sceNgs2SystemGetInfo(system, &info, sizeof(info) - 1) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); - Require(info.system_handle == system && info.uid != 0 && info.rack_count == 1 && info.sample_rate == 48000); + Require(info.system_handle == system && info.uid != 0 && info.rack_count == 2 && info.sample_rate == 48000); Require(info.num_grain_samples == Grain && info.max_grain_samples == 512 && info.render_count == 0); Ngs2ContextBufferInfo released{}; @@ -56,6 +96,29 @@ static void TestPcmBlockEnd() { for (int i = 0; i < 12; i++) pcm.push_back(static_cast(i * 1000 - 4000)); const auto sampler = Sampler(system, pcm, 0); Patch(sampler, master); + Ngs2VoicePortInfo portInfo{}; + Require(sceNgs2VoiceGetPortInfo(sampler, 0, nullptr, sizeof(portInfo)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &portInfo, sizeof(portInfo) - 1) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &portInfo, sizeof(portInfo)) == SCE_NGS2_OK); + Require(portInfo.dest_handle == master && portInfo.matrix_id == -1 && portInfo.volume == 1.0f); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_VOLUME, Ngs2VoicePortVolumeParam{{}, 0, 0.5f}); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Require(sceNgs2VoiceGetPortInfo(sampler, 0, &portInfo, sizeof(portInfo)) == SCE_NGS2_OK); + Require(portInfo.dest_handle == master && portInfo.matrix_id == 0 && portInfo.volume == 0.5f); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_VOLUME, Ngs2VoicePortVolumeParam{{}, 0, 1.0f}); + Control(sampler, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, -1}); + Require(sceNgs2VoiceGetPortInfo(sampler, 1, &portInfo, sizeof(portInfo)) == SCE_NGS2_OK && portInfo.dest_handle == 0); + bool portOutOfRange = false; + try { sceNgs2VoiceGetPortInfo(sampler, 8, &portInfo, sizeof(portInfo)); } catch (const std::invalid_argument&) { portOutOfRange = true; } + Require(portOutOfRange); + Ngs2VoiceChannelsInfo channelsInfo{}; + Require(sceNgs2VoiceQueryInfo(sampler, SCE_NGS2_VOICE_INFO_CHANNELS, nullptr, sizeof(channelsInfo)) == SCE_NGS2_ERROR_INVALID_OUT_ADDRESS); + Require(sceNgs2VoiceQueryInfo(sampler, SCE_NGS2_VOICE_INFO_CHANNELS, &channelsInfo, sizeof(channelsInfo) - 1) == SCE_NGS2_ERROR_INVALID_OUT_SIZE); + Require(sceNgs2VoiceQueryInfo(sampler, SCE_NGS2_VOICE_INFO_CHANNELS, &channelsInfo, sizeof(channelsInfo)) == SCE_NGS2_OK); + Require(channelsInfo.num_channels == 1 && channelsInfo.reserved == 0); + bool unknownInfo = false; + try { sceNgs2VoiceQueryInfo(sampler, 0x4000, &channelsInfo, sizeof(channelsInfo)); } catch (const std::runtime_error&) { unknownInfo = true; } + Require(unknownInfo); Control(sampler, SCE_NGS2_VOICE_PARAM_CALLBACK, Ngs2VoiceCallbackParam{{}, OnBlock, 7, SCE_NGS2_VOICE_CALLBACK_FLAG_BLOCK_END, 0}); Require(Flags(sampler) == 0); Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); @@ -152,6 +215,214 @@ static std::int32_t APS5_VABI Release(Ngs2ContextBufferInfo* info) { return SCE_NGS2_OK; } +static void TestSampleRate() { + const auto system = CreateSystem(); + Require(sceNgs2SystemSetSampleRate(0x1234, 96000) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + bool threw = false; + try { + sceNgs2SystemSetSampleRate(system, 0); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemSetSampleRate(system, 96000) == SCE_NGS2_OK); + Ngs2SystemInfo info{}; + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK && info.sample_rate == 96000); + + const auto master = Mastering(system, 1); + const std::vector pcm{0, 1000, 2000, 3000}; + const auto sampler = Sampler(system, pcm, 1); + Patch(sampler, master); + Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY); + const std::int16_t expected[Grain] = {0, 500, 1000, 1500, 2000, 2500, 3000, 1500}; + const auto out = RenderI16(system); + for (std::uint32_t i = 0; i < Grain; i++) Require(out[i] == expected[i]); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestUserData() { + uintptr_t value = 1; + Require(sceNgs2SystemSetUserData(0x1234, 5) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Require(sceNgs2SystemGetUserData(0x1234, &value) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE && value == 1); + const auto system = CreateSystem(); + Require(sceNgs2SystemGetUserData(system, &value) == SCE_NGS2_OK && value == 0); + Require(sceNgs2SystemSetUserData(system, 0xfedcba9876543210) == SCE_NGS2_OK); + Require(sceNgs2SystemGetUserData(system, &value) == SCE_NGS2_OK && value == 0xfedcba9876543210); + bool threw = false; + try { + sceNgs2SystemGetUserData(system, nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestMasteringGain() { + const auto system = CreateSystem(); + const auto stereo = Mastering(system, 2); + const auto surround = Mastering(system, 6); + const auto quad = Mastering(system, 4); + Control(stereo, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 0.5f, 1.0f}); + Control(surround, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 0.5f, 0.25f}); + Control(surround, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 1}); + Control(quad, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 0.5f, 0.25f}); + Control(quad, SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, 2}); + const std::vector pcm(Grain, 16384); + const auto toStereo = Sampler(system, pcm, 0); + const float stereoLevels[2] = {1.0f, 0.5f}; + Control(toStereo, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 2, stereoLevels}); + Control(toStereo, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(toStereo, stereo); + Event(toStereo, SCE_NGS2_VOICE_EVENT_PLAY); + const auto toSurround = Sampler(system, pcm, 0); + const float surroundLevels[6] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; + Control(toSurround, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 6, surroundLevels}); + Control(toSurround, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(toSurround, surround); + Event(toSurround, SCE_NGS2_VOICE_EVENT_PLAY); + const auto toQuad = Sampler(system, pcm, 0); + Control(toQuad, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 4, surroundLevels}); + Control(toQuad, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(toQuad, quad); + Event(toQuad, SCE_NGS2_VOICE_EVENT_PLAY); + bool rejected = false; + try { Control(stereo, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, NAN, 1.0f}); } catch (const std::exception&) { rejected = true; } + Require(rejected); + + std::vector outStereo(Grain * 2, -1.0f); + std::vector outSurround(Grain * 6, -1.0f); + std::vector outQuad(Grain * 4, -1.0f); + const Ngs2RenderBufferInfo info[3] = { + {outStereo.data(), outStereo.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 2}, + {outSurround.data(), outSurround.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 6}, + {outQuad.data(), outQuad.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 4}, + }; + Require(sceNgs2SystemRender(system, info, 3) == SCE_NGS2_OK); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(outStereo[i * 2] == 0.25f && outStereo[i * 2 + 1] == 0.125f); + for (std::uint32_t channel = 0; channel < 6; channel++) Require(outSurround[i * 6 + channel] == (channel == 3 ? 0.125f : 0.25f)); + for (std::uint32_t channel = 0; channel < 4; channel++) Require(outQuad[i * 4 + channel] == 0.25f); + } + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); + + const auto loudSystem = CreateSystem(); + const auto loud = Mastering(loudSystem, 1); + Control(loud, SCE_NGS2_MASTERING_VOICE_PARAM_GAIN, Ngs2MasteringVoiceGainParam{{}, 2.0f, 2.0f}); + const std::vector loudPcm(Grain * 2, 24576); + const auto toLoud = Sampler(loudSystem, loudPcm, 0); + Patch(toLoud, loud); + Event(toLoud, SCE_NGS2_VOICE_EVENT_PLAY); + auto clipped = RenderI16(loudSystem); + for (std::int16_t sample : clipped) Require(sample == 32767); + Control(loud, SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 1, 0}); + Event(loud, SCE_NGS2_VOICE_EVENT_PLAY); + auto reset = RenderI16(loudSystem); + for (std::int16_t sample : reset) Require(sample == 24576); + Require(sceNgs2SystemDestroy(loudSystem, nullptr) == SCE_NGS2_OK); +} + +static uintptr_t RackVoice(uintptr_t rack, std::uint32_t index) { + uintptr_t voice = 0; + Require(sceNgs2RackGetVoiceHandle(rack, index, &voice) == SCE_NGS2_OK && voice != 0); + return voice; +} + +static uintptr_t MasteringRack(uintptr_t system, std::uint32_t voices) { + Ngs2MasteringRackOption option{}; + option.rack_option.size = sizeof(option); + option.rack_option.max_grain_samples = 512; + option.rack_option.max_voices = voices; + option.rack_option.max_input_delay_blocks = 1; + option.rack_option.max_matrices = 1; + option.rack_option.max_ports = 8; + option.max_channels = 8; + Ngs2ContextBufferInfo query{}; + Require(sceNgs2RackQueryBufferSize(SCE_NGS2_RACK_ID_MASTERING, &option.rack_option, &query) == SCE_NGS2_OK); + const auto buffer = Buffer(query); + uintptr_t rack = 0; + Require(sceNgs2RackCreate(system, SCE_NGS2_RACK_ID_MASTERING, &option.rack_option, &buffer, &rack) == SCE_NGS2_OK && rack != 0); + return rack; +} + +static void StereoSource(uintptr_t voice, const std::vector& pcm, uintptr_t master) { + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}}); + const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast(pcm.size()), 0, 0}; + Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block}); + const float levels[2] = {1.0f, 0.5f}; + Control(voice, SCE_NGS2_VOICE_PARAM_MATRIX_LEVELS, Ngs2VoiceMatrixLevelsParam{{}, 0, 2, levels}); + Control(voice, SCE_NGS2_VOICE_PARAM_PORT_MATRIX, Ngs2VoicePortMatrixParam{{}, 0, 0}); + Patch(voice, master); +} + +static void TestStereoIntoSurround() { + const auto system = CreateSystem(); + const auto masteringRack = MasteringRack(system, 3); + const auto samplerRack = CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER); + uintptr_t masters[3]; + for (std::uint32_t i = 0; i < 3; i++) { + masters[i] = RackVoice(masteringRack, i); + Control(masters[i], SCE_NGS2_MASTERING_VOICE_PARAM_SETUP, Ngs2MasteringVoiceSetupParam{{}, 2, 0}); + Control(masters[i], SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT, Ngs2MasteringVoiceOutputParam{{}, i, 0}); + Event(masters[i], SCE_NGS2_VOICE_EVENT_PLAY); + } + const std::vector pcm(Grain, 16384); + for (std::uint32_t i = 0; i < 2; i++) { + const auto source = RackVoice(samplerRack, i); + StereoSource(source, pcm, masters[i]); + Event(source, SCE_NGS2_VOICE_EVENT_PLAY); + } + std::vector outFive(Grain * 6, -1.0f); + std::vector outSeven(Grain * 8, -1.0f); + std::vector outQuad(Grain * 4, -1.0f); + const Ngs2RenderBufferInfo info[3] = { + {outFive.data(), outFive.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 6}, + {outSeven.data(), outSeven.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 8}, + {outQuad.data(), outQuad.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, 4}, + }; + Require(sceNgs2SystemRender(system, info, 3) == SCE_NGS2_OK); + for (std::uint32_t i = 0; i < Grain; i++) { + Require(outFive[i * 6] == 0.5f && outFive[i * 6 + 1] == 0.25f); + for (std::uint32_t channel = 2; channel < 6; channel++) Require(outFive[i * 6 + channel] == 0.0f); + Require(outSeven[i * 8] == 0.5f && outSeven[i * 8 + 1] == 0.25f); + for (std::uint32_t channel = 2; channel < 8; channel++) Require(outSeven[i * 8 + channel] == 0.0f); + } + + const auto rejectedSource = RackVoice(samplerRack, 2); + StereoSource(rejectedSource, pcm, masters[2]); + Event(rejectedSource, SCE_NGS2_VOICE_EVENT_PLAY); + bool threw = false; + try { + sceNgs2SystemRender(system, info, 3); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + +static void TestLock() { + const auto system = CreateSystem(); + Require(sceNgs2SystemLock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Require(sceNgs2SystemUnlock(0x1234) == SCE_NGS2_ERROR_INVALID_SYSTEM_HANDLE); + Ngs2SystemInfo info{}; + std::thread free([&] { Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); }); + free.join(); + + Require(sceNgs2SystemLock(system) == SCE_NGS2_OK); + std::atomic done = false; + std::thread blocked([&] { + Require(sceNgs2SystemGetInfo(system, &info, sizeof(info)) == SCE_NGS2_OK); + done = true; + }); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Require(!done); + Require(sceNgs2SystemUnlock(system) == SCE_NGS2_OK); + blocked.join(); + Require(done); + Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK); +} + static void TestAllocator() { const Ngs2BufferAllocator allocator{Allocate, Release, 9}; uintptr_t system = 0; @@ -171,6 +442,11 @@ int main() { TestPcmBlockEnd(); TestPitchAndRepeat(); TestSubmixerMatrix(); + TestSampleRate(); + TestUserData(); + TestMasteringGain(); + TestStereoIntoSurround(); + TestLock(); TestAllocator(); return 0; } diff --git a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp index 223618449..ecfa52ce3 100644 --- a/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp +++ b/core/libs/prx/libSceNgs2.native/tests/Ngs2Test.hpp @@ -18,16 +18,24 @@ int APS5_VABI sceNgs2SystemCreateWithAllocator(const Ngs2SystemOption*, const Ng int APS5_VABI sceNgs2SystemDestroy(uintptr_t, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2SystemGetInfo(uintptr_t, Ngs2SystemInfo*, size_t); int APS5_VABI sceNgs2SystemSetGrainSamples(uintptr_t, uint32_t); +int APS5_VABI sceNgs2SystemSetSampleRate(uintptr_t, uint32_t); +int APS5_VABI sceNgs2SystemSetUserData(uintptr_t, uintptr_t); +int APS5_VABI sceNgs2SystemGetUserData(uintptr_t, uintptr_t*); +int APS5_VABI sceNgs2SystemLock(uintptr_t); +int APS5_VABI sceNgs2SystemUnlock(uintptr_t); int APS5_VABI sceNgs2SystemRender(uintptr_t, const Ngs2RenderBufferInfo*, uint32_t); int APS5_VABI sceNgs2RackQueryBufferSize(uint32_t, const Ngs2RackOption*, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2RackCreate(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2ContextBufferInfo*, uintptr_t*); int APS5_VABI sceNgs2RackCreateWithAllocator(uintptr_t, uint32_t, const Ngs2RackOption*, const Ngs2BufferAllocator*, uintptr_t*); int APS5_VABI sceNgs2RackDestroy(uintptr_t, Ngs2ContextBufferInfo*); int APS5_VABI sceNgs2RackGetVoiceHandle(uintptr_t, uint32_t, uintptr_t*); +int APS5_VABI sceNgs2RackGetInfo(uintptr_t, Ngs2RackInfo*, size_t); int APS5_VABI sceNgs2VoiceControl(uintptr_t, const Ngs2VoiceParamHeader*); int APS5_VABI sceNgs2VoiceRunCommands(uintptr_t, const Ngs2VoiceCommand*, size_t); int APS5_VABI sceNgs2VoiceGetState(uintptr_t, Ngs2VoiceState*, size_t); int APS5_VABI sceNgs2VoiceGetStateFlags(uintptr_t, uint32_t*); +int APS5_VABI sceNgs2VoiceGetPortInfo(uintptr_t, uint32_t, Ngs2VoicePortInfo*, size_t); +int APS5_VABI sceNgs2VoiceQueryInfo(uintptr_t, uint32_t, void*, size_t); } inline void Check(bool value, int line) { @@ -40,7 +48,7 @@ inline void Check(bool value, int line) { inline constexpr std::uint32_t Grain = 8; -inline std::vector buffers[16]; +inline std::vector buffers[32]; inline std::size_t usedBuffers = 0; inline Ngs2ContextBufferInfo Buffer(const Ngs2ContextBufferInfo& query) { diff --git a/core/libs/prx/libSceNgs2/CMakeLists.txt b/core/libs/prx/libSceNgs2/CMakeLists.txt new file mode 100644 index 000000000..8e57a8733 --- /dev/null +++ b/core/libs/prx/libSceNgs2/CMakeLists.txt @@ -0,0 +1,20 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/Ngs2Library.cmake) +add_sce_ngs2_library(libSceNgs2) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(ngs2_core_render_tests ${CMAKE_CURRENT_SOURCE_DIR}/../libSceNgs2.native/tests/Ngs2Render.cpp) + target_include_directories(ngs2_core_render_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src) + target_link_libraries(ngs2_core_render_tests PRIVATE libSceNgs2 atrac9 libkernel libc) + configure_windows_unwind(ngs2_core_render_tests) + add_test(NAME ngs2_core_render COMMAND ngs2_core_render_tests) + if(WIN32) + set_tests_properties(ngs2_core_render PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpCommerce/Export.cpp b/core/libs/prx/libSceNpCommerce/Export.cpp index 6b2bd01d9..525178f39 100644 --- a/core/libs/prx/libSceNpCommerce/Export.cpp +++ b/core/libs/prx/libSceNpCommerce/Export.cpp @@ -79,4 +79,9 @@ int APS5_VABI sceNpCommerceSetPsStoreIconLayout(int layout) { return 0; } +int APS5_VABI sceNpCommerceDialogOpen2(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt b/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt deleted file mode 100644 index 2519e8cc8..000000000 --- a/core/libs/prx/libSceNpCppWebApi/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ -add_library(libSceNpCppWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) -target_include_directories(libSceNpCppWebApi PRIVATE ${LIBS_INCLUDE_DIR}) -configure_windows_unwind(libSceNpCppWebApi) - -if(MINGW) - foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) - string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") - endforeach() -endif() - -if(BUILD_TESTING) - add_test_executable(guest_np_cpp_web_api_string_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpCppWebApiString.cpp) - target_include_directories(guest_np_cpp_web_api_string_tests PRIVATE ${LIBS_INCLUDE_DIR}) - target_link_libraries(guest_np_cpp_web_api_string_tests PRIVATE libSceNpCppWebApi libc) - configure_windows_unwind(guest_np_cpp_web_api_string_tests) - add_test(NAME guest_np_cpp_web_api_string COMMAND guest_np_cpp_web_api_string_tests) - if(WIN32) - set_tests_properties(guest_np_cpp_web_api_string PROPERTIES ENVIRONMENT_MODIFICATION - "PATH=path_list_prepend:$") - endif() -endif() diff --git a/core/libs/prx/libSceNpCppWebApi/Export.cpp b/core/libs/prx/libSceNpCppWebApi/Export.cpp deleted file mode 100644 index 75caf2847..000000000 --- a/core/libs/prx/libSceNpCppWebApi/Export.cpp +++ /dev/null @@ -1,1194 +0,0 @@ -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -static constexpr int SCE_NP_WEBAPI_ERROR_UNAVAILABLE = static_cast(0x80552901); - -// Offline PSN: request entry points (API calls, transaction start/readData, factories) fail with -// SCE_NP_WEBAPI_ERROR_UNAVAILABLE; parameter bookkeeping succeeds; response accessors stay unimplemented -// because a failed request never produces a response. -extern "C" { - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V111UserFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_4UserEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody18setStartSerialRankERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setGroupERKNS3_5GroupE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setLimitERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody8setUsersERKNS1_6Common6VectorINS5_12IntrusivePtrINS3_4UserEEEEE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V121GetRankingRequestBody9setOffsetERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122GetRankingResponseBody10getEntriesEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody10setCommentEPKc() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody12setSmallDataEPKvm() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody15setNeedsTmpRankERKb() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody19setComparedDateTimeERK10SceRtcTick() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V122RecordScoreRequestBody7setPcIdERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V128GetRankingRequestBodyFactory6createEPNS1_6Common10LibContextEPNS5_12IntrusivePtrINS3_21GetRankingRequestBodyEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V129RecordScoreRequestBodyFactory6createEPNS1_6Common10LibContextElPNS5_12IntrusivePtrINS3_22RecordScoreRequestBodyEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V14User12setAccountIdERKm() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V14User7setPcIdERKi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi10getRankingEiRKNS4_21ParameterToGetRankingERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_22GetRankingResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking10initializeEPNS1_6Common10LibContextEi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking24setgetRankingRequestBodyENS1_6Common12IntrusivePtrINS3_21GetRankingRequestBodyEEE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRanking9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRankingC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi21ParameterToGetRankingD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi22getLargeDataByObjectIdEiRKNS4_33ParameterToGetLargeDataByObjectIdERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_37GetLargeDataByObjectIdResponseHeadersEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectId10initializeEPNS1_6Common10LibContextEPKc() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectId9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectIdC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V17ViewApi33ParameterToGetLargeDataByObjectIdD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi18getBoardDefinitionEiRKNS4_29ParameterToGetBoardDefinitionERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_30GetBoardDefinitionResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinition10initializeEPNS1_6Common10LibContextEi() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinition9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinitionC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19BoardsApi29ParameterToGetBoardDefinitionD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi11recordScoreEiRKNS4_22ParameterToRecordScoreERNS1_6Common11TransactionINS8_12IntrusivePtrINS3_23RecordScoreResponseBodyEEENSA_INS4_26RecordScoreResponseHeadersEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi15recordLargeDataEiRKNS4_26ParameterToRecordLargeDataERNS1_6Common19UpStreamTransactionINS8_12IntrusivePtrINS3_27RecordLargeDataResponseBodyEEENSA_INS8_18ResponseHeaderBaseEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore10initializeEPNS1_6Common10LibContextEiNS6_12IntrusivePtrINS3_22RecordScoreRequestBodyEEE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore22setxPsnAtomicOperationENS5_19XPsnAtomicOperationE() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScore9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScoreC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi22ParameterToRecordScoreD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeData10initializeEPNS1_6Common10LibContextEiNS5_19XPsnAtomicOperationEPKc() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeData9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi12Leaderboards2V19RecordApi26ParameterToRecordLargeDataD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi12downloadDataEiRKNS4_23ParameterToDownloadDataERNS1_6Common21DownStreamTransactionINS8_12IntrusivePtrINS4_27DownloadDataResponseHeadersEEEEE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData10initializeEPNS1_6Common10LibContextEPKci() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadData9terminateEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi17TitleCloudStorage2V17DataApi23ParameterToDownloadDataD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common10InitParamsD1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common10LibContextC1Ev(uint64_t* self) { - *self = 0; - return 0; -} - -// Initialization succeeds as on an offline console (see libSceJson2); individual requests fail. -int APS5_VABI _ZN3sce2Np9CppWebApi6Common10initializeERKNS2_10InitParamsERNS2_10LibContextE(const void* params, uint64_t* context) { - (void)params; - if (context) *context = 1; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { - (void)self; - return 0; -} - -struct IntrusivePtr { - void* object; - void (APS5_VABI* deleter)(void*); - void* libContext; -}; -static_assert(sizeof(IntrusivePtr) == 0x18); - -static constexpr std::size_t ReferenceCountOffset = 0; -static constexpr std::size_t VectorReferenceCountOffset = 0x30; - -static std::atomic_ref ReferenceCount(void* object, const std::size_t offset) { - return std::atomic_ref(*reinterpret_cast(static_cast(object) + offset)); -} - -static void CopyIntrusivePtr(IntrusivePtr* self, const IntrusivePtr* source, const std::size_t referenceCountOffset) { - *self = *source; - if (self->object) ReferenceCount(self->object, referenceCountOffset).fetch_add(1); -} - -static void ReleaseIntrusivePtr(IntrusivePtr* self, const std::size_t referenceCountOffset, const char* function) { - if (!self->object) return; - if (ReferenceCount(self->object, referenceCountOffset).fetch_sub(1) != 1) return; - if (!self->deleter) NotImplemented_nid_no_patch(function); - self->deleter(self->object); - self->object = nullptr; - self->deleter = nullptr; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { - CopyIntrusivePtr(self, source, ReferenceCountOffset); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(IntrusivePtr* self, const IntrusivePtr* source) { - CopyIntrusivePtr(self, source, ReferenceCountOffset); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev(IntrusivePtr* self) { - *self = {}; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, ReferenceCountOffset, __func__); -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev(IntrusivePtr* self) { - ReleaseIntrusivePtr(self, VectorReferenceCountOffset, __func__); -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common13ConstIteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEED2Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE5startEPNS2_10LibContextEm() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE8sendDataEPKvm() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common19UpStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V127RecordLargeDataResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEE8readDataEPcm() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_12Leaderboards2V17ViewApi37GetLargeDataByObjectIdResponseHeadersEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE5startEPNS2_10LibContextE() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE6finishEv() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEE8readDataEPcm() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEEC1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common21DownStreamTransactionINS2_12IntrusivePtrINS1_17TitleCloudStorage2V17DataApi27DownloadDataResponseHeadersEEEED1Ev(void* self) { - (void)self; - return 0; -} - -struct CommonString { - std::int32_t referenceCount; - char* buffer; - std::uint64_t bufferSize; - void* libContext; -}; -static_assert(sizeof(CommonString) == 0x20 && offsetof(CommonString, buffer) == 0x08); -static_assert(offsetof(CommonString, bufferSize) == 0x10 && offsetof(CommonString, libContext) == 0x18); - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(CommonString* self, void* libContext) { - self->referenceCount = 0; - self->buffer = nullptr; - self->bufferSize = 0; - self->libContext = libContext; -} - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(CommonString* self) { - if (self->buffer != nullptr) NotImplemented_nid_no_patch("sce::Np::CppWebApi::Common::String buffer release"); -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEE8pushBackERKS8_() { - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEEC1EPNS2_10LibContextE(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V14UserEEEED1Ev(void* self) { - (void)self; - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEE3endEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common6VectorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEE5beginEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEppEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZN3sce2Np9CppWebApi6Common9terminateERNS2_10LibContextE(uint64_t* context) { - *context = 0; - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V122GetRankingResponseBody22getLastUpdatedDateTimeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V123RecordScoreResponseBody10getTmpRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V123RecordScoreResponseBody16getTmpSerialRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody11getSortModeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13getEntryLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13getUpdateModeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody13sortModeIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody15entryLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody15updateModeIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody16getMaxScoreLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody16getMinScoreLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody18maxScoreLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody18minScoreLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody20getLargeDataNumLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody21getLargeDataSizeLimitEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody22largeDataNumLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V130GetBoardDefinitionResponseBody23largeDataSizeLimitIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry10getCommentEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry11getObjectIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry11getOnlineIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12commentIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12getAccountIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry12getSmallDataEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry13getSerialRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry13objectIdIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry14getHighestRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry14smallDataIsSetEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry20getHighestSerialRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry7getPcIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry7getRankEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V15Entry8getScoreEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi12Leaderboards2V19RecordApi26RecordScoreResponseHeaders24getXPsnAtomicOperationIdEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE11getResponseERS8_() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common11TransactionINS2_12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEENS4_INS2_18ResponseHeaderBaseEEEE11getResponseERS8_() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv(const IntrusivePtr* self) { - return self->object; -} - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv(const IntrusivePtr* self) { - return self->object; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common15TransactionBaseINS2_12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEENS4_INS6_9RecordApi26RecordScoreResponseHeadersEEEE18getResponseHeadersERSB_() { - return SCE_NP_WEBAPI_ERROR_UNAVAILABLE; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6Binary4sizeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common6Binary9getBinaryEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const CommonString* self) { - return self->buffer != nullptr ? self->buffer : ""; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEdeEv() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI _ZNK3sce2Np9CppWebApi6Common8IteratorINS2_12IntrusivePtrINS1_12Leaderboards2V15EntryEEEEneERKS9_() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - - -APS5_EXPORT("+gSMGXHgrPU", sceNpCppWebApiUnknown00); -int APS5_VABI sceNpCppWebApiUnknown00(void) { - NotImplemented_nid_no_patch("+gSMGXHgrPU"); - return 0; -} - -APS5_EXPORT("-XNfwZTFkWw", sceNpCppWebApiUnknown01); -int APS5_VABI sceNpCppWebApiUnknown01(void) { - NotImplemented_nid_no_patch("-XNfwZTFkWw"); - return 0; -} - -APS5_EXPORT("0f5+eBcgB14", sceNpCppWebApiUnknown02); -int APS5_VABI sceNpCppWebApiUnknown02(void) { - NotImplemented_nid_no_patch("0f5+eBcgB14"); - return 0; -} - -APS5_EXPORT("1iq5Jtw4jVs", sceNpCppWebApiUnknown03); -int APS5_VABI sceNpCppWebApiUnknown03(void) { - NotImplemented_nid_no_patch("1iq5Jtw4jVs"); - return 0; -} - -APS5_EXPORT("1wpCXuFzH10", sceNpCppWebApiUnknown04); -int APS5_VABI sceNpCppWebApiUnknown04(void) { - NotImplemented_nid_no_patch("1wpCXuFzH10"); - return 0; -} - -APS5_EXPORT("4AFThLW28xQ", sceNpCppWebApiUnknown05); -int APS5_VABI sceNpCppWebApiUnknown05(void) { - NotImplemented_nid_no_patch("4AFThLW28xQ"); - return 0; -} - -APS5_EXPORT("8-9Y4oS+OuE", sceNpCppWebApiUnknown06); -int APS5_VABI sceNpCppWebApiUnknown06(void) { - NotImplemented_nid_no_patch("8-9Y4oS+OuE"); - return 0; -} - -APS5_EXPORT("8dWkWjo5EeE", sceNpCppWebApiUnknown07); -int APS5_VABI sceNpCppWebApiUnknown07(void) { - NotImplemented_nid_no_patch("8dWkWjo5EeE"); - return 0; -} - -APS5_EXPORT("BHvfIGdarBQ", sceNpCppWebApiUnknown08); -int APS5_VABI sceNpCppWebApiUnknown08(void) { - NotImplemented_nid_no_patch("BHvfIGdarBQ"); - return 0; -} - -APS5_EXPORT("Fxxr5lYBfl4", sceNpCppWebApiUnknown09); -int APS5_VABI sceNpCppWebApiUnknown09(void) { - NotImplemented_nid_no_patch("Fxxr5lYBfl4"); - return 0; -} - -APS5_EXPORT("M1LnjRscx-w", sceNpCppWebApiUnknown10); -int APS5_VABI sceNpCppWebApiUnknown10(void) { - NotImplemented_nid_no_patch("M1LnjRscx-w"); - return 0; -} - -APS5_EXPORT("PsX657reZeo", sceNpCppWebApiUnknown11); -int APS5_VABI sceNpCppWebApiUnknown11(void) { - NotImplemented_nid_no_patch("PsX657reZeo"); - return 0; -} - -APS5_EXPORT("RvH9AZ5rCkQ", sceNpCppWebApiUnknown12); -int APS5_VABI sceNpCppWebApiUnknown12(void) { - NotImplemented_nid_no_patch("RvH9AZ5rCkQ"); - return 0; -} - -APS5_EXPORT("SR3BKonD8yk", sceNpCppWebApiUnknown13); -int APS5_VABI sceNpCppWebApiUnknown13(void) { - NotImplemented_nid_no_patch("SR3BKonD8yk"); - return 0; -} - -APS5_EXPORT("TSoQhYKyq5g", sceNpCppWebApiUnknown14); -int APS5_VABI sceNpCppWebApiUnknown14(void) { - NotImplemented_nid_no_patch("TSoQhYKyq5g"); - return 0; -} - -APS5_EXPORT("TgsJjQqp+2k", sceNpCppWebApiUnknown15); -int APS5_VABI sceNpCppWebApiUnknown15(void) { - NotImplemented_nid_no_patch("TgsJjQqp+2k"); - return 0; -} - -APS5_EXPORT("UHQxE3HhTXA", sceNpCppWebApiUnknown16); -int APS5_VABI sceNpCppWebApiUnknown16(void) { - NotImplemented_nid_no_patch("UHQxE3HhTXA"); - return 0; -} - -APS5_EXPORT("ZN-glpk0Rug", sceNpCppWebApiUnknown17); -int APS5_VABI sceNpCppWebApiUnknown17(void) { - NotImplemented_nid_no_patch("ZN-glpk0Rug"); - return 0; -} - -APS5_EXPORT("ZQzXwokCnUE", sceNpCppWebApiUnknown18); -int APS5_VABI sceNpCppWebApiUnknown18(void) { - NotImplemented_nid_no_patch("ZQzXwokCnUE"); - return 0; -} - -APS5_EXPORT("ZwjmbSr4Cxc", sceNpCppWebApiUnknown19); -int APS5_VABI sceNpCppWebApiUnknown19(void) { - NotImplemented_nid_no_patch("ZwjmbSr4Cxc"); - return 0; -} - -APS5_EXPORT("b7leY-LNVnI", sceNpCppWebApiUnknown20); -int APS5_VABI sceNpCppWebApiUnknown20(void) { - NotImplemented_nid_no_patch("b7leY-LNVnI"); - return 0; -} - -APS5_EXPORT("c3J9K9XbQqE", sceNpCppWebApiUnknown21); -int APS5_VABI sceNpCppWebApiUnknown21(void) { - NotImplemented_nid_no_patch("c3J9K9XbQqE"); - return 0; -} - -APS5_EXPORT("cSLbQUQO2Ns", sceNpCppWebApiUnknown22); -int APS5_VABI sceNpCppWebApiUnknown22(void) { - NotImplemented_nid_no_patch("cSLbQUQO2Ns"); - return 0; -} - -APS5_EXPORT("cqOxjL0ZfFA", sceNpCppWebApiUnknown23); -int APS5_VABI sceNpCppWebApiUnknown23(void) { - NotImplemented_nid_no_patch("cqOxjL0ZfFA"); - return 0; -} - -APS5_EXPORT("d1eEjWR60wk", sceNpCppWebApiUnknown24); -int APS5_VABI sceNpCppWebApiUnknown24(void) { - NotImplemented_nid_no_patch("d1eEjWR60wk"); - return 0; -} - -APS5_EXPORT("epwr+cBCIFs", sceNpCppWebApiUnknown25); -int APS5_VABI sceNpCppWebApiUnknown25(void) { - NotImplemented_nid_no_patch("epwr+cBCIFs"); - return 0; -} - -APS5_EXPORT("eruduJ0KrT0", sceNpCppWebApiUnknown26); -int APS5_VABI sceNpCppWebApiUnknown26(void) { - NotImplemented_nid_no_patch("eruduJ0KrT0"); - return 0; -} - -APS5_EXPORT("fG06a38iao8", sceNpCppWebApiUnknown27); -int APS5_VABI sceNpCppWebApiUnknown27(void) { - NotImplemented_nid_no_patch("fG06a38iao8"); - return 0; -} - -APS5_EXPORT("jfpSx14AeLc", sceNpCppWebApiUnknown28); -int APS5_VABI sceNpCppWebApiUnknown28(void) { - NotImplemented_nid_no_patch("jfpSx14AeLc"); - return 0; -} - -APS5_EXPORT("kBxwE4YfbIM", sceNpCppWebApiUnknown29); -int APS5_VABI sceNpCppWebApiUnknown29(void) { - NotImplemented_nid_no_patch("kBxwE4YfbIM"); - return 0; -} - -APS5_EXPORT("kz2Z38yTLq0", sceNpCppWebApiUnknown30); -int APS5_VABI sceNpCppWebApiUnknown30(void) { - NotImplemented_nid_no_patch("kz2Z38yTLq0"); - return 0; -} - -APS5_EXPORT("lB8driFKaoU", sceNpCppWebApiUnknown31); -int APS5_VABI sceNpCppWebApiUnknown31(void) { - NotImplemented_nid_no_patch("lB8driFKaoU"); - return 0; -} - -APS5_EXPORT("mCsV7izOkjY", sceNpCppWebApiUnknown32); -int APS5_VABI sceNpCppWebApiUnknown32(void) { - NotImplemented_nid_no_patch("mCsV7izOkjY"); - return 0; -} - -APS5_EXPORT("n1yCLMxpseQ", sceNpCppWebApiUnknown33); -int APS5_VABI sceNpCppWebApiUnknown33(void) { - NotImplemented_nid_no_patch("n1yCLMxpseQ"); - return 0; -} - -APS5_EXPORT("nL9vSVq-29s", sceNpCppWebApiUnknown34); -int APS5_VABI sceNpCppWebApiUnknown34(void) { - NotImplemented_nid_no_patch("nL9vSVq-29s"); - return 0; -} - -APS5_EXPORT("nmz5JYKcMfY", sceNpCppWebApiUnknown35); -int APS5_VABI sceNpCppWebApiUnknown35(void) { - NotImplemented_nid_no_patch("nmz5JYKcMfY"); - return 0; -} - -APS5_EXPORT("oHRl7a+zdMU", sceNpCppWebApiUnknown36); -int APS5_VABI sceNpCppWebApiUnknown36(void) { - NotImplemented_nid_no_patch("oHRl7a+zdMU"); - return 0; -} - -APS5_EXPORT("pSpO3hPNv64", sceNpCppWebApiUnknown37); -int APS5_VABI sceNpCppWebApiUnknown37(void) { - NotImplemented_nid_no_patch("pSpO3hPNv64"); - return 0; -} - -APS5_EXPORT("q4UFICay6Hk", sceNpCppWebApiUnknown38); -int APS5_VABI sceNpCppWebApiUnknown38(void) { - NotImplemented_nid_no_patch("q4UFICay6Hk"); - return 0; -} - -APS5_EXPORT("qiD2PU2Jstc", sceNpCppWebApiUnknown39); -int APS5_VABI sceNpCppWebApiUnknown39(void) { - NotImplemented_nid_no_patch("qiD2PU2Jstc"); - return 0; -} - -APS5_EXPORT("tFe964qzGEM", sceNpCppWebApiUnknown40); -int APS5_VABI sceNpCppWebApiUnknown40(void) { - NotImplemented_nid_no_patch("tFe964qzGEM"); - return 0; -} - -APS5_EXPORT("tHaO36kincQ", sceNpCppWebApiUnknown41); -int APS5_VABI sceNpCppWebApiUnknown41(void) { - NotImplemented_nid_no_patch("tHaO36kincQ"); - return 0; -} - -APS5_EXPORT("umm5m+mXiZs", sceNpCppWebApiUnknown42); -int APS5_VABI sceNpCppWebApiUnknown42(void) { - NotImplemented_nid_no_patch("umm5m+mXiZs"); - return 0; -} - -APS5_EXPORT("uwhztB49KOQ", sceNpCppWebApiUnknown43); -int APS5_VABI sceNpCppWebApiUnknown43(void) { - NotImplemented_nid_no_patch("uwhztB49KOQ"); - return 0; -} - -APS5_EXPORT("w4K4nTYwhVE", sceNpCppWebApiUnknown44); -int APS5_VABI sceNpCppWebApiUnknown44(void) { - NotImplemented_nid_no_patch("w4K4nTYwhVE"); - return 0; -} - -APS5_EXPORT("wFcm6bgWB9Q", sceNpCppWebApiUnknown45); -int APS5_VABI sceNpCppWebApiUnknown45(void) { - NotImplemented_nid_no_patch("wFcm6bgWB9Q"); - return 0; -} - -APS5_EXPORT("wp8+c84G5Xw", sceNpCppWebApiUnknown46); -int APS5_VABI sceNpCppWebApiUnknown46(void) { - NotImplemented_nid_no_patch("wp8+c84G5Xw"); - return 0; -} - -APS5_EXPORT("zjqTmP0ST9A", sceNpCppWebApiUnknown47); -int APS5_VABI sceNpCppWebApiUnknown47(void) { - NotImplemented_nid_no_patch("zjqTmP0ST9A"); - return 0; -} - -APS5_EXPORT("zzO8ZGJ74ng", sceNpCppWebApiUnknown48); -int APS5_VABI sceNpCppWebApiUnknown48(void) { - NotImplemented_nid_no_patch("zzO8ZGJ74ng"); - return 0; -} - -APS5_EXPORT("12wY179+CE8", sceNpCppWebApiUnknown49); -int APS5_VABI sceNpCppWebApiUnknown49(void) { - NotImplemented_nid_no_patch("12wY179+CE8"); - return 0; -} - -APS5_EXPORT("3bjEFf8OhTs", sceNpCppWebApiUnknown50); -int APS5_VABI sceNpCppWebApiUnknown50(void) { - NotImplemented_nid_no_patch("3bjEFf8OhTs"); - return 0; -} - -APS5_EXPORT("6FK4IOnANkc", sceNpCppWebApiUnknown51); -int APS5_VABI sceNpCppWebApiUnknown51(void) { - NotImplemented_nid_no_patch("6FK4IOnANkc"); - return 0; -} - -APS5_EXPORT("EJAOHYWUki4", sceNpCppWebApiUnknown52); -int APS5_VABI sceNpCppWebApiUnknown52(void) { - NotImplemented_nid_no_patch("EJAOHYWUki4"); - return 0; -} - -APS5_EXPORT("FlagLhjAEmc", sceNpCppWebApiUnknown53); -int APS5_VABI sceNpCppWebApiUnknown53(void) { - NotImplemented_nid_no_patch("FlagLhjAEmc"); - return 0; -} - -APS5_EXPORT("G3h-NDHnyW4", sceNpCppWebApiUnknown54); -int APS5_VABI sceNpCppWebApiUnknown54(void) { - NotImplemented_nid_no_patch("G3h-NDHnyW4"); - return 0; -} - -APS5_EXPORT("H0XjNhflED4", sceNpCppWebApiUnknown55); -int APS5_VABI sceNpCppWebApiUnknown55(void) { - NotImplemented_nid_no_patch("H0XjNhflED4"); - return 0; -} - -APS5_EXPORT("IbWx007Acn4", sceNpCppWebApiUnknown56); -int APS5_VABI sceNpCppWebApiUnknown56(void) { - NotImplemented_nid_no_patch("IbWx007Acn4"); - return 0; -} - -APS5_EXPORT("Is+nI7Hq9jU", sceNpCppWebApiUnknown57); -int APS5_VABI sceNpCppWebApiUnknown57(void) { - NotImplemented_nid_no_patch("Is+nI7Hq9jU"); - return 0; -} - -APS5_EXPORT("Kc4x2uy0FFk", sceNpCppWebApiUnknown58); -int APS5_VABI sceNpCppWebApiUnknown58(void) { - NotImplemented_nid_no_patch("Kc4x2uy0FFk"); - return 0; -} - -APS5_EXPORT("OK+Ggpve7B0", sceNpCppWebApiUnknown59); -int APS5_VABI sceNpCppWebApiUnknown59(void) { - NotImplemented_nid_no_patch("OK+Ggpve7B0"); - return 0; -} - -APS5_EXPORT("P3daBLFFREw", sceNpCppWebApiUnknown60); -int APS5_VABI sceNpCppWebApiUnknown60(void) { - NotImplemented_nid_no_patch("P3daBLFFREw"); - return 0; -} - -APS5_EXPORT("RZcseJ3THfY", sceNpCppWebApiUnknown61); -int APS5_VABI sceNpCppWebApiUnknown61(void) { - NotImplemented_nid_no_patch("RZcseJ3THfY"); - return 0; -} - -APS5_EXPORT("RbvteD35X-8", sceNpCppWebApiUnknown62); -int APS5_VABI sceNpCppWebApiUnknown62(void) { - NotImplemented_nid_no_patch("RbvteD35X-8"); - return 0; -} - -APS5_EXPORT("XCtf+23QT0k", sceNpCppWebApiUnknown63); -int APS5_VABI sceNpCppWebApiUnknown63(void) { - NotImplemented_nid_no_patch("XCtf+23QT0k"); - return 0; -} - -APS5_EXPORT("a4q15LI1a4E", sceNpCppWebApiUnknown64); -int APS5_VABI sceNpCppWebApiUnknown64(void) { - NotImplemented_nid_no_patch("a4q15LI1a4E"); - return 0; -} - -APS5_EXPORT("eT4TQB7OsLA", sceNpCppWebApiUnknown65); -int APS5_VABI sceNpCppWebApiUnknown65(void) { - NotImplemented_nid_no_patch("eT4TQB7OsLA"); - return 0; -} - -APS5_EXPORT("g2dEqFnhFuw", sceNpCppWebApiUnknown66); -int APS5_VABI sceNpCppWebApiUnknown66(void) { - NotImplemented_nid_no_patch("g2dEqFnhFuw"); - return 0; -} - -APS5_EXPORT("lPAMjFZEpt8", sceNpCppWebApiUnknown67); -int APS5_VABI sceNpCppWebApiUnknown67(void) { - NotImplemented_nid_no_patch("lPAMjFZEpt8"); - return 0; -} - -APS5_EXPORT("mBagn+lW-iM", sceNpCppWebApiUnknown68); -int APS5_VABI sceNpCppWebApiUnknown68(void) { - NotImplemented_nid_no_patch("mBagn+lW-iM"); - return 0; -} - -APS5_EXPORT("mkWsKEh0h0o", sceNpCppWebApiUnknown69); -int APS5_VABI sceNpCppWebApiUnknown69(void) { - NotImplemented_nid_no_patch("mkWsKEh0h0o"); - return 0; -} - -APS5_EXPORT("nE0ooeCMRm8", sceNpCppWebApiUnknown70); -int APS5_VABI sceNpCppWebApiUnknown70(void) { - NotImplemented_nid_no_patch("nE0ooeCMRm8"); - return 0; -} - -APS5_EXPORT("qf2I9BlKXis", sceNpCppWebApiUnknown71); -int APS5_VABI sceNpCppWebApiUnknown71(void) { - NotImplemented_nid_no_patch("qf2I9BlKXis"); - return 0; -} - -APS5_EXPORT("xwy1I52dE8o", sceNpCppWebApiUnknown72); -int APS5_VABI sceNpCppWebApiUnknown72(void) { - NotImplemented_nid_no_patch("xwy1I52dE8o"); - return 0; -} -} diff --git a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp index 05fc69362..ccf92707c 100644 --- a/core/libs/prx/libSceNpEntitlementAccess/Export.cpp +++ b/core/libs/prx/libSceNpEntitlementAccess/Export.cpp @@ -118,4 +118,55 @@ int APS5_VABI sceNpEntitlementAccessRequestConsumeUnifiedEntitlement(void) { return SCE_NP_ERROR_SIGNED_OUT; } + +int APS5_VABI sceNpEntitlementAccessRequestConsumeServiceEntitlement(void) { + return SCE_NP_ERROR_SIGNED_OUT; +} + + +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessGetPftFlag(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollServiceEntitlementInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessPollUnifiedEntitlementInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestServiceEntitlementInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpEntitlementAccessRequestUnifiedEntitlementInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index ca76483a5..f48425a3f 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -191,4 +191,9 @@ int APS5_VABI sceNpUnregisterPremiumEventCallback(void) { return 0; } +int APS5_VABI sceNpGetUserIdByAccountId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpPartner001/Export.cpp b/core/libs/prx/libSceNpPartner001/Export.cpp new file mode 100644 index 000000000..ef5b77a1f --- /dev/null +++ b/core/libs/prx/libSceNpPartner001/Export.cpp @@ -0,0 +1,17 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +APS5_EXPORT("7CxI50-xlCk", sceNpPartnerUnknown_7CxI50_MxlCk); +int APS5_VABI sceNpPartnerUnknown_7CxI50_MxlCk(void) { + NotImplemented_nid_no_patch("7CxI50-xlCk"); + return 0; +} + +APS5_EXPORT("pMxXhNozUX8", sceNpPartnerUnknown_pMxXhNozUX8); +int APS5_VABI sceNpPartnerUnknown_pMxXhNozUX8(void) { + NotImplemented_nid_no_patch("pMxXhNozUX8"); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSessionSignaling/Export.cpp b/core/libs/prx/libSceNpSessionSignaling/Export.cpp index eb3ee27e0..996613a42 100644 --- a/core/libs/prx/libSceNpSessionSignaling/Export.cpp +++ b/core/libs/prx/libSceNpSessionSignaling/Export.cpp @@ -5,7 +5,7 @@ #include // Peer-to-peer signaling needs the network: contexts exist, but sessions never activate. -static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast(0x80552D02); +static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_INVALID_ARGUMENT = static_cast(0x80553303); static constexpr int SCE_NP_SESSION_SIGNALING_ERROR_UNAVAILABLE = static_cast(0x80552D06); static std::atomic g_nextContext{1}; diff --git a/core/libs/prx/libSceNpTrophy2/src/Context.cpp b/core/libs/prx/libSceNpTrophy2/src/Context.cpp index e259afb73..0a7bd1daf 100644 --- a/core/libs/prx/libSceNpTrophy2/src/Context.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/Context.cpp @@ -34,4 +34,15 @@ int APS5_VABI sceNpTrophy2UnregisterUnlockCallback() { return SCE_NP_TROPHY2_OK; } + +int APS5_VABI sceNpTrophy2GetRewardIcon(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophy2ShowTrophyList(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp index 6a41aa647..0c5b320f6 100644 --- a/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp +++ b/core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp @@ -9,7 +9,7 @@ extern "C" { int APS5_VABI sceNpTrophy2GetGameInfo(int context, int handle, NpTrophy2GameDetails* details, NpTrophy2GameData* data) { - if (details == nullptr || data == nullptr) { + if (details == nullptr && data == nullptr) { APS5_INVALID_ARG_EX; } (void)context; diff --git a/core/libs/prx/libSceNpWebApi2/Export.cpp b/core/libs/prx/libSceNpWebApi2/Export.cpp index 7e9445433..2c3e02d70 100644 --- a/core/libs/prx/libSceNpWebApi2/Export.cpp +++ b/core/libs/prx/libSceNpWebApi2/Export.cpp @@ -153,4 +153,9 @@ int APS5_VABI sceNpWebApi2PushEventUnregisterPushContextCallback() { return 0; } +int APS5_VABI sceNpWebApi2SetRequestTimeout() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index adc42a6b7..c426e5067 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -150,7 +150,7 @@ int APS5_VABI scePadRead_nid_postfix(int handle, PadData* data, int num) { } int APS5_VABI scePadReadState(int handle, PadData* data) { - if (handle != 1) APS5_INVALID_ARG_EX; + if (handle != PAD_HANDLE) return PAD_ERROR_INVALID_HANDLE; if (data == nullptr) APS5_INVALID_ARG_EX; *data = Pad::ReadState(); @@ -233,6 +233,16 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enab return PAD_OK; } +int APS5_VABI scePadVrControllerGetDeviceInformation() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI scePadVrControllerRead() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + APS5_EXPORT("fCWdlnmB1Ks", scePadUnknown_fCWdlnmB1Ks); int APS5_VABI scePadUnknown_fCWdlnmB1Ks(void) { NotImplemented_nid_no_patch(__func__); diff --git a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp index 104080043..36965cc8e 100644 --- a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp +++ b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp @@ -6,6 +6,11 @@ namespace { +constexpr std::int32_t COMMON_DIALOG_RESULT_USER_CANCELED = 1; +constexpr auto COMMON_DIALOG_ERROR_NOT_INITIALIZED = static_cast(0x80B80003u); +constexpr auto COMMON_DIALOG_ERROR_NOT_FINISHED = static_cast(0x80B80005u); +constexpr auto COMMON_DIALOG_ERROR_ARG_NULL = static_cast(0x80B8000Du); + std::mutex g_dialog_mutex; PlayerInvitationDialogStatus g_dialog_status = PlayerInvitationDialogStatus::None; @@ -67,9 +72,14 @@ std::int32_t APS5_VABI scePlayerInvitationDialogTerminate(void) { return 0; } -std::int32_t APS5_VABI scePlayerInvitationDialogGetResult(void* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); +std::int32_t APS5_VABI scePlayerInvitationDialogGetResult(ScePlayerInvitationDialogResult* result) { + std::lock_guard lock(g_dialog_mutex); + APS5_LOG_OUT("result=%p status=%d", static_cast(result), status()); + if (g_dialog_status == PlayerInvitationDialogStatus::None) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; + if (result == nullptr) return COMMON_DIALOG_ERROR_ARG_NULL; + if (g_dialog_status != PlayerInvitationDialogStatus::Finished) return COMMON_DIALOG_ERROR_NOT_FINISHED; + *result = ScePlayerInvitationDialogResult{}; + result->result = COMMON_DIALOG_RESULT_USER_CANCELED; return 0; } diff --git a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h index a8a4d2724..a52d8dd90 100644 --- a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h +++ b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h @@ -16,6 +16,13 @@ struct ScePlayerInvitationDialogParam { std::uint8_t reserved[1]; }; +struct ScePlayerInvitationDialogResult { + std::int32_t errorCode; + std::int32_t result; + std::uint8_t reserved[32]; +}; +static_assert(sizeof(ScePlayerInvitationDialogResult) == 40); + extern "C" { std::int32_t APS5_VABI scePlayerInvitationDialogInitialize(void); @@ -24,7 +31,7 @@ std::int32_t APS5_VABI scePlayerInvitationDialogUpdateStatus(void); std::int32_t APS5_VABI scePlayerInvitationDialogGetStatus(void); std::int32_t APS5_VABI scePlayerInvitationDialogClose(void); std::int32_t APS5_VABI scePlayerInvitationDialogTerminate(void); -std::int32_t APS5_VABI scePlayerInvitationDialogGetResult(void* result); +std::int32_t APS5_VABI scePlayerInvitationDialogGetResult(ScePlayerInvitationDialogResult* result); } diff --git a/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp new file mode 100644 index 000000000..882e6e92c --- /dev/null +++ b/core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp @@ -0,0 +1,25 @@ +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceProprietaryVoiceChatHelperGetVoiceChatUsageState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceProprietaryVoiceChatHelperInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceProprietaryVoiceChatHelperSetVoiceChatState(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceProprietaryVoiceChatHelperTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libScePsml_debug/Export.cpp b/core/libs/prx/libScePsml_debug/Export.cpp index 2e8ee6e64..dcace7377 100644 --- a/core/libs/prx/libScePsml_debug/Export.cpp +++ b/core/libs/prx/libScePsml_debug/Export.cpp @@ -105,4 +105,58 @@ int APS5_VABI scePsmlUnknown_jEevBXmagOQ() { return 0; } +APS5_EXPORT("0GAw7SmkwII", scePsmlMfsr2Unknown_0GAw7SmkwII); +int APS5_VABI scePsmlMfsr2Unknown_0GAw7SmkwII(void) { + NotImplemented_nid_no_patch("0GAw7SmkwII"); + return 0; +} + +APS5_EXPORT("1ic5q-kdOsc", scePsmlMfsr2Unknown_1ic5q_MkdOsc); +int APS5_VABI scePsmlMfsr2Unknown_1ic5q_MkdOsc(void) { + NotImplemented_nid_no_patch("1ic5q-kdOsc"); + return 0; +} + +APS5_EXPORT("5z2gBlqxJ+0", scePsmlMfsr2Unknown_5z2gBlqxJ_P0); +int APS5_VABI scePsmlMfsr2Unknown_5z2gBlqxJ_P0(void) { + NotImplemented_nid_no_patch("5z2gBlqxJ+0"); + return 0; +} + +APS5_EXPORT("Kyy1baXgaVU", scePsmlMfsr2Unknown_Kyy1baXgaVU); +int APS5_VABI scePsmlMfsr2Unknown_Kyy1baXgaVU(void) { + NotImplemented_nid_no_patch("Kyy1baXgaVU"); + return 0; +} + +APS5_EXPORT("ZLL31lzzxr4", scePsmlMfsr2Unknown_ZLL31lzzxr4); +int APS5_VABI scePsmlMfsr2Unknown_ZLL31lzzxr4(void) { + NotImplemented_nid_no_patch("ZLL31lzzxr4"); + return 0; +} + +APS5_EXPORT("gMduXCLYrNg", scePsmlMfsr2Unknown_gMduXCLYrNg); +int APS5_VABI scePsmlMfsr2Unknown_gMduXCLYrNg(void) { + NotImplemented_nid_no_patch("gMduXCLYrNg"); + return 0; +} + +APS5_EXPORT("lrJwpLjKXRc", scePsmlMfsr2Unknown_lrJwpLjKXRc); +int APS5_VABI scePsmlMfsr2Unknown_lrJwpLjKXRc(void) { + NotImplemented_nid_no_patch("lrJwpLjKXRc"); + return 0; +} + +APS5_EXPORT("m9JLPc3wOQw", scePsmlMfsr2Unknown_m9JLPc3wOQw); +int APS5_VABI scePsmlMfsr2Unknown_m9JLPc3wOQw(void) { + NotImplemented_nid_no_patch("m9JLPc3wOQw"); + return 0; +} + +APS5_EXPORT("o+NM86gEwFE", scePsmlMfsr2Unknown_o_PNM86gEwFE); +int APS5_VABI scePsmlMfsr2Unknown_o_PNM86gEwFE(void) { + NotImplemented_nid_no_patch("o+NM86gEwFE"); + return 0; +} + } diff --git a/core/libs/prx/libSceRazorCpu/Export.cpp b/core/libs/prx/libSceRazorCpu/Export.cpp index 0a5ec3953..90663520e 100644 --- a/core/libs/prx/libSceRazorCpu/Export.cpp +++ b/core/libs/prx/libSceRazorCpu/Export.cpp @@ -35,4 +35,9 @@ int APS5_VABI sceRazorCpuPopMarker(void) { return 0; } +int APS5_VABI sceRazorCpuFlushOccurred(uint64_t* timeSpentInFlush) { + if (timeSpentInFlush != nullptr) *timeSpentInFlush = 0; + return 0; +} + } diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 08bf4bb06..a8986f228 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include "SceTypes.hpp" @@ -107,6 +108,13 @@ std::int64_t localOffsetSeconds(std::uint64_t utcTick) { return localSeconds - static_cast(seconds); } +constexpr const char* WEEKDAY_NAMES[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +constexpr const char* MONTH_NAMES[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + +int localOffsetMinutes(const RtcTick& utc) { + return static_cast(localOffsetSeconds(utc.tick) / 60); +} + int addTicks(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t unit) { if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; if (src->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; @@ -158,6 +166,97 @@ bool parseZoneSuffix(const char*& cursor, std::int64_t& offsetMinutes) { return false; } +bool parseName(const char*& cursor, const char* const* names, int count, int& index) { + for (index = 0; index < count; ++index) { + if (std::strncmp(cursor, names[index], 3) == 0) { + cursor += 3; + return true; + } + } + return false; +} + +bool parseWeekday(const char*& cursor) { + static constexpr const char* names[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + int index = 0; + return parseName(cursor, names, 7, index); +} + +bool parseMonth(const char*& cursor, int& month) { + static constexpr const char* names[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + if (!parseName(cursor, names, 12, month)) return false; + ++month; + return true; +} + +bool parseClock(const char*& cursor, int& hour, int& minute, int& second) { + return parseDigits(cursor, 2, hour) && *cursor++ == ':' && parseDigits(cursor, 2, minute) && *cursor++ == ':' && parseDigits(cursor, 2, second); +} + +RtcDateTime makeDateTime(int year, int month, int day, int hour, int minute, int second, std::uint32_t microsecond) { + return RtcDateTime{static_cast(year), static_cast(month), static_cast(day), + static_cast(hour), static_cast(minute), static_cast(second), microsecond}; +} + +bool parseIsoDateTime(const char* cursor, RtcDateTime& time, std::int64_t& offsetMinutes) { + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!parseDigits(cursor, 4, year) || *cursor++ != '-' || !parseDigits(cursor, 2, month) || *cursor++ != '-' || !parseDigits(cursor, 2, day)) return false; + if (*cursor != 'T' && *cursor != 't' && *cursor != ' ') return false; + ++cursor; + if (!parseClock(cursor, hour, minute, second)) return false; + std::uint32_t microsecond = 0; + if (*cursor == '.') { + ++cursor; + std::uint32_t scale = 100000; + if (*cursor < '0' || *cursor > '9') return false; + for (; *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; + } + offsetMinutes = 0; + if (*cursor != 0 && !parseZoneSuffix(cursor, offsetMinutes)) return false; + if (*cursor != 0) return false; + time = makeDateTime(year, month, day, hour, minute, second, microsecond); + return true; +} + +bool parseRfc2822DateTime(const char* cursor, RtcDateTime& time, std::int64_t& offsetMinutes) { + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!parseWeekday(cursor) || *cursor++ != ',' || *cursor++ != ' ' || !parseDigits(cursor, 2, day) || *cursor++ != ' ') return false; + if (!parseMonth(cursor, month) || *cursor++ != ' ' || !parseDigits(cursor, 4, year) || *cursor++ != ' ' || !parseClock(cursor, hour, minute, second)) return false; + offsetMinutes = 0; + if (*cursor == ' ') { + ++cursor; + if (std::strcmp(cursor, "GMT") == 0) { + cursor += 3; + } else if (*cursor == '+' || *cursor == '-') { + const int sign = *cursor++ == '-' ? -1 : 1; + int offsetHours = 0, offsetMinute = 0; + if (!parseDigits(cursor, 2, offsetHours) || !parseDigits(cursor, 2, offsetMinute)) return false; + if (offsetHours > 23 || offsetMinute > 59) return false; + offsetMinutes = sign * (offsetHours * 60 + offsetMinute); + } else { + return false; + } + } + if (*cursor != 0) return false; + time = makeDateTime(year, month, day, hour, minute, second, 0); + return true; +} + +bool parseAsctimeDateTime(const char* cursor, RtcDateTime& time, std::int64_t& offsetMinutes) { + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!parseWeekday(cursor) || *cursor++ != ' ' || !parseMonth(cursor, month) || *cursor++ != ' ') return false; + if (*cursor == ' ') { + ++cursor; + if (!parseDigits(cursor, 1, day)) return false; + } else if (!parseDigits(cursor, 2, day)) { + return false; + } + if (*cursor++ != ' ' || !parseClock(cursor, hour, minute, second) || *cursor++ != ' ' || !parseDigits(cursor, 4, year) || *cursor != 0) return false; + offsetMinutes = 0; + time = makeDateTime(year, month, day, hour, minute, second, 0); + return true; +} + } extern "C" { @@ -270,6 +369,31 @@ int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32_time) { return 0; } +int APS5_VABI sceRtcGetDosTime(const RtcDateTime* time, uint32_t* dos_time) { + if (!dos_time) return SCE_RTC_ERROR_INVALID_POINTER; + if (const int result = validate(time); result != 0) return result; + if (time->year < 1980 || time->year > 2107) { + NotImplemented_nid_no_patch(__func__); + return SCE_RTC_ERROR_INVALID_YEAR; + } + *dos_time = static_cast(time->year - 1980) << 25 | static_cast(time->month) << 21 + | static_cast(time->day) << 16 | static_cast(time->hour) << 11 + | static_cast(time->minute) << 5 | static_cast(time->second / 2); + return 0; +} + +int APS5_VABI sceRtcSetDosTime(RtcDateTime* time, uint32_t dos_time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + time->year = static_cast(1980 + (dos_time >> 25)); + time->month = static_cast(dos_time >> 21 & 0x0f); + time->day = static_cast(dos_time >> 16 & 0x1f); + time->hour = static_cast(dos_time >> 11 & 0x1f); + time->minute = static_cast(dos_time >> 5 & 0x3f); + time->second = static_cast((dos_time & 0x1f) * 2); + time->microsecond = 0; + return 0; +} + int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; @@ -286,6 +410,28 @@ int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_ return 0; } +int APS5_VABI sceRtcFormatRFC3339LocalTime(char* date_time, const RtcTick* utc) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + return sceRtcFormatRFC3339(date_time, utc, localOffsetMinutes(*utc)); +} + +int APS5_VABI sceRtcFormatRFC2822(char* date_time, const RtcTick* utc, int time_zone_minutes) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + if (time_zone_minutes < -1439 || time_zone_minutes > 1439) return SCE_RTC_ERROR_INVALID_VALUE; + RtcTick local{}; + if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcDateTime time = fromTick(local.tick); + const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; + std::snprintf(date_time, 32, "%s, %02u %s %04u %02u:%02u:%02u %c%02d%02d", WEEKDAY_NAMES[(local.tick / TICKS_PER_DAY + 1) % 7], time.day, + MONTH_NAMES[time.month - 1], time.year, time.hour, time.minute, time.second, time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + return 0; +} + +int APS5_VABI sceRtcFormatRFC2822LocalTime(char* date_time, const RtcTick* utc) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + return sceRtcFormatRFC2822(date_time, utc, localOffsetMinutes(*utc)); +} + int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; const char* cursor = date_time; @@ -313,28 +459,13 @@ int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { int APS5_VABI sceRtcParseDateTime(RtcTick* utc, const char* date_time) { if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; - const char* cursor = date_time; - int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; - bool ok = parseDigits(cursor, 4, year) && *cursor++ == '-' && parseDigits(cursor, 2, month) && *cursor++ == '-' && parseDigits(cursor, 2, day); - ok = ok && (*cursor == 'T' || *cursor == 't' || *cursor == ' '); - if (ok) ++cursor; - ok = ok && parseDigits(cursor, 2, hour) && *cursor++ == ':' && parseDigits(cursor, 2, minute) && *cursor++ == ':' && parseDigits(cursor, 2, second); - std::uint32_t microsecond = 0; - if (ok && *cursor == '.') { - ++cursor; - std::uint32_t scale = 100000; - ok = *cursor >= '0' && *cursor <= '9'; - for (; ok && *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; - } + RtcDateTime time{}; std::int64_t offsetMinutes = 0; - if (ok && *cursor != 0) ok = parseZoneSuffix(cursor, offsetMinutes); - if (ok && *cursor != 0) ok = false; - if (!ok) { + if (!parseIsoDateTime(date_time, time, offsetMinutes) && !parseRfc2822DateTime(date_time, time, offsetMinutes) + && !parseAsctimeDateTime(date_time, time, offsetMinutes)) { NotImplemented_nid_no_patch(__func__); return SCE_RTC_ERROR_BAD_PARSE; } - const RtcDateTime time{static_cast(year), static_cast(month), static_cast(day), - static_cast(hour), static_cast(minute), static_cast(second), microsecond}; if (const int result = validate(&time); result != 0) return result; const RtcTick local{toTick(time)}; return addTicks(utc, &local, -offsetMinutes, TICKS_PER_MINUTE); diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index f0b798f5d..785ec69ca 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -24,7 +24,7 @@ static constexpr char SAVE_DIR[] = "_sd"; static std::atomic g_transaction_counter{1}; -static bool g_initialized = false; +static std::atomic g_initializations{0}; static std::string save_root() { return std::string(SAVE_DIR); @@ -306,6 +306,7 @@ static int getMountInfo(const SaveDataMountPoint* mount_point, SaveDataMountInfo if (mount_point == nullptr || info == nullptr) { throw std::runtime_error("sceSaveDataGetMountInfo: null argument"); } + std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } @@ -325,6 +326,7 @@ static int getParam(const SaveDataMountPoint* mount_point, uint32_t param_type, if (mount_point == nullptr || param_buf == nullptr) { throw std::runtime_error("sceSaveDataGetParam: null argument"); } + std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; @@ -351,7 +353,7 @@ static int getSaveDataMemory2(SaveDataMemoryGet2* get_param) { if (get_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - if (!g_initialized) { + if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } std::lock_guard lk(g_mem_mutex); @@ -393,10 +395,7 @@ int APS5_VABI sceSaveDataGetSaveDataMemory2(SaveDataMemoryGet2* get_param) { int APS5_VABI sceSaveDataInitialize3(const void* init) { (void)init; - if (g_initialized) { - return SAVE_DATA_ERROR_ALREADY_INITIALIZED; - } - g_initialized = true; + ++g_initializations; return SAVE_DATA_OK; } @@ -405,6 +404,7 @@ int APS5_VABI sceSaveDataLoadIcon(const SaveDataMountPoint* mount_point, SaveDat if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataLoadIcon: null mount_point"); } + std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } @@ -436,6 +436,7 @@ static int mount3(const SaveDataMount3* mount, SaveDataMountResult* mount_result throw std::runtime_error("sceSaveDataMount3: invalid directory name"); } const std::string real_path = save_root() + "/" + dirName; + std::lock_guard lock(g_slots_mutex); for (const auto& used : g_slots) { if (used.used && used.real_path == real_path) { return SAVE_DATA_ERROR_BUSY; @@ -485,6 +486,7 @@ int APS5_VABI sceSaveDataSaveIcon(const SaveDataMountPoint* mount_point, const S if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataSaveIcon: null mount_point"); } + std::lock_guard lock(g_slots_mutex); if (find_slot_by_mount_point(mount_point->data) == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; } @@ -501,6 +503,7 @@ static int setParam(const SaveDataMountPoint* mount_point, uint32_t param_type, if (mount_point == nullptr || param_buf == nullptr) { throw std::runtime_error("sceSaveDataSetParam: null argument"); } + std::lock_guard lock(g_slots_mutex); const int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; @@ -536,7 +539,7 @@ static int setSaveDataMemory2(const SaveDataMemorySet2* set_param) { if (set_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - if (!g_initialized) { + if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } std::lock_guard lk(g_mem_mutex); @@ -594,7 +597,7 @@ static int setupSaveDataMemory2(const SaveDataMemorySetup2* setup_param, SaveDat if (setup_param == nullptr) { return SAVE_DATA_ERROR_PARAMETER; } - if (!g_initialized) { + if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } if (setup_param->memory_size == 0 || setup_param->memory_size > MEM_MAX_SIZE) { @@ -654,13 +657,14 @@ int APS5_VABI sceSaveDataSyncSaveDataMemory(const void* sync_param) { } int APS5_VABI sceSaveDataTerminate(void) { - if (!g_initialized) { + std::lock_guard lock(g_slots_mutex); + if (g_initializations == 0) { return SAVE_DATA_ERROR_NOT_INITIALIZED; } - if (any_slot_used()) { + if (g_initializations == 1 && any_slot_used()) { return SAVE_DATA_ERROR_BUSY; } - g_initialized = false; + --g_initializations; return SAVE_DATA_OK; } @@ -684,6 +688,7 @@ static int umount2(uint32_t mode, const SaveDataMountPoint* mount_point) { if (mount_point == nullptr) { throw std::runtime_error("sceSaveDataUmount2: null mount_point"); } + std::lock_guard lock(g_slots_mutex); int slot = find_slot_by_mount_point(mount_point->data); if (slot == -1) { return SAVE_DATA_ERROR_NOT_MOUNTED; diff --git a/core/libs/prx/libSceSaveData.native/SaveData.hpp b/core/libs/prx/libSceSaveData.native/SaveData.hpp index 15ecf354d..20d02ad5a 100644 --- a/core/libs/prx/libSceSaveData.native/SaveData.hpp +++ b/core/libs/prx/libSceSaveData.native/SaveData.hpp @@ -3,6 +3,7 @@ #include #include +#include #include constexpr int SAVE_DATA_OK = 0; @@ -41,6 +42,7 @@ struct MountSlot { }; inline std::array g_slots; +inline std::mutex g_slots_mutex; inline int find_slot_by_mount_point(const char* mount_point) { for (int i = 0; i < static_cast(SAVE_DATA_MOUNT_SLOTS); i++) { diff --git a/core/libs/prx/libSceShare/Export.cpp b/core/libs/prx/libSceShare/Export.cpp index eb65ee977..afd98addc 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -104,4 +104,20 @@ int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, int3 return ERROR_NOT_SUPPORTED; } + +int APS5_VABI sceShareCaptureVideoClipExtended(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareGetRunningStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceShareSetContentParamForApplicationTitle(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceSystemService/Export.cpp b/core/libs/prx/libSceSystemService/Export.cpp index 19358d5c1..0de0c37f4 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -136,8 +136,7 @@ int APS5_VABI sceSystemServiceLaunchPlayerDialog(const void* param) { } int APS5_VABI sceSystemServiceDisableMusicPlayer(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { @@ -151,8 +150,7 @@ int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { } int APS5_VABI sceSystemServiceReenableMusicPlayer(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceShowControllerSettings(void) { diff --git a/core/libs/prx/libSceNgs2/Export.cpp b/core/libs/prx/libSceTextToSpeech2/Export.cpp similarity index 52% rename from core/libs/prx/libSceNgs2/Export.cpp rename to core/libs/prx/libSceTextToSpeech2/Export.cpp index fb34206c2..2b94ca2f8 100644 --- a/core/libs/prx/libSceNgs2/Export.cpp +++ b/core/libs/prx/libSceTextToSpeech2/Export.cpp @@ -5,6 +5,9 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceTextToSpeech2GetSystemStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceUserService/CMakeLists.txt b/core/libs/prx/libSceUserService/CMakeLists.txt new file mode 100644 index 000000000..8eafc99ab --- /dev/null +++ b/core/libs/prx/libSceUserService/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceUserService SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceUserService PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceUserService) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_user_service_tests ${LIBS_INCLUDE_DIR}/tests/GuestUserService.cpp) + target_include_directories(guest_user_service_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_user_service_tests PRIVATE libSceUserService) + add_test(NAME guest_user_service COMMAND guest_user_service_tests) + if(WIN32) + set_tests_properties(guest_user_service PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 9ae783818..2a54f7c9f 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -156,4 +156,39 @@ int APS5_VABI sceUserServiceGetPlatformPrivacyWs1(int32_t user_id, int32_t* valu return 0; } +int APS5_VABI sceUserServiceGetForegroundUser(int* user_id) { + if (user_id == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *user_id = USER_SERVICE_INITIAL_USER_ID; + return USER_SERVICE_OK; +} + +int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list) { + if (user_id_list == nullptr) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + user_id_list->user_id[0] = USER_SERVICE_INITIAL_USER_ID; + for (int i = 1; i < 16; ++i) { + user_id_list->user_id[i] = USER_SERVICE_USER_ID_INVALID; + } + return USER_SERVICE_OK; +} + +int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color) { + if (color == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *color = 0; + return USER_SERVICE_OK; +} + +int APS5_VABI sceUserServiceGetNpAccountId(int user_id, uint64_t* account_id) { + if (account_id == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *account_id = 0; + return USER_SERVICE_OK; +} + } diff --git a/core/libs/prx/libSceVdecsw/Export.cpp b/core/libs/prx/libSceVdecsw/Export.cpp new file mode 100644 index 000000000..a0391dc7c --- /dev/null +++ b/core/libs/prx/libSceVdecsw/Export.cpp @@ -0,0 +1,73 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVdecswCreateDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswDeleteDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswQueryComputeMemoryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswFinalizeDecodeSequence() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswQueryDecoderMemoryInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswSetDecodeInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswReleaseComputeQueue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswAllocateComputeQueue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswGetAvcPictureInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswTrySyncDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswTrySyncDecodeInput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswSetDecodeOutput() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVdecswResetDecoder() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp index 6717efe30..1b60c53ec 100644 --- a/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp +++ b/core/libs/prx/libSceVideoOut/src/DisplayWindow.cpp @@ -1,5 +1,6 @@ #include "prx/libSceVideoOut/include/DisplayWindow.hpp" #include "prx/libSceAgcDriver/Execution/include/AspectFit.hpp" +#include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "prx/libkernel/Time/include/Time.hpp" #include "SDL_vulkan.h" @@ -45,7 +46,9 @@ void DisplayWindow::create(std::uint32_t sourceWidth, std::uint32_t sourceHeight const auto initialSize = AgcDriver::ComputeContainSize_nid_postfix(sourceWidth, sourceHeight, boundsWidth, boundsHeight, true); require(initialSize.width >= DisplayWindowMinimumWidth && initialSize.height >= DisplayWindowMinimumHeight, "initial window extent is smaller than the minimum"); const auto title = GetAppTitle_nid_postfix(); + AgcDriverLockVulkanLoader_nid_postfix(); window = SDL_CreateWindow(title.value, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, static_cast(initialSize.width), static_cast(initialSize.height), SDL_WINDOW_SHOWN | SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI); + AgcDriverUnlockVulkanLoader_nid_postfix(); require(window != nullptr, SDL_GetError()); SDL_SetWindowMinimumSize(window, static_cast(DisplayWindowMinimumWidth), static_cast(DisplayWindowMinimumHeight)); installSubclass(); diff --git a/core/libs/prx/libSceVideoOut/src/Output.cpp b/core/libs/prx/libSceVideoOut/src/Output.cpp index cf471691d..af435d9a3 100644 --- a/core/libs/prx/libSceVideoOut/src/Output.cpp +++ b/core/libs/prx/libSceVideoOut/src/Output.cpp @@ -25,9 +25,6 @@ static int validateOutputConfig(int handle, uint64_t mode, const VideoOutOutputO } } } - if (mode != VIDEO_OUT_OUTPUT_MODE_DEFAULT && mode != VIDEO_OUT_OUTPUT_MODE_119_88HZ) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE"); - } return 0; } @@ -199,7 +196,7 @@ int APS5_VABI sceVideoOutIsOutputSupported(int handle, uint64_t mode, const Vide if (result != 0) { return result; } - return (mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) ? 0 : 1; + return mode == VIDEO_OUT_OUTPUT_MODE_DEFAULT ? 1 : 0; } catch (const ProcessShutdown&) { LibcAwaitExit_nid_postfix(); } @@ -209,8 +206,8 @@ int APS5_VABI sceVideoOutConfigureOutput(int handle, uint64_t mode, const VideoO if (supported < 0) { return supported; } - if (supported == 0 && mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ) { - throw std::runtime_error(std::string(__func__) + ": VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE"); + if (supported == 0) { + return VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE; } auto cfg = VideoOutDriver::Get().GetConfig(handle); if (cfg == nullptr) { @@ -279,4 +276,34 @@ int APS5_VABI sceVideoOutAdjustColor(int handle, const VideoOutColorSettings* se LibcAwaitExit_nid_postfix(); } +int APS5_VABI sceVideoOutVrrUnpegFromFixedRate() try { + NotImplemented_nid_no_patch(__func__); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +int APS5_VABI sceVideoOutVrrPegToFixedRate() try { + NotImplemented_nid_no_patch(__func__); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +APS5_EXPORT("kP2L8t3j-aM", sceVideoOutUnknown00); +int APS5_VABI sceVideoOutUnknown00() try { + NotImplemented_nid_no_patch(__func__); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + +APS5_EXPORT("LibwuIonIBw", sceVideoOutUnknown01); +int APS5_VABI sceVideoOutUnknown01() try { + NotImplemented_nid_no_patch(__func__); + return 0; +} catch (const ProcessShutdown&) { + LibcAwaitExit_nid_postfix(); +} + } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index c0835a26e..048a7e892 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -229,6 +230,12 @@ void testControls() { const auto handle = sceVideoOutOpen(255, 0, 0, openParam.data()); check(handle >= 0, "open with the highest priority on every CPU failed"); const auto cfg = VideoOutDriver::Get().GetConfig(handle); + check(sceVideoOutIsOutputSupported(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 1, "default output mode is unsupported"); + for (const uint64_t unsupported : std::initializer_list{VIDEO_OUT_OUTPUT_MODE_119_88HZ, 0xd000000aull}) { + check(sceVideoOutIsOutputSupported(handle, unsupported, nullptr, nullptr, 0) == 0, "unavailable output mode is supported"); + check(sceVideoOutConfigureOutput(handle, unsupported, nullptr, nullptr, 0) == VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE, "unavailable output mode was configured"); + } + check(sceVideoOutConfigureOutput(handle, VIDEO_OUT_OUTPUT_MODE_DEFAULT, nullptr, nullptr, 0) == 0, "default output mode was rejected"); for (int rate = 0; rate <= 2; ++rate) { check(sceVideoOutSetFlipRate(handle, rate) == 0 && cfg->flipRate == rate, "flip rate was not applied"); } diff --git a/core/libs/prx/libSceVideoRecordingP/Export.cpp b/core/libs/prx/libSceVideoRecordingP/Export.cpp index 9400810ba..7d1cb61a2 100644 --- a/core/libs/prx/libSceVideoRecordingP/Export.cpp +++ b/core/libs/prx/libSceVideoRecordingP/Export.cpp @@ -37,10 +37,11 @@ int APS5_VABI sceVideoRecordingGetStatus(void) { return 0; } -int APS5_VABI sceVideoRecordingOpen(void* buffer, size_t size, const void* param) { - (void)buffer; - (void)size; +int APS5_VABI sceVideoRecordingOpen(const char* path, const void* param, void* heap, int heapSize) { + (void)path; (void)param; + (void)heap; + (void)heapSize; NotImplemented_nid_no_patch(__func__); return 0; } diff --git a/core/libs/prx/libSceVideodec2/Export.cpp b/core/libs/prx/libSceVideodec2/Export.cpp index 055a5d509..28bea7762 100644 --- a/core/libs/prx/libSceVideodec2/Export.cpp +++ b/core/libs/prx/libSceVideodec2/Export.cpp @@ -428,4 +428,8 @@ int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo* output, A return 0; } +int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo* output, AvcPictureInfo* first, AvcPictureInfo* second) { + return sceVideodec2GetPictureInfo_nid_postfix(output, first, second); +} + } diff --git a/core/libs/prx/libSceVoice/Export.cpp b/core/libs/prx/libSceVoice/Export.cpp index 0f824d139..898511c88 100644 --- a/core/libs/prx/libSceVoice/Export.cpp +++ b/core/libs/prx/libSceVoice/Export.cpp @@ -209,4 +209,24 @@ int APS5_VABI sceVoiceSetMuteFlag() { return 0; } +int APS5_VABI sceVoiceGetResourceInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceEnableChat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceResetPort(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVoiceDisableChat(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 9cc97fe93..0aa4ef2cb 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -82,4 +82,9 @@ int APS5_VABI sceVoiceQoSWritePacket() { return 0; } +int APS5_VABI sceVoiceQoSGetLocalEndpointAttribute() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceVrSetupDialog/Export.cpp b/core/libs/prx/libSceVrSetupDialog/Export.cpp new file mode 100644 index 000000000..5c9a5da6a --- /dev/null +++ b/core/libs/prx/libSceVrSetupDialog/Export.cpp @@ -0,0 +1,38 @@ +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceVrSetupDialogClose(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogGetResult(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogInitialize(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogOpen(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogTerminate(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceVrSetupDialogUpdateStatus(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt new file mode 100644 index 000000000..b73af180a --- /dev/null +++ b/core/libs/prx/libSceWebBrowserDialog/CMakeLists.txt @@ -0,0 +1,20 @@ +add_library(libSceWebBrowserDialog SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceWebBrowserDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceWebBrowserDialog) + +if(MINGW) + foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) + string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") + endforeach() +endif() + +if(BUILD_TESTING) + add_test_executable(guest_web_browser_dialog_tests ${LIBS_INCLUDE_DIR}/tests/GuestWebBrowserDialog.cpp) + target_include_directories(guest_web_browser_dialog_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_web_browser_dialog_tests PRIVATE libSceWebBrowserDialog) + add_test(NAME guest_web_browser_dialog COMMAND guest_web_browser_dialog_tests) + if(WIN32) + set_tests_properties(guest_web_browser_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceWebBrowserDialog/Export.cpp b/core/libs/prx/libSceWebBrowserDialog/Export.cpp index 879608486..dd35e045a 100644 --- a/core/libs/prx/libSceWebBrowserDialog/Export.cpp +++ b/core/libs/prx/libSceWebBrowserDialog/Export.cpp @@ -46,6 +46,10 @@ int APS5_VABI sceWebBrowserDialogGetResult(void* result) { return 0; } +int APS5_VABI sceWebBrowserDialogGetStatus(void) { + return g_status.load(); +} + int APS5_VABI sceWebBrowserDialogOpen(const void* param) { const int status = g_status.load(); if (status == COMMON_DIALOG_STATUS_NONE) return COMMON_DIALOG_ERROR_NOT_INITIALIZED; @@ -59,4 +63,20 @@ int APS5_VABI sceWebBrowserDialogUpdateStatus(void) { return g_status.load(); } + +int APS5_VABI sceWebBrowserDialogSetCookie(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogOpenForPredeterminedContent() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceWebBrowserDialogResetCookie() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 575971f71..c59665ab8 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -45,6 +45,15 @@ if(WIN32) endif() if(BUILD_TESTING) + add_test_executable(guest_scan_count_tests ${LIBS_INCLUDE_DIR}/tests/GuestScanCount.cpp) + target_include_directories(guest_scan_count_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_scan_count_tests PRIVATE libc) + configure_windows_unwind(guest_scan_count_tests) + add_test(NAME guest_scan_count COMMAND guest_scan_count_tests) + if(WIN32) + set_tests_properties(guest_scan_count PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_access_tests ${LIBS_INCLUDE_DIR}/tests/GuestAccess.cpp) target_include_directories(guest_access_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_access_tests PRIVATE libc) @@ -101,6 +110,16 @@ if(BUILD_TESTING) set_tests_properties(guest_execute_once PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_std_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestStdExecuteOnce.cpp) + target_include_directories(guest_std_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_std_execute_once_tests PRIVATE libc) + configure_windows_unwind(guest_std_execute_once_tests) + add_test(NAME guest_std_execute_once COMMAND guest_std_execute_once_tests) + set_tests_properties(guest_std_execute_once PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_std_execute_once PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_fiopen_tests ${LIBS_INCLUDE_DIR}/tests/GuestFiopen.cpp) target_include_directories(guest_fiopen_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_fiopen_tests PRIVATE libc) @@ -232,6 +251,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_collate_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestCollate.cpp) +target_include_directories(guest_collate_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_collate_tests PRIVATE libc) +configure_windows_unwind(guest_collate_tests) +set_target_properties(guest_collate_tests PROPERTIES CXX_EXTENSIONS OFF) +if(BUILD_TESTING) + set_target_properties(guest_collate_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_collate COMMAND guest_collate_tests) + if(WIN32) + set_tests_properties(guest_collate PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(application_heap_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/ApplicationHeap.cpp) target_include_directories(application_heap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(application_heap_tests PRIVATE libc) @@ -242,8 +274,9 @@ if(BUILD_TESTING) add_test(NAME application_heap_replacement COMMAND application_heap_tests) add_test(NAME application_heap_invalid_metadata COMMAND application_heap_tests invalid) add_test(NAME application_heap_default COMMAND application_heap_tests default) + add_test(NAME application_heap_exit_order COMMAND application_heap_tests exit-order) if(WIN32) - set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default + set_tests_properties(application_heap_replacement application_heap_invalid_metadata application_heap_default application_heap_exit_order PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() @@ -253,6 +286,13 @@ target_include_directories(guest_locale_tests PRIVATE ${LIBS_INCLUDE_DIR}) set_target_properties(guest_locale_tests PROPERTIES CXX_EXTENSIONS OFF) target_link_libraries(guest_locale_tests PRIVATE libc) configure_windows_unwind(guest_locale_tests) +if(BUILD_TESTING) + set_target_properties(guest_locale_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_locale COMMAND guest_locale_tests) + if(WIN32) + set_tests_properties(guest_locale PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index bdb6ca001..ebbb25bea 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -8,12 +8,10 @@ uint32_t Need_sceLibc = 1; extern "C" { - int APS5_VABI std_execute_once_nid_postfix(int* flag, int (*func)(void*, void*, void**), void* arg) { - (void)flag; - (void)func; - (void)arg; - NotImplemented_nid_no_patch(__func__); - return 0; + int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(int*, int (APS5_VABI *)(void*, void*, void**), void*); + + int APS5_VABI std_execute_once_nid_postfix(int* flag, int (APS5_VABI *func)(void*, void*, void**), void* arg) { + return _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(flag, func, arg); } void APS5_VABI LibcHeapGetTraceInfo_nid_postfix(LibcHeapInfo* info) { diff --git a/core/libs/prx/libc/include/GuestAllocations.hpp b/core/libs/prx/libc/include/GuestAllocations.hpp index a292f5d6e..81c711b9e 100644 --- a/core/libs/prx/libc/include/GuestAllocations.hpp +++ b/core/libs/prx/libc/include/GuestAllocations.hpp @@ -30,6 +30,7 @@ void GuestAllocationsAdd_nid_postfix(void* mutation, void* pointer, std::size_t void GuestAllocationsRequireUnpinned_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); void GuestAllocationsRequireAvailable_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); bool GuestAllocationsCovers_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); +bool GuestAllocationsOverlaps_nid_postfix(void* mutation, const void* pointer, std::size_t bytes); Range GuestAllocationsFind_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsRemove_nid_postfix(void* mutation, const void* pointer); void GuestAllocationsProtect_nid_postfix(void* mutation, const void* pointer, std::size_t bytes, bool readable, bool writable, const std::function& apply); @@ -60,6 +61,7 @@ public: void RequireUnpinned(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireUnpinned_nid_postfix(handle, pointer, bytes); } void RequireAvailable(const void* pointer, std::size_t bytes) const { GuestAllocationsRequireAvailable_nid_postfix(handle, pointer, bytes); } bool Covers(const void* pointer, std::size_t bytes) const { return GuestAllocationsCovers_nid_postfix(handle, pointer, bytes); } + bool Overlaps(const void* pointer, std::size_t bytes) const { return GuestAllocationsOverlaps_nid_postfix(handle, pointer, bytes); } Range Find(const void* pointer) const { return GuestAllocationsFind_nid_postfix(handle, pointer); } void Remove(const void* pointer) { GuestAllocationsRemove_nid_postfix(handle, pointer); } void Unmap(const void* pointer, std::size_t bytes, const std::function& apply) { GuestAllocationsUnmap_nid_postfix(handle, pointer, bytes, apply); } diff --git a/core/libs/prx/libc/include/GuestLocale.hpp b/core/libs/prx/libc/include/GuestLocale.hpp index 1e46c9210..45e29df3f 100644 --- a/core/libs/prx/libc/include/GuestLocale.hpp +++ b/core/libs/prx/libc/include/GuestLocale.hpp @@ -35,6 +35,29 @@ struct Implementation { const char* name; }; +struct String { + std::uint64_t reserved; + union { + char buffer[16]; + char* pointer; + }; + std::uint64_t size; + std::uint64_t capacity; +}; + +struct CollateFacet { + Facet base; + const void* collation; + const void* wideCollation; +}; + +struct CollateVtable { + FacetVtable facet; + int (APS5_VABI *compare)(const CollateFacet* self, const char* first1, const char* last1, const char* first2, const char* last2); + String* (APS5_VABI *transform)(String* result, const CollateFacet* self, const char* first, const char* last); + std::int64_t (APS5_VABI *hash)(const CollateFacet* self, const char* first, const char* last); +}; + struct IosBase { const void* vtable; std::int64_t standardStream; @@ -59,6 +82,13 @@ static_assert(offsetof(IosBase, storage) == 0x28); static_assert(offsetof(IosBase, callbacks) == 0x30); static_assert(offsetof(IosBase, locale) == 0x38); static_assert(sizeof(IosBase) == 0x40); +static_assert(offsetof(String, buffer) == 0x8); +static_assert(offsetof(String, size) == 0x18); +static_assert(sizeof(String) == 0x28); +static_assert(offsetof(CollateFacet, collation) == 0x10); +static_assert(sizeof(CollateFacet) == 0x20); +static_assert(offsetof(CollateVtable, compare) == 0x20); +static_assert(offsetof(CollateVtable, hash) == 0x30); static_assert(alignof(LocinfoStorage) == 8); } diff --git a/core/libs/prx/libc/include/WindowsFormatting.hpp b/core/libs/prx/libc/include/WindowsFormatting.hpp index 19d02b612..43c99d111 100644 --- a/core/libs/prx/libc/include/WindowsFormatting.hpp +++ b/core/libs/prx/libc/include/WindowsFormatting.hpp @@ -146,15 +146,25 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo } else { while (*format >= '0' && *format <= '9') spec += *format++; } + size_t precisionLimit = std::numeric_limits::max(); if (*format == '.') { ++format; if (*format == '*') { ++format; const int precision = args.Next(); - if (precision >= 0) spec += "." + std::to_string(precision); + if (precision >= 0) { + spec += "." + std::to_string(precision); + precisionLimit = static_cast(precision); + } } else { spec += '.'; - while (*format >= '0' && *format <= '9') spec += *format++; + precisionLimit = 0; + while (*format >= '0' && *format <= '9') { + const auto digit = static_cast(*format - '0'); + precisionLimit = precisionLimit <= (std::numeric_limits::max() - digit) / 10 + ? precisionLimit * 10 + digit : std::numeric_limits::max(); + spec += *format++; + } } } std::string length; @@ -209,10 +219,15 @@ inline int FormatWindows(char* buffer, size_t size, const char* format, const vo const char16_t* value = args.Next(); if (!value) value = u"(null)"; std::string utf8; - while (*value) { + while (utf8.size() < precisionLimit && *value) { char32_t code = *value++; if (code >= 0xd800 && code < 0xdc00 && *value >= 0xdc00 && *value < 0xe000) code = 0x10000 + ((code - 0xd800) << 10) + (*value++ - 0xdc00); + const auto previousSize = utf8.size(); AppendUtf8(utf8, code); + if (utf8.size() > precisionLimit) { + utf8.resize(previousSize); + break; + } } output.Value(spec + 's', utf8.c_str()); } else if (conversion == 'p' && length.empty()) { diff --git a/core/libs/prx/libc/include/WindowsScanning.hpp b/core/libs/prx/libc/include/WindowsScanning.hpp index ff7701281..e4dd53c00 100644 --- a/core/libs/prx/libc/include/WindowsScanning.hpp +++ b/core/libs/prx/libc/include/WindowsScanning.hpp @@ -7,8 +7,9 @@ namespace LibcDetail { -inline int ScanWindows(const char* input, const char* format, const void* source) { - if (!input || !format || !source) { errno = 22; return EOF; } +template +inline int ScanWindowsArguments_nid_no_patch(const char* format, const void* source, TScanner scan) { + if (!format || !source) { errno = 22; return EOF; } std::string translated; std::vector pointers; FormatArguments args(source); @@ -45,7 +46,21 @@ inline int ScanWindows(const char* input, const char* format, const void* source } if (!suppressed) pointers.push_back(args.Next()); } - return std::vsscanf(input, translated.c_str(), reinterpret_cast(pointers.data())); + return scan(translated.c_str(), reinterpret_cast(pointers.data())); +} + +inline int ScanWindows(const char* input, const char* format, const void* source) { + if (!input) { errno = 22; return EOF; } + return ScanWindowsArguments_nid_no_patch(format, source, [input](const char* translated, char* pointers) { + return std::vsscanf(input, translated, pointers); + }); +} + +inline int ScanFileWindows_nid_no_patch(std::FILE* stream, const char* format, const void* source) { + if (!stream) { errno = 22; return EOF; } + return ScanWindowsArguments_nid_no_patch(format, source, [stream](const char* translated, char* pointers) { + return std::vfscanf(stream, translated, pointers); + }); } } diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index 3cd055106..cc12f6571 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -95,11 +95,10 @@ void requireAlignment(std::size_t alignment) { } void finalize() { - Initialize finalizeCallback; + Initialize finalizeCallback = nullptr; { std::lock_guard lock(heapMutex); - if (heapFailure) std::rethrow_exception(heapFailure); - if (heapFinalized) throw std::runtime_error("application heap: duplicate finalization"); + if (heapFinalized || heapFailure) return; finalizeCallback = heapFinalize; } if (finalizeCallback != nullptr) finalizeCallback(); @@ -107,6 +106,13 @@ void finalize() { heapFinalized = true; } +struct HeapFinalizerRegistration { + HeapFinalizerRegistration() { + if (std::atexit(finalize) != 0) std::terminate(); + } +}; +const HeapFinalizerRegistration heapFinalizerRegistration; + } void ApplicationHeapRegister_nid_no_patch(void* const* api) { @@ -144,7 +150,6 @@ void ApplicationHeapInitialize_nid_no_patch(const void* processParameters) { std::lock_guard lock(heapMutex); heapFinalize = read(replacement, 0x18); } - if (std::atexit(finalize) != 0) throw std::runtime_error("application heap: cannot register finalization"); } catch (...) { std::lock_guard lock(heapMutex); heapFailure = std::current_exception(); @@ -221,7 +226,8 @@ int ApplicationHeapPosixAlign_nid_no_patch(void** pointer, std::size_t alignment const auto align = callback(6); CallbackScope scope; void* result = nullptr; - if (align(&result, alignment, bytes) != 0) throw std::runtime_error("application heap: posix_memalign failed"); + const int error = align(&result, alignment, bytes); + if (error != 0) return error; requireAllocation(result); if (reinterpret_cast(result) % alignment != 0) throw std::runtime_error("application heap: allocator returned a misaligned pointer"); *pointer = result; diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index fd29fb581..008cc422e 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -42,14 +42,6 @@ int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(void (*destructor)(void const std::error_category* _ZSt17iostream_categoryv_nid_postfix() { return &std::iostream_category(); } -std::FILE* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char* filename, int mode, int protection) { - static_cast(filename); - static_cast(mode); - static_cast(protection); - NotImplemented_nid_no_patch("std::_Fiopen"); - return nullptr; -} - int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix( int* flag, int (APS5_VABI *callback)(void*, void*, void**), void* arg ) { diff --git a/core/libs/prx/libc/src/FileAndHeap.cpp b/core/libs/prx/libc/src/FileAndHeap.cpp index 80f1716a1..c6a5de95d 100644 --- a/core/libs/prx/libc/src/FileAndHeap.cpp +++ b/core/libs/prx/libc/src/FileAndHeap.cpp @@ -12,6 +12,14 @@ #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/General.hpp" +static std::string NativeFileMode(const char* mode) { + std::string result(mode); +#ifdef _WIN32 + if (!result.empty() && result.find('b') == std::string::npos) result.insert(1, 1, 'b'); +#endif + return result; +} + extern "C" { [[noreturn]] void APS5_VABI _ZSt11_Xbad_allocv_nid_postfix(); @@ -49,7 +57,7 @@ FileStream* APS5_VABI freopen_nid_postfix(const char* filename, const char* mode if (!valid) { errno = 22; return nullptr; } try { const auto path = *filename ? ResolvePath_nid_no_patch(filename).string() : std::string{}; - if (stream->Reopen(path.c_str(), mode)) return stream; + if (stream->Reopen(path.c_str(), NativeFileMode(mode).c_str())) return stream; const int error = errno; if (stream->IsDynamic()) delete stream; errno = error; @@ -62,7 +70,7 @@ FileStream* APS5_VABI fopen_nid_postfix(const char* filename, const char* mode) if (!filename || !mode) throw std::runtime_error(std::string(__func__) + ": " + FOPEN_MSG_NULL_ARG); const std::filesystem::path fpath = ResolvePath_nid_no_patch(filename); const auto abs_path = fpath.string(); - std::unique_ptr handle(std::fopen(abs_path.c_str(), mode), std::fclose); + std::unique_ptr handle(std::fopen(abs_path.c_str(), NativeFileMode(mode).c_str()), std::fclose); if (!handle) { const int error = errno; if (error == ENOENT) { @@ -106,6 +114,33 @@ int APS5_VABI fclose_nid_postfix(FileStream* stream) { return 0; } +int APS5_VABI fseek_nid_postfix(FileStream* stream, std::int64_t offset, int origin); + +FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char* filename, int mode, int protection) { + static_cast(protection); + constexpr int In = 0x01, Out = 0x02, Ate = 0x04, App = 0x08, Trunc = 0x10, Nocreate = 0x20, Noreplace = 0x40, + Binary = 0x80; + constexpr std::pair modes[] = { + {In, "r"}, {Out, "w"}, {Out | Trunc, "w"}, {Out | App, "a"}, + {In | Binary, "rb"}, {Out | Binary, "wb"}, {Out | Trunc | Binary, "wb"}, {Out | App | Binary, "ab"}, + {In | Out, "r+"}, {In | Out | Trunc, "w+"}, {In | Out | App, "a+"}, + {In | Out | Binary, "r+b"}, {In | Out | Trunc | Binary, "w+b"}, {In | Out | App | Binary, "a+b"}}; + const int open = (mode & (In | Out | App | Trunc | Binary)) | ((mode & Nocreate) ? In : 0) | ((mode & App) ? Out : 0); + const char* openMode = nullptr; + for (const auto& [flags, text] : modes) if (flags == open) openMode = text; + if (!openMode) return nullptr; + if ((mode & Noreplace) && (open & Out)) { + if (auto* existing = fopen_nid_postfix(filename, "r")) { + fclose_nid_postfix(existing); + return nullptr; + } + } + auto* stream = fopen_nid_postfix(filename, openMode); + if (!stream || !(mode & Ate) || fseek_nid_postfix(stream, 0, SEEK_END) == 0) return stream; + fclose_nid_postfix(stream); + return nullptr; +} + size_t APS5_VABI fread_nid_postfix(void* buffer, size_t size, size_t count, FileStream* stream) { auto* handle = GetNativeStream(stream); if (size == 0 || count == 0) return 0; @@ -211,7 +246,11 @@ void* APS5_VABI calloc_nid_postfix(size_t count, size_t size) { } int APS5_VABI posix_memalign_nid_postfix(void** pointer, size_t alignment, size_t size) { - return ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, size); + if (!pointer || alignment < sizeof(void*) || (alignment & (alignment - 1)) != 0) return 22; + const int savedError = errno; + const int result = ApplicationHeapPosixAlign_nid_no_patch(pointer, alignment, size); + errno = savedError; + return result; } void* APS5_VABI bsearch_nid_postfix(const void* key, const void* base, size_t count, diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index 258f10c3f..d7f191170 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -2,7 +2,9 @@ #include #include #ifdef _WIN32 +#ifndef NOMINMAX #define NOMINMAX +#endif #include #else #include diff --git a/core/libs/prx/libc/src/Formatting.cpp b/core/libs/prx/libc/src/Formatting.cpp index 0cc9b6e63..875bdf156 100644 --- a/core/libs/prx/libc/src/Formatting.cpp +++ b/core/libs/prx/libc/src/Formatting.cpp @@ -76,7 +76,17 @@ int ScanGuest(const char* buffer, const char* format, bool secure, NextPointer n } ++cursor; if (conversion == 'n') { - if (!suppress) *static_cast(nextPointer()) = static_cast(input - buffer); + if (!suppress) { + void* target = nextPointer(); + const auto count = input - buffer; + if (length == "hh") *static_cast(target) = static_cast(count); + else if (length == "h") *static_cast(target) = static_cast(count); + else if (length.empty()) *static_cast(target) = static_cast(count); + else if (length == "l" || length == "ll" || length == "j" || length == "z" || length == "t") { + const std::int64_t value = count; + std::memcpy(target, &value, sizeof(value)); + } else throw std::invalid_argument("Invalid scan count length"); + } continue; } if (conversion != 'c' && conversion != '[') { @@ -208,7 +218,7 @@ int APS5_VABI fscanf_nid_postfix(FileStream* stream, const char* format, ...) { #ifdef _WIN32 __builtin_sysv_va_list args; __builtin_sysv_va_start(args, format); - const int result = std::vfscanf(native, format, *reinterpret_cast(args)); + const int result = LibcDetail::ScanFileWindows_nid_no_patch(native, format, args); __builtin_sysv_va_end(args); #else std::va_list args; diff --git a/core/libs/prx/libc/src/FormattingWide.cpp b/core/libs/prx/libc/src/FormattingWide.cpp index ad53b9862..35912693c 100644 --- a/core/libs/prx/libc/src/FormattingWide.cpp +++ b/core/libs/prx/libc/src/FormattingWide.cpp @@ -89,7 +89,7 @@ void AppendPadded(std::u16string& out, const std::u16string& text, bool left, in if (left) out.append(pad, u' '); } -std::u16string FormatWide(const char16_t* format, VaList* source, bool rejectNullStrings = false) { +std::u16string FormatWide(const char16_t* format, VaList* source, bool secure = false) { if (format == nullptr || source == nullptr) throw std::invalid_argument("Null formatting argument"); LibcDetail::FormatArguments args(source); std::u16string out; @@ -172,15 +172,23 @@ std::u16string FormatWide(const char16_t* format, VaList* source, bool rejectNul const std::size_t limit = precision < 0 ? SIZE_MAX : static_cast(precision); if (conversion == u'S' || length == "l") { const char16_t* value = args.Next(); - if (value == nullptr && rejectNullStrings) throw std::invalid_argument("Null string argument"); + if (value == nullptr && secure) throw std::invalid_argument("Null string argument"); if (value == nullptr) value = u"(null)"; for (std::size_t index = 0; index < limit && value[index] != 0; ++index) text.push_back(value[index]); } else { const char* value = args.Next(); - if (value == nullptr && rejectNullStrings) throw std::invalid_argument("Null string argument"); + if (value == nullptr && secure) throw std::invalid_argument("Null string argument"); AppendUtf16(text, value != nullptr ? value : "(null)", limit); } AppendPadded(out, text, left, width); + } else if (conversion == u'n' && integerLength && spec == "%" && !secure) { + void* pointer = args.Next(); + if (!pointer) throw std::invalid_argument("Null format count pointer"); + const int count = static_cast(out.size()); + if (length == "hh") *static_cast(pointer) = static_cast(count); + else if (length == "h") *static_cast(pointer) = static_cast(count); + else if (length.empty()) *static_cast(pointer) = count; + else *static_cast(pointer) = count; } else { throw std::invalid_argument("Unsupported format conversion"); } diff --git a/core/libs/prx/libc/src/GuestAllocations.cpp b/core/libs/prx/libc/src/GuestAllocations.cpp index 64c246c2a..344fcaa82 100644 --- a/core/libs/prx/libc/src/GuestAllocations.cpp +++ b/core/libs/prx/libc/src/GuestAllocations.cpp @@ -257,6 +257,16 @@ void GuestAllocationsRequireAvailable_nid_postfix(void*, const void* pointer, st } } +bool GuestAllocationsOverlaps_nid_postfix(void*, const void* pointer, std::size_t bytes) { + const auto address = reinterpret_cast(pointer); + require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); + for (const auto& [base, range] : registry().ranges) { + if (base >= address + bytes) break; + if (base + range->bytes > address) return true; + } + return false; +} + bool GuestAllocationsCovers_nid_postfix(void*, const void* pointer, std::size_t bytes) { const auto address = reinterpret_cast(pointer); require(address != 0 && bytes != 0 && bytes <= std::numeric_limits::max() - address, "invalid guest allocation range"); diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index abc4717b0..941df32b3 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -13,12 +13,19 @@ #include #include #include +#include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/GuestLocale.hpp" #include "SceTypes.hpp" +extern "C" { +void* APS5_VABI _Znwm_nid_postfix(std::size_t size); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); +} + namespace { std::mutex g_localeInitMutex; @@ -71,6 +78,71 @@ const GuestLocale::FacetVtable g_copiedLocaleVtable{DestroyCopiedLocale, Destroy GuestLocale::Facet* g_classicFacets[1]{}; GuestLocale::Implementation g_classicLocale{{&g_localeVtable, 1, 0}, g_classicFacets, 1, 0, false, "C"}; +constexpr std::size_t GuestStringInlineCapacity = 15; +constexpr std::size_t GuestStringLargeAllocation = 0x1000; + +void ValidateCharacterRange(const char* first, const char* last, const char* function) { + if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); +} + +void APS5_VABI DestroyCollate(GuestLocale::Facet*) {} + +void APS5_VABI DeleteCollate(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("collate delete: null facet"); + _ZdlPv_nid_postfix(self); +} + +void APS5_VABI RetainFacet(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("facet retain: null facet"); + std::atomic_ref(self->references).fetch_add(1); +} + +GuestLocale::Facet* APS5_VABI ReleaseFacet(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("facet release: null facet"); + return std::atomic_ref(self->references).fetch_sub(1) == 1 ? self : nullptr; +} + +int APS5_VABI CollateCompare(const GuestLocale::CollateFacet*, const char* first1, const char* last1, const char* first2, const char* last2) { + ValidateCharacterRange(first1, last1, "collate::do_compare"); + ValidateCharacterRange(first2, last2, "collate::do_compare"); + const auto length1 = static_cast(last1 - first1); + const auto length2 = static_cast(last2 - first2); + const auto common = std::min(length1, length2); + const int order = common == 0 ? 0 : std::memcmp(first1, first2, common); + if (order != 0) return order < 0 ? -1 : 1; + if (length1 == length2) return 0; + return length1 < length2 ? -1 : 1; +} + +GuestLocale::String* APS5_VABI CollateTransform(GuestLocale::String* result, const GuestLocale::CollateFacet*, const char* first, const char* last) { + if (result == nullptr) throw std::invalid_argument("collate::do_transform: null result"); + ValidateCharacterRange(first, last, "collate::do_transform"); + const auto length = static_cast(last - first); + if (length != 0 && std::memchr(first, 0, length) != nullptr) throw std::runtime_error("collate::do_transform: embedded null characters are not supported"); + if (length + 1 >= GuestStringLargeAllocation) throw std::runtime_error("collate::do_transform: results of 4095 characters or more are not supported"); + char* data = result->buffer; + std::size_t capacity = GuestStringInlineCapacity; + if (length > GuestStringInlineCapacity) { + capacity = length; + data = static_cast(_Znwm_nid_postfix(capacity + 1)); + } + if (length != 0) std::memcpy(data, first, length); + data[length] = 0; + if (data != result->buffer) result->pointer = data; + result->size = length; + result->capacity = capacity; + return result; +} + +std::int64_t APS5_VABI CollateHash(const GuestLocale::CollateFacet*, const char* first, const char* last) { + ValidateCharacterRange(first, last, "collate::do_hash"); + std::uint64_t hash = 0xcbf29ce484222325; + for (; first != last; ++first) hash = (hash ^ static_cast(*first)) * 0x100000001b3; + return static_cast(hash); +} + +const GuestLocale::CollateVtable g_collateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CollateCompare, CollateTransform, CollateHash}; + constexpr std::array MakeClassificationTable() { std::array table{}; for (unsigned int value = 0; value < 256; ++value) { @@ -189,8 +261,14 @@ void APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(Gu NotImplemented_nid_no_patch(__func__); } -void APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet**, const GuestLocale::Implementation*) { - NotImplemented_nid_no_patch(__func__); +std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { + if (facet != nullptr && *facet == nullptr) { + if (locale == nullptr || *locale == nullptr || (*locale)->name == nullptr || std::strcmp((*locale)->name, "C") != 0) throw std::invalid_argument("collate::_Getcat: only the C locale is supported"); + auto* collate = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::CollateFacet))); + *collate = {{&g_collateVtable.facet, 0, 0}, nullptr, nullptr}; + *facet = &collate->base; + } + return 1; } void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(GuestLocale::LocinfoStorage* self, const char* localeName) { diff --git a/core/libs/prx/libc/src/StdioExtras.cpp b/core/libs/prx/libc/src/StdioExtras.cpp index a29b690bc..09e1b5799 100644 --- a/core/libs/prx/libc/src/StdioExtras.cpp +++ b/core/libs/prx/libc/src/StdioExtras.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/ApplicationHeap.hpp" @@ -76,12 +77,20 @@ void* APS5_VABI _Znam_nid_postfix(std::size_t size) { return Allocate(size); } -void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) { - return Allocate(size); +void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) noexcept { + try { + return Allocate(size); + } catch (const std::bad_alloc&) { + return nullptr; + } } -void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) { - return Allocate(size); +void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t size, const void*) noexcept { + try { + return Allocate(size); + } catch (const std::bad_alloc&) { + return nullptr; + } } void APS5_VABI _ZdlPv_nid_postfix(void* pointer) { diff --git a/core/libs/prx/libc/src/Strings.cpp b/core/libs/prx/libc/src/Strings.cpp index 7c9be2a2e..8cba0d618 100644 --- a/core/libs/prx/libc/src/Strings.cpp +++ b/core/libs/prx/libc/src/Strings.cpp @@ -227,6 +227,13 @@ int APS5_VABI memcpy_s_nid_postfix(void* dest, size_t destsz, const void* src, s std::memset(dest, 0, destsz); return src ? GuestErange : GuestEinval; } + const auto destination = reinterpret_cast(dest); + const auto source = reinterpret_cast(src); + const auto distance = destination < source ? source - destination : destination - source; + if (count != 0 && distance < count) { + std::memset(dest, 0, destsz); + return GuestEinval; + } std::memcpy(dest, src, count); return 0; } diff --git a/core/libs/prx/libcohtml/CMakeLists.txt b/core/libs/prx/libcohtml/CMakeLists.txt deleted file mode 100644 index 842799d34..000000000 --- a/core/libs/prx/libcohtml/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -add_library(libcohtml SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp -) -target_include_directories(libcohtml PRIVATE ${LIBS_INCLUDE_DIR}) -set_target_properties(libcohtml PROPERTIES CXX_EXTENSIONS OFF) -configure_windows_unwind(libcohtml) diff --git a/core/libs/prx/libcohtml/DEPLOY.md b/core/libs/prx/libcohtml/DEPLOY.md deleted file mode 100644 index 0b4cbdc5d..000000000 --- a/core/libs/prx/libcohtml/DEPLOY.md +++ /dev/null @@ -1,16 +0,0 @@ -# libcohtml — deploy note - -Game `DT_NEEDED` expects exactly: `libcohtml.Prospero.prx`. - -Copy next to the relinked eboot (same directory): - -- `build/core/libs/libs/libcohtml.Prospero.prx` (patched build output, 50 NIDs) — required. -- `libgcc_s_seh-1.dll`, `libstdc++-6.dll`, `libwinpthread-1.dll` from `winlibs-gcc15/mingw64/bin` — required on Windows (see [technical debt](../../../../docs/dev/TechnicalDebt.md)). - -Do NOT deploy: - -- `libcohtml.prx` (build intermediate kept for the NID-patch pipeline, wrong `DT_NEEDED` name). -- Anything under `build/core/libs/libs/unpatched/` (NIDs not patched). -- `libc.prx` / `libkernel.prx` are not runtime imports of this module (no such DLL dependency); the game may need its other modules separately (out of scope). - -Scope: raw-NID stubs only; real Cohtml rendering stays out of scope. diff --git a/core/libs/prx/libcohtml/Export.cpp b/core/libs/prx/libcohtml/Export.cpp deleted file mode 100644 index 4ae1996b4..000000000 --- a/core/libs/prx/libcohtml/Export.cpp +++ /dev/null @@ -1,307 +0,0 @@ -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -extern "C" { - -APS5_EXPORT("1fOA53oGwU8", cohtml_stub_00); -std::uint64_t APS5_VABI cohtml_stub_00(void) { - NotImplemented_nid_no_patch("1fOA53oGwU8"); - return 0; -} - -APS5_EXPORT("2vxXHkZlhbw", cohtml_stub_01); -std::uint64_t APS5_VABI cohtml_stub_01(void) { - NotImplemented_nid_no_patch("2vxXHkZlhbw"); - return 0; -} - -APS5_EXPORT("3rew70M0SmY", cohtml_stub_02); -std::uint64_t APS5_VABI cohtml_stub_02(void) { - NotImplemented_nid_no_patch("3rew70M0SmY"); - return 0; -} - -APS5_EXPORT("4Le0SBPvnF8", cohtml_stub_03); -std::uint64_t APS5_VABI cohtml_stub_03(void) { - NotImplemented_nid_no_patch("4Le0SBPvnF8"); - return 0; -} - -APS5_EXPORT("53ZTBeo2a54", cohtml_stub_04); -std::uint64_t APS5_VABI cohtml_stub_04(void) { - NotImplemented_nid_no_patch("53ZTBeo2a54"); - return 0; -} - -APS5_EXPORT("6PaNuQipllM", cohtml_stub_05); -std::uint64_t APS5_VABI cohtml_stub_05(void) { - NotImplemented_nid_no_patch("6PaNuQipllM"); - return 0; -} - -APS5_EXPORT("7hn5DAs2bLk", cohtml_stub_06); -std::uint64_t APS5_VABI cohtml_stub_06(void) { - NotImplemented_nid_no_patch("7hn5DAs2bLk"); - return 0; -} - -APS5_EXPORT("8EYWdAM2-R0", cohtml_stub_07); -std::uint64_t APS5_VABI cohtml_stub_07(void) { - NotImplemented_nid_no_patch("8EYWdAM2-R0"); - return 0; -} - -APS5_EXPORT("9bGvfp1zbKo", cohtml_stub_08); -std::uint64_t APS5_VABI cohtml_stub_08(void) { - NotImplemented_nid_no_patch("9bGvfp1zbKo"); - return 0; -} - -APS5_EXPORT("a4E5dJh0onE", cohtml_stub_09); -std::uint64_t APS5_VABI cohtml_stub_09(void) { - NotImplemented_nid_no_patch("a4E5dJh0onE"); - return 0; -} - -APS5_EXPORT("BhL-ZtSHmB8", cohtml_stub_10); -std::uint64_t APS5_VABI cohtml_stub_10(void) { - NotImplemented_nid_no_patch("BhL-ZtSHmB8"); - return 0; -} - -APS5_EXPORT("blvwzOvsEAA", cohtml_stub_11); -std::uint64_t APS5_VABI cohtml_stub_11(void) { - NotImplemented_nid_no_patch("blvwzOvsEAA"); - return 0; -} - -APS5_EXPORT("dko-cMqv5yw", cohtml_stub_12); -std::uint64_t APS5_VABI cohtml_stub_12(void) { - NotImplemented_nid_no_patch("dko-cMqv5yw"); - return 0; -} - -APS5_EXPORT("EG4kqkxLU3Q", cohtml_stub_13); -std::uint64_t APS5_VABI cohtml_stub_13(void) { - NotImplemented_nid_no_patch("EG4kqkxLU3Q"); - return 0; -} - -APS5_EXPORT("-El4B0xJF2U", cohtml_stub_14); -std::uint64_t APS5_VABI cohtml_stub_14(void) { - NotImplemented_nid_no_patch("-El4B0xJF2U"); - return 0; -} - -APS5_EXPORT("EMhQNylkSwk", cohtml_stub_15); -std::uint64_t APS5_VABI cohtml_stub_15(void) { - NotImplemented_nid_no_patch("EMhQNylkSwk"); - return 0; -} - -APS5_EXPORT("-eNSKAypRz0", cohtml_stub_16); -std::uint64_t APS5_VABI cohtml_stub_16(void) { - NotImplemented_nid_no_patch("-eNSKAypRz0"); - return 0; -} - -APS5_EXPORT("FG9VRZfson0", cohtml_stub_17); -std::uint64_t APS5_VABI cohtml_stub_17(void) { - NotImplemented_nid_no_patch("FG9VRZfson0"); - return 0; -} - -APS5_EXPORT("fNvtExaHN4U", cohtml_stub_18); -std::uint64_t APS5_VABI cohtml_stub_18(void) { - NotImplemented_nid_no_patch("fNvtExaHN4U"); - return 0; -} - -APS5_EXPORT("g10DkPARoa8", cohtml_stub_19); -std::uint64_t APS5_VABI cohtml_stub_19(void) { - NotImplemented_nid_no_patch("g10DkPARoa8"); - return 0; -} - -APS5_EXPORT("gwQczxoDmG0", cohtml_stub_20); -std::uint64_t APS5_VABI cohtml_stub_20(void) { - NotImplemented_nid_no_patch("gwQczxoDmG0"); - return 0; -} - -APS5_EXPORT("HQF4O9D+Di4", cohtml_stub_21); -std::uint64_t APS5_VABI cohtml_stub_21(void) { - NotImplemented_nid_no_patch("HQF4O9D+Di4"); - return 0; -} - -APS5_EXPORT("igt3a6Q0VMc", cohtml_stub_22); -std::uint64_t APS5_VABI cohtml_stub_22(void) { - NotImplemented_nid_no_patch("igt3a6Q0VMc"); - return 0; -} - -APS5_EXPORT("kyApuhEHHxQ", cohtml_stub_23); -std::uint64_t APS5_VABI cohtml_stub_23(void) { - NotImplemented_nid_no_patch("kyApuhEHHxQ"); - return 0; -} - -APS5_EXPORT("Ljp75QXUi7s", cohtml_stub_24); -std::uint64_t APS5_VABI cohtml_stub_24(void) { - NotImplemented_nid_no_patch("Ljp75QXUi7s"); - return 0; -} - -APS5_EXPORT("loJ5Ef3MZrs", cohtml_stub_25); -std::uint64_t APS5_VABI cohtml_stub_25(void) { - NotImplemented_nid_no_patch("loJ5Ef3MZrs"); - return 0; -} - -APS5_EXPORT("LWe+u8SHKgU", cohtml_stub_26); -std::uint64_t APS5_VABI cohtml_stub_26(void) { - NotImplemented_nid_no_patch("LWe+u8SHKgU"); - return 0; -} - -APS5_EXPORT("nhn1ds+I2t4", cohtml_stub_27); -std::uint64_t APS5_VABI cohtml_stub_27(void) { - NotImplemented_nid_no_patch("nhn1ds+I2t4"); - return 0; -} - -APS5_EXPORT("NmVsRWgsKt4", cohtml_stub_28); -std::uint64_t APS5_VABI cohtml_stub_28(void) { - NotImplemented_nid_no_patch("NmVsRWgsKt4"); - return 0; -} - -APS5_EXPORT("ONEnhMinebs", cohtml_stub_29); -std::uint64_t APS5_VABI cohtml_stub_29(void) { - NotImplemented_nid_no_patch("ONEnhMinebs"); - return 0; -} - -APS5_EXPORT("PdOmR8dy1fM", cohtml_stub_30); -std::uint64_t APS5_VABI cohtml_stub_30(void) { - NotImplemented_nid_no_patch("PdOmR8dy1fM"); - return 0; -} - -APS5_EXPORT("PSwGBN5Icv8", cohtml_stub_31); -std::uint64_t APS5_VABI cohtml_stub_31(void) { - NotImplemented_nid_no_patch("PSwGBN5Icv8"); - return 0; -} - -APS5_EXPORT("RalyfwiTt8o", cohtml_stub_32); -std::uint64_t APS5_VABI cohtml_stub_32(void) { - NotImplemented_nid_no_patch("RalyfwiTt8o"); - return 0; -} - -APS5_EXPORT("t+OHUjVWIwU", cohtml_stub_33); -std::uint64_t APS5_VABI cohtml_stub_33(void) { - NotImplemented_nid_no_patch("t+OHUjVWIwU"); - return 0; -} - -APS5_EXPORT("Ter8YCVZ9tQ", cohtml_stub_34); -std::uint64_t APS5_VABI cohtml_stub_34(void) { - NotImplemented_nid_no_patch("Ter8YCVZ9tQ"); - return 0; -} - -APS5_EXPORT("tG3LJfAOH3k", cohtml_stub_35); -std::uint64_t APS5_VABI cohtml_stub_35(void) { - NotImplemented_nid_no_patch("tG3LJfAOH3k"); - return 0; -} - -APS5_EXPORT("ukSFhKPmCTg", cohtml_stub_36); -std::uint64_t APS5_VABI cohtml_stub_36(void) { - NotImplemented_nid_no_patch("ukSFhKPmCTg"); - return 0; -} - -APS5_EXPORT("V7KGze2XjJc", cohtml_stub_37); -std::uint64_t APS5_VABI cohtml_stub_37(void) { - NotImplemented_nid_no_patch("V7KGze2XjJc"); - return 0; -} - -APS5_EXPORT("vlmAvJqR7mQ", cohtml_stub_38); -std::uint64_t APS5_VABI cohtml_stub_38(void) { - NotImplemented_nid_no_patch("vlmAvJqR7mQ"); - return 0; -} - -APS5_EXPORT("VzhxjXDzYSI", cohtml_stub_39); -std::uint64_t APS5_VABI cohtml_stub_39(void) { - NotImplemented_nid_no_patch("VzhxjXDzYSI"); - return 0; -} - -APS5_EXPORT("X3FzDPzpD2Y", cohtml_stub_40); -std::uint64_t APS5_VABI cohtml_stub_40(void) { - NotImplemented_nid_no_patch("X3FzDPzpD2Y"); - return 0; -} - -APS5_EXPORT("x68HPh+vxsE", cohtml_stub_41); -std::uint64_t APS5_VABI cohtml_stub_41(void) { - NotImplemented_nid_no_patch("x68HPh+vxsE"); - return 0; -} - -APS5_EXPORT("XUe8OAKB-vQ", cohtml_stub_42); -std::uint64_t APS5_VABI cohtml_stub_42(void) { - NotImplemented_nid_no_patch("XUe8OAKB-vQ"); - return 0; -} - -APS5_EXPORT("y6UNQtnr0wY", cohtml_stub_43); -std::uint64_t APS5_VABI cohtml_stub_43(void) { - NotImplemented_nid_no_patch("y6UNQtnr0wY"); - return 0; -} - -APS5_EXPORT("YoHAXkBV06c", cohtml_stub_44); -std::uint64_t APS5_VABI cohtml_stub_44(void) { - NotImplemented_nid_no_patch("YoHAXkBV06c"); - return 0; -} - -APS5_EXPORT("Z9XFJCfmId4", cohtml_stub_45); -std::uint64_t APS5_VABI cohtml_stub_45(void) { - NotImplemented_nid_no_patch("Z9XFJCfmId4"); - return 0; -} - -APS5_EXPORT("zEniuuzG2W0", cohtml_stub_46); -std::uint64_t APS5_VABI cohtml_stub_46(void) { - NotImplemented_nid_no_patch("zEniuuzG2W0"); - return 0; -} - -APS5_EXPORT("zH9Z2nK1fmA", cohtml_stub_47); -std::uint64_t APS5_VABI cohtml_stub_47(void) { - NotImplemented_nid_no_patch("zH9Z2nK1fmA"); - return 0; -} - -APS5_EXPORT("ZHGgdWeYTo8", cohtml_stub_48); -std::uint64_t APS5_VABI cohtml_stub_48(void) { - NotImplemented_nid_no_patch("ZHGgdWeYTo8"); - return 0; -} - -APS5_EXPORT("ZWftPxZ4E6k", cohtml_stub_49); -std::uint64_t APS5_VABI cohtml_stub_49(void) { - NotImplemented_nid_no_patch("ZWftPxZ4E6k"); - return 0; -} - -} diff --git a/core/libs/prx/libfmod/CMakeLists.txt b/core/libs/prx/libfmod/CMakeLists.txt deleted file mode 100644 index 5d1cb47f7..000000000 --- a/core/libs/prx/libfmod/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -add_library(libfmod SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp -) - -target_include_directories(libfmod PRIVATE - ${LIBS_INCLUDE_DIR} - ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include -) -target_link_libraries(libfmod PRIVATE libkernel libc SDL2-static) -target_compile_options(libfmod PRIVATE -fkeep-inline-functions) -configure_windows_unwind(libfmod) diff --git a/core/libs/prx/libfmod/Export.cpp b/core/libs/prx/libfmod/Export.cpp deleted file mode 100644 index bd7545a64..000000000 --- a/core/libs/prx/libfmod/Export.cpp +++ /dev/null @@ -1,1902 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" -#include "SDL.h" - -// FMOD_MODE bits (subset, from fmod_common.h) — used to detect memory buffers. -constexpr unsigned FMOD_LOOP_OFF = 0x00000001u; -constexpr unsigned FMOD_LOOP_NORMAL = 0x00000002u; -constexpr unsigned FMOD_LOOP_BIDI = 0x00000004u; -constexpr unsigned FMOD_2D_FLAG = 0x00000008u; -constexpr unsigned FMOD_3D_FLAG = 0x00000010u; -constexpr unsigned FMOD_CREATESTREAM_FLAG = 0x00000080u; -constexpr unsigned FMOD_OPENMEMORY_FLAG = 0x00000800u; -constexpr unsigned FMOD_OPENMEMORY_POINT_FLAG = 0x10000000u; -constexpr unsigned FMOD_OPENRAW_FLAG = 0x00001000u; -constexpr unsigned FMOD_NONBLOCKING_FLAG = 0x00010000u; - -enum FMOD_OUTPUTTYPE { - FMOD_OUTPUTTYPE_AUTODETECT = 0, - FMOD_OUTPUTTYPE_UNKNOWN = 1, - FMOD_OUTPUTTYPE_NOSOUND = 2, - FMOD_OUTPUTTYPE_WAVWRITER = 3, - FMOD_OUTPUTTYPE_NOSOUND_NRT = 4, - FMOD_OUTPUTTYPE_WAVWRITER_NRT = 5, - FMOD_OUTPUTTYPE_WASAPI = 6, - FMOD_OUTPUTTYPE_ASIO = 7, - FMOD_OUTPUTTYPE_PULSEAUDIO = 8, - FMOD_OUTPUTTYPE_ALSA = 9, - FMOD_OUTPUTTYPE_COREAUDIO = 10, - FMOD_OUTPUTTYPE_AUDIOTRACK = 11, - FMOD_OUTPUTTYPE_OPENSL = 12, - FMOD_OUTPUTTYPE_AUDIOOUT = 13, - FMOD_OUTPUTTYPE_AUDIO3D = 14, - FMOD_OUTPUTTYPE_MAX = 15 -}; - -enum FMOD_SPEAKERMODE { - FMOD_SPEAKERMODE_DEFAULT = 0, - FMOD_SPEAKERMODE_RAW = 1, - FMOD_SPEAKERMODE_MONO = 2, - FMOD_SPEAKERMODE_STEREO = 3, - FMOD_SPEAKERMODE_QUAD = 4, - FMOD_SPEAKERMODE_SURROUND = 5, - FMOD_SPEAKERMODE_5POINT1 = 6, - FMOD_SPEAKERMODE_7POINT1 = 7, - FMOD_SPEAKERMODE_7POINT1POINT4 = 8, - FMOD_SPEAKERMODE_MAX = 9 -}; - -enum FMOD_SOUND_TYPE { - FMOD_SOUND_TYPE_UNKNOWN = 0, - FMOD_SOUND_TYPE_AIFF = 1, - FMOD_SOUND_TYPE_ASF = 2, - FMOD_SOUND_TYPE_DLS = 3, - FMOD_SOUND_TYPE_FLAC = 4, - FMOD_SOUND_TYPE_FSB = 5, - FMOD_SOUND_TYPE_IT = 6, - FMOD_SOUND_TYPE_MIDI = 7, - FMOD_SOUND_TYPE_MOD = 8, - FMOD_SOUND_TYPE_MPEG = 9, - FMOD_SOUND_TYPE_OGGVORBIS = 10, - FMOD_SOUND_TYPE_PLAYLIST = 11, - FMOD_SOUND_TYPE_RAW = 12, - FMOD_SOUND_TYPE_S3M = 13, - FMOD_SOUND_TYPE_USER = 14, - FMOD_SOUND_TYPE_XM = 15, - FMOD_SOUND_TYPE_XMA = 16, - FMOD_SOUND_TYPE_AUDIOQUEUE = 17, - FMOD_SOUND_TYPE_AT9 = 18, - FMOD_SOUND_TYPE_VB = 19, - FMOD_SOUND_TYPE_MAX = 20 -}; - -enum FMOD_SOUND_FORMAT { - FMOD_SOUND_FORMAT_NONE = 0, - FMOD_SOUND_FORMAT_PCM8 = 1, - FMOD_SOUND_FORMAT_PCM16 = 2, - FMOD_SOUND_FORMAT_PCM24 = 3, - FMOD_SOUND_FORMAT_PCM32 = 4, - FMOD_SOUND_FORMAT_PCMFLOAT = 5, - FMOD_SOUND_FORMAT_BITSTREAM = 6, - FMOD_SOUND_FORMAT_MAX = 7 -}; - -enum FMOD_OPENSTATE { - FMOD_OPENSTATE_READY = 0, - FMOD_OPENSTATE_LOADING = 1, - FMOD_OPENSTATE_ERROR = 2, - FMOD_OPENSTATE_CONNECTING = 3, - FMOD_OPENSTATE_BUFFERING = 4, - FMOD_OPENSTATE_SEEKING = 5, - FMOD_OPENSTATE_PLAYING = 6, - FMOD_OPENSTATE_SETPOSITION = 7, - FMOD_OPENSTATE_MAX = 8 -}; - -enum FMOD_SOUNDGROUP_BEHAVIOR { - FMOD_SOUNDGROUP_BEHAVIOR_FAIL = 0, - FMOD_SOUNDGROUP_BEHAVIOR_MUTE = 1, - FMOD_SOUNDGROUP_BEHAVIOR_STEALLOWEST = 2, - FMOD_SOUNDGROUP_BEHAVIOR_MAX = 3 -}; - -enum FMOD_DSP_TYPE { - FMOD_DSP_TYPE_UNKNOWN = 0, - FMOD_DSP_TYPE_MIXER = 1, - FMOD_DSP_TYPE_OSCILLATOR = 2, - FMOD_DSP_TYPE_LOWPASS = 3, - FMOD_DSP_TYPE_ITLOWPASS = 4, - FMOD_DSP_TYPE_HIGHPASS = 5, - FMOD_DSP_TYPE_ECHO = 6, - FMOD_DSP_TYPE_FADER = 7, - FMOD_DSP_TYPE_FLANGE = 8, - FMOD_DSP_TYPE_DISTORTION = 9, - FMOD_DSP_TYPE_NORMALIZE = 10, - FMOD_DSP_TYPE_LIMITER = 11, - FMOD_DSP_TYPE_PARAMEQ = 12, - FMOD_DSP_TYPE_PITCHSHIFT = 13, - FMOD_DSP_TYPE_CHORUS = 14, - FMOD_DSP_TYPE_VSTPLUGIN = 15, - FMOD_DSP_TYPE_WINAMPPLUGIN = 16, - FMOD_DSP_TYPE_ITECHO = 17, - FMOD_DSP_TYPE_COMPRESSOR = 18, - FMOD_DSP_TYPE_SFXREVERB = 19, - FMOD_DSP_TYPE_LOWPASS_SIMPLE = 20, - FMOD_DSP_TYPE_DELAY = 21, - FMOD_DSP_TYPE_TREMOLO = 22, - FMOD_DSP_TYPE_SEND = 24, - FMOD_DSP_TYPE_RETURN = 25, - FMOD_DSP_TYPE_HIGHPASS_SIMPLE = 26, - FMOD_DSP_TYPE_PAN = 27, - FMOD_DSP_TYPE_THREE_EQ = 28, - FMOD_DSP_TYPE_FFT = 29, - FMOD_DSP_TYPE_LOUDNESS_METER = 30, - FMOD_DSP_TYPE_ENVELOPEFOLLOWER = 31, - FMOD_DSP_TYPE_CONVOLUTIONREVERB = 32, - FMOD_DSP_TYPE_TRANSCEIVER = 33, - FMOD_DSP_TYPE_MAX = 34 -}; - -enum FMOD_DSPCONNECTION_TYPE { - FMOD_DSPCONNECTION_TYPE_STANDARD = 0, - FMOD_DSPCONNECTION_TYPE_SIDECHAIN = 1, - FMOD_DSPCONNECTION_TYPE_SEND = 2, - FMOD_DSPCONNECTION_TYPE_SEND_SIDECHAIN = 3, - FMOD_DSPCONNECTION_TYPE_MAX = 4 -}; - -enum FMOD_DSP_FFT_WINDOW { - FMOD_DSP_FFT_WINDOW_RECT = 0, - FMOD_DSP_FFT_WINDOW_TRIANGLE = 1, - FMOD_DSP_FFT_WINDOW_HAMMING = 2, - FMOD_DSP_FFT_WINDOW_HANNING = 3, - FMOD_DSP_FFT_WINDOW_BLACKMAN = 4, - FMOD_DSP_FFT_WINDOW_BLACKMANHARRIS = 5, - FMOD_DSP_FFT_WINDOW_MAX = 6 -}; - -enum FMOD_PLUGINTYPE { - FMOD_PLUGINTYPE_OUTPUT = 0, - FMOD_PLUGINTYPE_CODEC = 1, - FMOD_PLUGINTYPE_DSP = 2, - FMOD_PLUGINTYPE_MAX = 3 -}; - -enum FMOD_DRIVER_STATE { - FMOD_DRIVER_STATE_CONNECTED = 1, - FMOD_DRIVER_STATE_DEFAULT = 2 -}; - -enum FMOD_PORT_TYPE { - FMOD_PORT_TYPE_MUSIC = 0, - FMOD_PORT_TYPE_COPY = 1, - FMOD_PORT_TYPE_SPLIT = 2, - FMOD_PORT_TYPE_FEEDBACK = 3, - FMOD_PORT_TYPE_MAX = 4 -}; - -enum FMOD_SYSTEM_CALLBACK_TYPE { - FMOD_SYSTEM_CALLBACK_DEVICELISTCHANGED = 1, - FMOD_SYSTEM_CALLBACK_DEVICELOST = 2, - FMOD_SYSTEM_CALLBACK_MEMORYALLOCATIONFAILED = 4, - FMOD_SYSTEM_CALLBACK_THREADCREATED = 8, - FMOD_SYSTEM_CALLBACK_BADDSPCONNECTION = 16, - FMOD_SYSTEM_CALLBACK_PREMIX = 32, - FMOD_SYSTEM_CALLBACK_POSTMIX = 64, - FMOD_SYSTEM_CALLBACK_ERROR = 128, - FMOD_SYSTEM_CALLBACK_MIDMIX = 256, - FMOD_SYSTEM_CALLBACK_THREADDESTROYED = 512, - FMOD_SYSTEM_CALLBACK_PREUPDATE = 1024, - FMOD_SYSTEM_CALLBACK_POSTUPDATE = 2048, - FMOD_SYSTEM_CALLBACK_ALL = 0xFFFFFFFF -}; - -enum FMOD_CHANNELCONTROL_TYPE { - FMOD_CHANNELCONTROL_CHANNEL = 0, - FMOD_CHANNELCONTROL_CHANNELGROUP = 1, - FMOD_CHANNELCONTROL_MAX = 2 -}; - -enum FMOD_CHANNELCONTROL_CALLBACK_TYPE { - FMOD_CHANNELCONTROL_CALLBACK_END = 0, - FMOD_CHANNELCONTROL_CALLBACK_VIRTUALVOICE = 1, - FMOD_CHANNELCONTROL_CALLBACK_SYNCPOINT = 2, - FMOD_CHANNELCONTROL_CALLBACK_OCCLUSION = 3, - FMOD_CHANNELCONTROL_CALLBACK_MAX = 4 -}; - -enum FMOD_TAGTYPE { - FMOD_TAGTYPE_UNKNOWN = 0, - FMOD_TAGTYPE_ID3V1 = 1, - FMOD_TAGTYPE_ID3V2 = 2, - FMOD_TAGTYPE_VORBISCOMMENT = 3, - FMOD_TAGTYPE_SHOUTCAST = 4, - FMOD_TAGTYPE_ICECAST = 5, - FMOD_TAGTYPE_ASF = 6, - FMOD_TAGTYPE_MIDI = 7, - FMOD_TAGTYPE_PLAYLIST = 8, - FMOD_TAGTYPE_FMOD = 9, - FMOD_TAGTYPE_USER = 10, - FMOD_TAGTYPE_MAX = 11 -}; - -enum FMOD_TAGDATATYPE { - FMOD_TAGDATATYPE_BINARY = 0, - FMOD_TAGDATATYPE_INT = 1, - FMOD_TAGDATATYPE_FLOAT = 2, - FMOD_TAGDATATYPE_STRING = 3, - FMOD_TAGDATATYPE_STRING_UTF16 = 4, - FMOD_TAGDATATYPE_STRING_UTF16BE = 5, - FMOD_TAGDATATYPE_STRING_UTF8 = 6, - FMOD_TAGDATATYPE_CDTOC = 7, - FMOD_TAGDATATYPE_MAX = 8 -}; - -struct FMOD_GUID { - unsigned Data1; - unsigned short Data2; - unsigned short Data3; - unsigned char Data4[8]; -}; - -struct FMOD_VECTOR { - float x; - float y; - float z; -}; - -struct FMOD_3D_ATTRIBUTES { - FMOD_VECTOR position; - FMOD_VECTOR velocity; - FMOD_VECTOR forward; - FMOD_VECTOR up; -}; - -struct FMOD_CREATESOUNDEXINFO { - int cbsize; - unsigned length; - unsigned fileoffset; - int numchannels; - int defaultfrequency; - FMOD_SOUND_FORMAT format; - unsigned decodebuffersize; - int initialsubsound; - int numsubsounds; - void* inclusionlist; - unsigned inclusionlistnum; - unsigned char dlsrawdata[8]; - char* encryptionkey; -}; - -struct FMOD_REVERB_PROPERTIES { - int Instance; - float Environment; - float EnvSize; - float EnvDiffusion; - int Room; - int RoomHF; - int RoomLF; - float DecayTime; - float DecayHFRatio; - float DecayLFRatio; - int Reflections; - float ReflectionsDelay; - int Reverb; - float ReverbDelay; - float EchoTime; - float EchoDepth; - float ModulationTime; - float ModulationDepth; - float AirAbsorptionHF; - float HFReference; - float LFReference; - float RoomRolloffFactor; - float Diffusion; - float Density; - unsigned Flags; -}; - -struct FMOD_ADVANCEDSETTINGS { - int cbSize; - unsigned maxMPEGCodecs; - unsigned maxADPCMCodecs; - unsigned maxXMACodecs; - unsigned maxVorbisCodecs; - unsigned maxAT9Codecs; - unsigned maxFADPCMCodecs; - unsigned maxPCMCodecs; - unsigned ASIONumChannels; - char* ASIOChannelList; - char* ASIOSpeakerList; - float vol0virtualvol; - unsigned defaultDecodeBufferSize; - unsigned short profilePort; - unsigned geometryMaxFadeTime; - float distanceFilterCenterFreq; - int reverb3Dinstance; - unsigned DSPBufferPoolSize; - unsigned stackSizeStream; - unsigned stackSizeNonBlocking; - unsigned stackSizeMixer; - int resamplerMethod; - unsigned commandQueueSize; - unsigned handleInitialSize; -}; - -struct FMOD_CPU_USAGE { - float dsp; - float stream; - float geometry; - float update; - float convolution1; - float convolution2; -}; - -struct FMOD_TAG { - FMOD_TAGTYPE type; - FMOD_TAGDATATYPE datatype; - char* name; - void* data; - unsigned datalen; - bool updated; -}; - -struct FMOD_SYNC_POINT { - void* opaque; -}; - -struct FMOD_DSP_DESCRIPTION { - unsigned pluginsdkversion; - char name[32]; - unsigned version; - int numinputbuffers; - int numoutputbuffers; - void* create; - void* release; - void* reset; - void* read; - void* process; - void* setposition; - int numparameters; - void* paramdesc; - void* setparameterfloat; - void* setparameterint; - void* setparameterbool; - void* setparameterdata; - void* getparameterfloat; - void* getparameterint; - void* getparameterbool; - void* getparameterdata; - void* shouldiprocess; - void* userdata; - void* sys_register; - void* sys_deregister; - void* sys_mix; -}; - -struct FMOD_DSP_PARAMETER_DESC { - int type; - char name[16]; - char label[16]; - char* description; -}; - -struct FMOD_DSP_METERING_INFO { - int numsamples; - float peaklevel[32]; - float rmslevel[32]; - short numchannels; -}; - -struct FMOD_DSP_BUFFER_ARRAY { - int numbuffers; - int* buffernumchannels; - int* bufferchannelmask; - float** buffers; - FMOD_SPEAKERMODE speakermode; -}; - -struct FMOD_DSP_STATE { - void* instance; - void* plugindata; - int channelmask; - FMOD_SPEAKERMODE source_speakermode; - float* sidechaindata; - int sidechainchannels; - void* callbacks; - int systemobject; -}; - -struct FMOD_SYSTEM { - int unused; -}; - -struct FMOD_CHANNELCONTROL { - int unused; -}; - -struct FMOD_CHANNELGROUP { - int unused; -}; - -typedef int (*FMOD_SYSTEM_CALLBACK)(FMOD_SYSTEM* system, FMOD_SYSTEM_CALLBACK_TYPE type, void* commanddata1, void* commanddata2, void* userdata); -typedef int (*FMOD_CHANNELCONTROL_CALLBACK)(FMOD_CHANNELCONTROL* channelcontrol, FMOD_CHANNELCONTROL_TYPE controltype, FMOD_CHANNELCONTROL_CALLBACK_TYPE callbacktype, void* commanddata1, void* commanddata2); - -namespace { - -std::mutex gFmodMutex; - -constexpr int FMOD_OK = 0; -constexpr unsigned FMOD_VERSION_CURRENT = 0x00020206; - -// ---- Real-audio backend (SDL queue, mirrors libSceAudioOut) ---- -// Silent fallback is kept via ANYPS5_FMOD_SILENT=1 or NOSOUND output type -// or when SDL cannot be opened. When silent, update() is a no-op and -// isPlaying() keeps the previous sticky behaviour so the old smoke test -// (playSound(nullptr) -> playing==true) still passes. -constexpr int kFmodOutRate = 48000; -constexpr int kFmodOutCh = 2; -constexpr int kFmodMixFrames = 1024; - -static SDL_AudioDeviceID gFmodDevice = 0; -static SDL_AudioSpec gFmodSpec = {}; -static bool gFmodSdlTried = false; -static bool gFmodSilent = false; - -static bool FmodSilentRequested() { - const char* s = std::getenv("ANYPS5_FMOD_SILENT"); - if (s && (s[0] == '1' || s[0] == 'y' || s[0] == 'Y')) return true; - s = std::getenv("ANYPS5_FMOD_NOSOUND"); - if (s && (s[0] == '1' || s[0] == 'y' || s[0] == 'Y')) return true; - return false; -} - -static bool FmodEnsureDevice() { - if (gFmodDevice != 0) return true; - if (gFmodSdlTried) return gFmodDevice != 0; - gFmodSdlTried = true; - if (FmodSilentRequested()) { - gFmodSilent = true; - return false; - } - if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) { - gFmodSilent = true; - return false; - } - SDL_AudioSpec desired{}; - desired.freq = kFmodOutRate; - desired.format = AUDIO_F32SYS; - desired.channels = static_cast(kFmodOutCh); - desired.samples = 1024; - desired.callback = nullptr; - SDL_AudioSpec obtained{}; - gFmodDevice = SDL_OpenAudioDevice(nullptr, 0, &desired, &obtained, SDL_AUDIO_ALLOW_ANY_CHANGE); - if (gFmodDevice == 0) { - gFmodSilent = true; - return false; - } - gFmodSpec = obtained; - SDL_PauseAudioDevice(gFmodDevice, 0); - return true; -} - -static void FmodMakeSine(std::vector& out, int rate, int ch, float freqHz, float seconds) { - const size_t frames = static_cast(rate * seconds); - out.resize(frames * static_cast(ch)); - const float phaseInc = 2.0f * 3.14159265358979323846f * freqHz / static_cast(rate); - float phase = 0.0f; - for (size_t f = 0; f < frames; f++) { - const float s = std::sin(phase) * 0.25f; - phase += phaseInc; - for (int c = 0; c < ch; c++) out[f * static_cast(ch) + static_cast(c)] = s; - } -} - -// Minimal WAV (RIFF/WAVE) parser -> float stereo 48k. Returns false if not WAV. -static bool FmodParseWav(const uint8_t* data, size_t len, std::vector& outPcm, int& outRate, int& outCh) { - if (len < 44 || std::memcmp(data, "RIFF", 4) != 0 || std::memcmp(data + 8, "WAVE", 4) != 0) return false; - uint16_t audioFmt = 0, numCh = 0, bitsPerSample = 0; - uint32_t sampleRate = 0; - const uint8_t* snd = nullptr; - size_t sndLen = 0; - size_t off = 12; - auto rd16 = [](const uint8_t* p) -> uint16_t { return static_cast(p[0] | (p[1] << 8)); }; - auto rd32 = [](const uint8_t* p) -> uint32_t { return static_cast(p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24)); }; - while (off + 8 <= len) { - const uint8_t* chunk = data + off; - uint32_t chunkLen = rd32(chunk + 4); - if (off + 8 + chunkLen > len) break; - if (std::memcmp(chunk, "fmt ", 4) == 0 && chunkLen >= 16) { - audioFmt = rd16(chunk + 8); - numCh = rd16(chunk + 10); - sampleRate = rd32(chunk + 12); - bitsPerSample = rd16(chunk + 22); - } else if (std::memcmp(chunk, "data", 4) == 0) { - snd = chunk + 8; - sndLen = chunkLen; - } - off += 8 + ((chunkLen + 1) & ~static_cast(1)); - } - if (!snd || sndLen == 0 || numCh == 0 || sampleRate == 0) return false; - if (audioFmt != 1 && audioFmt != 3) return false; // PCM or FLOAT - int bytesPerSamp = bitsPerSample / 8; - if (bytesPerSamp <= 0) return false; - size_t frames = sndLen / (static_cast(numCh) * static_cast(bytesPerSamp)); - if (frames == 0) return false; - if (frames > 48000u * 30u) frames = 48000u * 30u; // cap 30s - std::vector tmp; - tmp.reserve(frames * 2); - for (size_t f = 0; f < frames; f++) { - float l = 0.0f, r = 0.0f; - for (int c = 0; c < numCh && c < 8; c++) { - const uint8_t* p = snd + (f * numCh + c) * bytesPerSamp; - float s = 0.0f; - if (audioFmt == 1) { - if (bitsPerSample == 8) s = (static_cast(p[0]) - 128) / 128.0f; - else if (bitsPerSample == 16) { int16_t v; std::memcpy(&v, p, 2); s = v / 32768.0f; } - else if (bitsPerSample == 24) { int32_t v = (p[0] | (p[1] << 8) | (p[2] << 16)); if (v & 0x800000) v |= ~0xFFFFFF; s = v / 8388608.0f; } - else if (bitsPerSample == 32) { int32_t v; std::memcpy(&v, p, 4); s = v / 2147483648.0f; } - else return false; - } else { - if (bitsPerSample == 32) { float v; std::memcpy(&v, p, 4); s = v; } - else return false; - } - if (c == 0) l = s; - else if (c == 1) r = s; - else { l += s; r += s; } - } - if (numCh == 1) r = l; - tmp.push_back(l); - tmp.push_back(r); - } - // Resample to 48k if needed (linear). - if (sampleRate != static_cast(kFmodOutRate)) { - const size_t inFrames = tmp.size() / 2; - const size_t outFrames = static_cast((static_cast(inFrames) * kFmodOutRate) / sampleRate); - std::vector rs; - rs.reserve(outFrames * 2); - for (size_t i = 0; i < outFrames; i++) { - double pos = (static_cast(i) * sampleRate) / kFmodOutRate; - size_t i0 = static_cast(pos); - size_t i1 = i0 + 1 < inFrames ? i0 + 1 : i0; - float t = static_cast(pos - i0); - rs.push_back(tmp[i0 * 2] * (1 - t) + tmp[i1 * 2] * t); - rs.push_back(tmp[i0 * 2 + 1] * (1 - t) + tmp[i1 * 2 + 1] * t); - } - outPcm.swap(rs); - } else { - outPcm.swap(tmp); - } - outRate = kFmodOutRate; - outCh = 2; - return true; -} - -} - -namespace FMOD { - -class System; -class ChannelGroup; -class Channel; -class Sound; -class SoundGroup; -class DSP; -class DSPConnection; -class Geometry; -class Reverb3D; - -class Sound { -public: - std::string name; - unsigned mode = 0; - unsigned length = 0; - int loopCount = 0; - float minDistance = 0.0f; - float maxDistance = 10000.0f; - SoundGroup* soundGroup = nullptr; - void* userData = nullptr; - // Real-audio decoded PCM (float stereo 48k, interleaved). Empty = silent/placeholder. - std::vector pcm; - int pcmRate = kFmodOutRate; - int pcmCh = 2; - int APS5_VABI release(); - int APS5_VABI getSystemObject(System** system); - int APS5_VABI setMode(unsigned mode) { - std::lock_guard lock(gFmodMutex); - this->mode = mode; - return FMOD_OK; - } - int APS5_VABI getMode(unsigned* mode) { - std::lock_guard lock(gFmodMutex); - if (mode) *mode = this->mode; - return FMOD_OK; - } - int APS5_VABI set3DMinMaxDistance(float mindistance, float maxdistance) { - std::lock_guard lock(gFmodMutex); - minDistance = mindistance; - maxDistance = maxdistance; - return FMOD_OK; - } - int APS5_VABI getNumSubSounds(int* numsubsounds) { - std::lock_guard lock(gFmodMutex); - if (numsubsounds) *numsubsounds = 0; - return FMOD_OK; - } - int APS5_VABI getLength(unsigned* length, unsigned lengthtype) { - std::lock_guard lock(gFmodMutex); - if (!length) return FMOD_OK; - if (!pcm.empty()) { - const unsigned frames = static_cast(pcm.size() / 2); - // FMOD_TIMEUNIT: MS=1, PCM=2, PCMBYTES=4, RAWBYTES=8 - if (lengthtype == 1) *length = (frames * 1000u) / static_cast(kFmodOutRate); - else if (lengthtype == 2) *length = frames; - else if (lengthtype == 4 || lengthtype == 8) *length = frames * 2u * sizeof(float); - else *length = frames; - return FMOD_OK; - } - *length = this->length; - return FMOD_OK; - } - int APS5_VABI getFormat(FMOD_SOUND_TYPE* type, FMOD_SOUND_FORMAT* format, int* channels, int* bits) { - std::lock_guard lock(gFmodMutex); - if (!pcm.empty()) { - if (type) *type = FMOD_SOUND_TYPE_UNKNOWN; - if (format) *format = FMOD_SOUND_FORMAT_PCMFLOAT; - if (channels) *channels = 2; - if (bits) *bits = 32; - return FMOD_OK; - } - if (type) *type = FMOD_SOUND_TYPE_UNKNOWN; - if (format) *format = FMOD_SOUND_FORMAT_PCM16; - if (channels) *channels = 2; - if (bits) *bits = 16; - return FMOD_OK; - } - int APS5_VABI getName(char* name, int namelen) { - std::lock_guard lock(gFmodMutex); - if (name && namelen > 0) { - std::strncpy(name, this->name.c_str(), static_cast(namelen - 1)); - name[namelen - 1] = '\0'; - } - return FMOD_OK; - } - int APS5_VABI setLoopCount(int loopcount) { - std::lock_guard lock(gFmodMutex); - loopCount = loopcount; - return FMOD_OK; - } - int APS5_VABI getLoopCount(int* loopcount) { - std::lock_guard lock(gFmodMutex); - if (loopcount) *loopcount = loopCount; - return FMOD_OK; - } - int APS5_VABI setSoundGroup(SoundGroup* soundgroup) { - std::lock_guard lock(gFmodMutex); - soundGroup = soundgroup; - return FMOD_OK; - } - int APS5_VABI getSoundGroup(SoundGroup** soundgroup) { - std::lock_guard lock(gFmodMutex); - if (soundgroup) *soundgroup = soundGroup; - return FMOD_OK; - } - int APS5_VABI getSubSound(int index, Sound** subsound) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (subsound) *subsound = nullptr; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI getOpenState(FMOD_OPENSTATE* openstate, unsigned* percentbuffered, bool* starving, bool* diskbusy) { - std::lock_guard lock(gFmodMutex); - if (openstate) *openstate = FMOD_OPENSTATE_READY; - if (percentbuffered) *percentbuffered = 100; - if (starving) *starving = false; - if (diskbusy) *diskbusy = false; - return FMOD_OK; - } - int APS5_VABI readData(void* buffer, unsigned length, unsigned* read) { - std::lock_guard lock(gFmodMutex); - if (buffer && length) std::memset(buffer, 0, length); - if (read) *read = length; - return FMOD_OK; - } - int APS5_VABI seekData(unsigned pcm) { - (void)pcm; - return FMOD_OK; - } -}; - -class SoundGroup { -public: - int maxAudible = 0; - float volume = 1.0f; - void* userData = nullptr; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI setMaxAudible(int maxaudible) { - std::lock_guard lock(gFmodMutex); - maxAudible = maxaudible; - return FMOD_OK; - } - int APS5_VABI getMaxAudible(int* maxaudible) { - std::lock_guard lock(gFmodMutex); - if (maxaudible) *maxaudible = maxAudible; - return FMOD_OK; - } - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gFmodMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI getVolume(float* volume) { - std::lock_guard lock(gFmodMutex); - if (volume) *volume = this->volume; - return FMOD_OK; - } - int APS5_VABI stop() { return FMOD_OK; } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI getNumSounds(int* numsounds) { - std::lock_guard lock(gFmodMutex); - if (numsounds) *numsounds = 0; - return FMOD_OK; - } -}; - -class DSPConnection { -public: - DSP* input = nullptr; - DSP* output = nullptr; - void* userData = nullptr; - int APS5_VABI getInput(DSP** input) { - std::lock_guard lock(gFmodMutex); - if (input) *input = this->input; - return FMOD_OK; - } - int APS5_VABI getOutput(DSP** output) { - std::lock_guard lock(gFmodMutex); - if (output) *output = this->output; - return FMOD_OK; - } - int APS5_VABI getType(FMOD_DSPCONNECTION_TYPE* type) { - std::lock_guard lock(gFmodMutex); - if (type) *type = FMOD_DSPCONNECTION_TYPE_STANDARD; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class DSP { -public: - FMOD_DSP_TYPE type = FMOD_DSP_TYPE_UNKNOWN; - bool active = true; - bool bypass = false; - void* userData = nullptr; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI setActive(bool active) { - std::lock_guard lock(gFmodMutex); - this->active = active; - return FMOD_OK; - } - int APS5_VABI getActive(bool* active) { - std::lock_guard lock(gFmodMutex); - if (active) *active = this->active; - return FMOD_OK; - } - int APS5_VABI setBypass(bool bypass) { - std::lock_guard lock(gFmodMutex); - this->bypass = bypass; - return FMOD_OK; - } - int APS5_VABI getBypass(bool* bypass) { - std::lock_guard lock(gFmodMutex); - if (bypass) *bypass = this->bypass; - return FMOD_OK; - } - int APS5_VABI reset() { return FMOD_OK; } - int APS5_VABI setParameterFloat(int index, float value) { - (void)index; (void)value; - return FMOD_OK; - } - int APS5_VABI getParameterFloat(int index, float* value) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (value) *value = 0.0f; - return FMOD_OK; - } - int APS5_VABI getType(FMOD_DSP_TYPE* type) { - std::lock_guard lock(gFmodMutex); - if (type) *type = this->type; - return FMOD_OK; - } - int APS5_VABI getInfo(char* name, unsigned* version, int* channels, int* configwidth, int* configheight) { - std::lock_guard lock(gFmodMutex); - if (name) name[0] = '\0'; - if (version) *version = 0; - if (channels) *channels = 0; - if (configwidth) *configwidth = 0; - if (configheight) *configheight = 0; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class ChannelControl { -public: - float volume = 1.0f; - float pitch = 1.0f; - bool paused = false; - bool mute = false; - bool playing = false; - unsigned mode = 0; - bool volumeRamp = false; - void* userData = nullptr; - // Mixer state for real-audio backend (used when this control is a Channel - // with decoded PCM; groups keep mixHasData==false and use sticky flag). - size_t mixCursor = 0; - size_t mixTotal = 0; - bool mixHasData = false; - bool mixLoop = false; - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gFmodMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI getVolume(float* volume) { - std::lock_guard lock(gFmodMutex); - if (volume) *volume = this->volume; - return FMOD_OK; - } - int APS5_VABI setPitch(float pitch) { - std::lock_guard lock(gFmodMutex); - this->pitch = pitch; - return FMOD_OK; - } - int APS5_VABI getPitch(float* pitch) { - std::lock_guard lock(gFmodMutex); - if (pitch) *pitch = this->pitch; - return FMOD_OK; - } - int APS5_VABI setPaused(bool paused) { - std::lock_guard lock(gFmodMutex); - this->paused = paused; - return FMOD_OK; - } - int APS5_VABI getPaused(bool* paused) { - std::lock_guard lock(gFmodMutex); - if (paused) *paused = this->paused; - return FMOD_OK; - } - int APS5_VABI setMute(bool mute) { - std::lock_guard lock(gFmodMutex); - this->mute = mute; - return FMOD_OK; - } - int APS5_VABI getMute(bool* mute) { - std::lock_guard lock(gFmodMutex); - if (mute) *mute = this->mute; - return FMOD_OK; - } - int APS5_VABI stop() { - std::lock_guard lock(gFmodMutex); - playing = false; - return FMOD_OK; - } - int APS5_VABI isPlaying(bool* isplaying) { - std::lock_guard lock(gFmodMutex); - if (!isplaying) return FMOD_OK; - if (mixHasData) { - *isplaying = playing && !paused && (mixLoop || mixCursor < mixTotal); - } else { - *isplaying = playing && !paused; - } - return FMOD_OK; - } - int APS5_VABI setMode(unsigned mode) { - std::lock_guard lock(gFmodMutex); - this->mode = mode; - return FMOD_OK; - } - int APS5_VABI getMode(unsigned* mode) { - std::lock_guard lock(gFmodMutex); - if (mode) *mode = this->mode; - return FMOD_OK; - } - int APS5_VABI addFadePoint(unsigned long long dspclock, float volume) { - (void)dspclock; (void)volume; - return FMOD_OK; - } - int APS5_VABI setDelay(unsigned long long dspclock_start, unsigned long long dspclock_end, bool stopchannels) { - (void)dspclock_start; (void)dspclock_end; (void)stopchannels; - return FMOD_OK; - } - int APS5_VABI getDelay(unsigned long long* dspclock_start, unsigned long long* dspclock_end, bool* stopchannels) { - std::lock_guard lock(gFmodMutex); - if (dspclock_start) *dspclock_start = 0; - if (dspclock_end) *dspclock_end = 0; - if (stopchannels) *stopchannels = false; - return FMOD_OK; - } - int APS5_VABI setVolumeRamp(bool ramp) { - std::lock_guard lock(gFmodMutex); - volumeRamp = ramp; - return FMOD_OK; - } - int APS5_VABI getVolumeRamp(bool* ramp) { - std::lock_guard lock(gFmodMutex); - if (ramp) *ramp = volumeRamp; - return FMOD_OK; - } - int APS5_VABI setCallback(FMOD_CHANNELCONTROL_CALLBACK callback) { - (void)callback; - return FMOD_OK; - } - int APS5_VABI isVirtual(bool* isvirtual) { - std::lock_guard lock(gFmodMutex); - if (isvirtual) *isvirtual = false; - return FMOD_OK; - } - int APS5_VABI getIndex(int* index) { - std::lock_guard lock(gFmodMutex); - if (index) *index = 0; - return FMOD_OK; - } - int APS5_VABI getSystemObject(System** system); - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI set3DAttributes(const FMOD_VECTOR* pos, const FMOD_VECTOR* vel) { - (void)pos; (void)vel; - return FMOD_OK; - } - int APS5_VABI setLowPassGain(float gain) { - (void)gain; - return FMOD_OK; - } - int APS5_VABI getLowPassGain(float* gain) { - std::lock_guard lock(gFmodMutex); - if (gain) *gain = 1.0f; - return FMOD_OK; - } - int APS5_VABI setPan(float pan) { - (void)pan; - return FMOD_OK; - } - int APS5_VABI getDSPClock(unsigned long long* dspclock, unsigned long long* parentclock) { - std::lock_guard lock(gFmodMutex); - if (dspclock) *dspclock = 0; - if (parentclock) *parentclock = 0; - return FMOD_OK; - } - int APS5_VABI getDSP(int index, DSP** dsp) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (dsp) *dsp = nullptr; - return FMOD_OK; - } -}; - -class Channel : public ChannelControl { -public: - Sound* sound = nullptr; - ChannelGroup* group = nullptr; - float frequency = 48000.0f; - int priority = 128; - unsigned position = 0; - int loopCount = 0; - System* owner = nullptr; - int APS5_VABI setPriority(int priority) { - std::lock_guard lock(gFmodMutex); - this->priority = priority; - return FMOD_OK; - } - int APS5_VABI getPriority(int* priority) { - std::lock_guard lock(gFmodMutex); - if (priority) *priority = this->priority; - return FMOD_OK; - } - int APS5_VABI setFrequency(float frequency) { - std::lock_guard lock(gFmodMutex); - this->frequency = frequency; - return FMOD_OK; - } - int APS5_VABI getFrequency(float* frequency) { - std::lock_guard lock(gFmodMutex); - if (frequency) *frequency = this->frequency; - return FMOD_OK; - } - int APS5_VABI setPosition(unsigned position, unsigned postype) { - (void)postype; - std::lock_guard lock(gFmodMutex); - this->position = position; - // Keep mixer cursor in sync (PCM frames). postype==2 (PCM) assumed. - mixCursor = static_cast(position); - if (mixHasData && mixCursor > mixTotal) mixCursor = mixTotal; - return FMOD_OK; - } - int APS5_VABI getPosition(unsigned* position, unsigned postype) { - (void)postype; - std::lock_guard lock(gFmodMutex); - if (position) *position = static_cast(mixHasData ? mixCursor : this->position); - return FMOD_OK; - } - int APS5_VABI setChannelGroup(ChannelGroup* channelgroup) { - std::lock_guard lock(gFmodMutex); - group = channelgroup; - return FMOD_OK; - } - int APS5_VABI getChannelGroup(ChannelGroup** channelgroup) { - std::lock_guard lock(gFmodMutex); - if (channelgroup) *channelgroup = group; - return FMOD_OK; - } - int APS5_VABI setLoopCount(int loopcount) { - std::lock_guard lock(gFmodMutex); - loopCount = loopcount; - return FMOD_OK; - } - int APS5_VABI getCurrentSound(Sound** sound) { - std::lock_guard lock(gFmodMutex); - if (sound) *sound = this->sound; - return FMOD_OK; - } -}; - -class ChannelGroup : public ChannelControl { -public: - std::string name; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI addGroup(ChannelGroup* group, bool propagatedspclock, DSPConnection** connection) { - (void)group; (void)propagatedspclock; - std::lock_guard lock(gFmodMutex); - if (connection) *connection = nullptr; - return FMOD_OK; - } - int APS5_VABI getNumGroups(int* numgroups) { - std::lock_guard lock(gFmodMutex); - if (numgroups) *numgroups = 0; - return FMOD_OK; - } - int APS5_VABI getGroup(int index, ChannelGroup** group) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (group) *group = nullptr; - return FMOD_OK; - } - int APS5_VABI getParentGroup(ChannelGroup** group) { - std::lock_guard lock(gFmodMutex); - if (group) *group = nullptr; - return FMOD_OK; - } - int APS5_VABI getName(char* name, int namelen) { - std::lock_guard lock(gFmodMutex); - if (name && namelen > 0) { - std::strncpy(name, this->name.c_str(), static_cast(namelen - 1)); - name[namelen - 1] = '\0'; - } - return FMOD_OK; - } - int APS5_VABI getNumChannels(int* numchannels) { - std::lock_guard lock(gFmodMutex); - if (numchannels) *numchannels = 0; - return FMOD_OK; - } - int APS5_VABI getChannel(int index, Channel** channel) { - (void)index; - std::lock_guard lock(gFmodMutex); - if (channel) *channel = nullptr; - return FMOD_OK; - } -}; - -class Geometry { -public: - void* userData = nullptr; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI addPolygon(float directocclusion, float reverbocclusion, bool doublesided, int numvertices, const FMOD_VECTOR* vertices, int* polygonindex) { - (void)directocclusion; (void)reverbocclusion; (void)doublesided; (void)numvertices; (void)vertices; - std::lock_guard lock(gFmodMutex); - if (polygonindex) *polygonindex = 0; - return FMOD_OK; - } - int APS5_VABI setActive(bool active) { - (void)active; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class Reverb3D { -public: - void* userData = nullptr; - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI set3DAttributes(const FMOD_VECTOR* pos, float mindistance, float maxdistance) { - (void)pos; (void)mindistance; (void)maxdistance; - return FMOD_OK; - } - int APS5_VABI setActive(bool active) { - (void)active; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -static std::vector gFmodChannels; - -static int APS5_VABI SoundReleaseImpl(Sound* self) { - std::lock_guard lock(gFmodMutex); - for (Channel* ch : gFmodChannels) { - if (ch && ch->sound == self) { - ch->playing = false; - ch->sound = nullptr; - ch->mixHasData = false; - } - } - delete self; - return FMOD_OK; -} - -int APS5_VABI Sound::release() { return SoundReleaseImpl(this); } - -class System { -public: - bool initialized = false; - int maxChannels = 0; - unsigned initFlags = 0; - int driver = 0; - FMOD_OUTPUTTYPE output = FMOD_OUTPUTTYPE_AUTODETECT; - int softwareChannels = 0; - int numListeners = 1; - float dopplerScale = 1.0f; - float distanceFactor = 1.0f; - float rolloffScale = 1.0f; - void* userData = nullptr; - ChannelGroup* masterChannelGroup = nullptr; - SoundGroup* masterSoundGroup = nullptr; - static int APS5_VABI create(System** system) { - std::lock_guard lock(gFmodMutex); - if (!system) return FMOD_OK; - *system = new System(); - return FMOD_OK; - } - static int APS5_VABI create(System** system, unsigned headerversion) { - (void)headerversion; - return create(system); - } - int APS5_VABI release() { - std::lock_guard lock(gFmodMutex); - // Drop channels owned by this system (game holds raw pointers; mark - // them stopped and free to avoid unbounded growth). - for (auto it = gFmodChannels.begin(); it != gFmodChannels.end();) { - if (*it && (*it)->owner == this) { - delete *it; - it = gFmodChannels.erase(it); - } else { - ++it; - } - } - delete masterChannelGroup; - delete masterSoundGroup; - masterChannelGroup = nullptr; - masterSoundGroup = nullptr; - delete this; - return FMOD_OK; - } - int APS5_VABI init(int maxchannels, unsigned flags, void* extradriverdata) { - (void)extradriverdata; - std::lock_guard lock(gFmodMutex); - maxChannels = maxchannels; - initFlags = flags; - initialized = true; - if (!masterChannelGroup) masterChannelGroup = new ChannelGroup(); - if (!masterSoundGroup) masterSoundGroup = new SoundGroup(); - // Real-audio: try SDL unless NOSOUND output or env requests silent. - // Keep silent backend as fallback (no failure returned). - bool wantSilent = FmodSilentRequested() || - output == FMOD_OUTPUTTYPE_NOSOUND || - output == FMOD_OUTPUTTYPE_NOSOUND_NRT; - if (!wantSilent) { - // Unlock during SDL open (SDL may call back)? FmodEnsureDevice uses - // no FMOD lock internally except SDL, so we can call with lock held - // for simplicity; SDL_OpenAudioDevice does not call into FMOD. - if (!FmodEnsureDevice()) { - APS5_LOG_ERR("%s", "FMOD: SDL audio unavailable, silent fallback"); - } else { - APS5_LOG_OUT("%s", "FMOD: SDL audio output active (48k stereo)"); - } - } - return FMOD_OK; - } - int APS5_VABI close() { - std::lock_guard lock(gFmodMutex); - initialized = false; - return FMOD_OK; - } - int APS5_VABI update() { - std::lock_guard lock(gFmodMutex); - if (!initialized) return FMOD_OK; - if (gFmodSilent || gFmodDevice == 0) return FMOD_OK; // silent fallback - // Throttle like libSceAudioOut: avoid unbounded queue growth. - Uint32 queued = SDL_GetQueuedAudioSize(gFmodDevice); - const Uint32 oneBuf = static_cast(kFmodMixFrames * kFmodOutCh * sizeof(float)); - if (queued > oneBuf * 8u) return FMOD_OK; - float mix[kFmodMixFrames * 2] = {}; - bool anyActive = false; - for (Channel* ch : gFmodChannels) { - if (!ch || ch->owner != this) continue; - if (!ch->playing || ch->paused || ch->mute) continue; - if (!ch->mixHasData || !ch->sound || ch->sound->pcm.empty()) continue; - Sound* s = ch->sound; - const size_t total = ch->mixTotal; - if (total == 0) continue; - size_t cur = ch->mixCursor; - const float vol = ch->volume; - if (vol <= 0.0f) { - // Still advance cursor so silent channels finish. - size_t adv = kFmodMixFrames; - if (!ch->mixLoop) { - if (cur + adv >= total) { cur = total; ch->playing = false; } - else cur += adv; - } else { - cur = (cur + adv) % (total ? total : 1); - } - ch->mixCursor = cur; - ch->position = static_cast(cur); - continue; - } - anyActive = true; - const float* pcm = s->pcm.data(); - size_t pos = cur; - for (int i = 0; i < kFmodMixFrames; i++) { - if (pos >= total) { - if (ch->mixLoop || s->loopCount != 0 || - (s->mode & FMOD_LOOP_NORMAL) != 0) { - pos = 0; - } else { - break; - } - } - mix[i * 2] += pcm[pos * 2] * vol; - mix[i * 2 + 1] += pcm[pos * 2 + 1] * vol; - pos++; - } - // Advance cursor by frames consumed (approx, ignoring loop wrap details above). - size_t adv = static_cast(kFmodMixFrames); - if (!ch->mixLoop && s->loopCount == 0 && (s->mode & FMOD_LOOP_NORMAL) == 0) { - if (cur + adv >= total) { - cur = total; - ch->playing = false; - } else { - cur += adv; - } - } else { - cur = total ? ((cur + adv) % total) : 0; - } - ch->mixCursor = cur; - ch->position = static_cast(cur); - } - if (!anyActive) { - // Still queue silence rarely to keep device alive? No — skip to - // avoid filling queue with silence when nothing plays. - return FMOD_OK; - } - for (int i = 0; i < kFmodMixFrames * 2; i++) { - if (mix[i] > 1.0f) mix[i] = 1.0f; - else if (mix[i] < -1.0f) mix[i] = -1.0f; - } - // Convert to device format if needed. - if (gFmodSpec.format == AUDIO_F32SYS || gFmodSpec.format == 0) { - if (SDL_QueueAudio(gFmodDevice, mix, sizeof(mix)) < 0) { - APS5_LOG_ERR("%s", "FMOD: SDL_QueueAudio failed"); - } - } else { - int16_t tmp[kFmodMixFrames * 2]; - for (int i = 0; i < kFmodMixFrames * 2; i++) { - float v = mix[i] * 32767.0f; - if (v > 32767.0f) v = 32767.0f; - if (v < -32768.0f) v = -32768.0f; - tmp[i] = static_cast(v); - } - // Resample/format convert via SDL if spec differs (channels/rate). - // For minimal path, queue directly when spec is S16 stereo 48k; - // otherwise use SDL_ConvertAudio. - if (gFmodSpec.channels == 2 && gFmodSpec.freq == kFmodOutRate && - (gFmodSpec.format == AUDIO_S16SYS)) { - SDL_QueueAudio(gFmodDevice, tmp, sizeof(tmp)); - } else { - SDL_AudioCVT cvt{}; - if (SDL_BuildAudioCVT(&cvt, AUDIO_F32SYS, 2, kFmodOutRate, - gFmodSpec.format, gFmodSpec.channels, gFmodSpec.freq) >= 0) { - std::vector buf(sizeof(mix) * 4u); - std::memcpy(buf.data(), mix, sizeof(mix)); - cvt.buf = buf.data(); - cvt.len = static_cast(sizeof(mix)); - if (SDL_ConvertAudio(&cvt) == 0) { - SDL_QueueAudio(gFmodDevice, cvt.buf, static_cast(cvt.len_cvt)); - } - } - } - } - return FMOD_OK; - } - int APS5_VABI setOutput(FMOD_OUTPUTTYPE output) { - std::lock_guard lock(gFmodMutex); - this->output = output; - if (output == FMOD_OUTPUTTYPE_NOSOUND || output == FMOD_OUTPUTTYPE_NOSOUND_NRT) { - gFmodSilent = true; - } - return FMOD_OK; - } - int APS5_VABI getOutput(FMOD_OUTPUTTYPE* output) { - std::lock_guard lock(gFmodMutex); - if (output) *output = this->output; - return FMOD_OK; - } - int APS5_VABI getNumDrivers(int* numdrivers) { - std::lock_guard lock(gFmodMutex); - if (numdrivers) *numdrivers = 1; - return FMOD_OK; - } - int APS5_VABI getDriverInfo(int id, char* name, int namelen, FMOD_GUID* guid, int* systemrate, FMOD_SPEAKERMODE* speakermode, int* speakermodechannels) { - (void)id; - std::lock_guard lock(gFmodMutex); - if (name && namelen > 0) { - const char* driverName = (gFmodDevice != 0 && !gFmodSilent) ? "AnyPS5 FMOD Output" : "AnyPS5 Null Output"; - std::strncpy(name, driverName, static_cast(namelen - 1)); - name[namelen - 1] = '\0'; - } - if (guid) std::memset(guid, 0, sizeof(*guid)); - if (systemrate) *systemrate = 48000; - if (speakermode) *speakermode = FMOD_SPEAKERMODE_STEREO; - if (speakermodechannels) *speakermodechannels = 2; - return FMOD_OK; - } - int APS5_VABI setDriver(int driver) { - std::lock_guard lock(gFmodMutex); - this->driver = driver; - return FMOD_OK; - } - int APS5_VABI getDriver(int* driver) { - std::lock_guard lock(gFmodMutex); - if (driver) *driver = this->driver; - return FMOD_OK; - } - int APS5_VABI setSoftwareChannels(int numsoftwarechannels) { - std::lock_guard lock(gFmodMutex); - softwareChannels = numsoftwarechannels; - return FMOD_OK; - } - int APS5_VABI getSoftwareChannels(int* numsoftwarechannels) { - std::lock_guard lock(gFmodMutex); - if (numsoftwarechannels) *numsoftwarechannels = softwareChannels; - return FMOD_OK; - } - int APS5_VABI setSoftwareFormat(int samplerate, FMOD_SPEAKERMODE speakermode, int numrawspeakers) { - (void)samplerate; (void)speakermode; (void)numrawspeakers; - return FMOD_OK; - } - int APS5_VABI getSoftwareFormat(int* samplerate, FMOD_SPEAKERMODE* speakermode, int* numrawspeakers) { - std::lock_guard lock(gFmodMutex); - if (samplerate) *samplerate = 48000; - if (speakermode) *speakermode = FMOD_SPEAKERMODE_STEREO; - if (numrawspeakers) *numrawspeakers = 0; - return FMOD_OK; - } - int APS5_VABI setDSPBufferSize(unsigned bufferlength, int numbuffers) { - (void)bufferlength; (void)numbuffers; - return FMOD_OK; - } - int APS5_VABI getDSPBufferSize(unsigned* bufferlength, int* numbuffers) { - std::lock_guard lock(gFmodMutex); - if (bufferlength) *bufferlength = 1024; - if (numbuffers) *numbuffers = 4; - return FMOD_OK; - } - int APS5_VABI setAdvancedSettings(FMOD_ADVANCEDSETTINGS* settings) { - (void)settings; - return FMOD_OK; - } - int APS5_VABI getAdvancedSettings(FMOD_ADVANCEDSETTINGS* settings) { - std::lock_guard lock(gFmodMutex); - if (settings) std::memset(settings, 0, sizeof(*settings)); - return FMOD_OK; - } - int APS5_VABI setCallback(FMOD_SYSTEM_CALLBACK callback, FMOD_SYSTEM_CALLBACK_TYPE callbackmask) { - (void)callback; (void)callbackmask; - return FMOD_OK; - } - int APS5_VABI setFileSystem(void* useropen, void* userclose, void* userread, void* userseek, void* userasyncread, void* userasynccancel, int blockalign) { - (void)useropen; (void)userclose; (void)userread; (void)userseek; - (void)userasyncread; (void)userasynccancel; (void)blockalign; - return FMOD_OK; - } - int APS5_VABI setPluginPath(const char* pluginpath) { - (void)pluginpath; - return FMOD_OK; - } - int APS5_VABI loadPlugin(const char* filename, unsigned* handle, FMOD_PLUGINTYPE plugintype) { - (void)filename; (void)plugintype; - std::lock_guard lock(gFmodMutex); - if (handle) *handle = 0; - return FMOD_OK; - } - int APS5_VABI getVersion(unsigned* version) { - std::lock_guard lock(gFmodMutex); - if (version) *version = FMOD_VERSION_CURRENT; - return FMOD_OK; - } - int APS5_VABI getOutputHandle(void** handle) { - std::lock_guard lock(gFmodMutex); - if (handle) *handle = nullptr; - return FMOD_OK; - } - int APS5_VABI getChannelsPlaying(int* channels, int* realchannels) { - std::lock_guard lock(gFmodMutex); - int active = 0; - for (Channel* ch : gFmodChannels) { - if (!ch || ch->owner != this) continue; - if (!ch->playing || ch->paused) continue; - if (ch->mixHasData) { - if (ch->mixLoop || ch->mixCursor < ch->mixTotal) active++; - } - // Null/empty sounds (smoke test) are not counted to preserve old - // behaviour (previously always returned 0). - } - if (channels) *channels = active; - if (realchannels) *realchannels = active; - return FMOD_OK; - } - int APS5_VABI getCPUUsage(FMOD_CPU_USAGE* usage) { - std::lock_guard lock(gFmodMutex); - if (usage) std::memset(usage, 0, sizeof(*usage)); - return FMOD_OK; - } - int APS5_VABI getFileUsage(long long* samplebytesread, long long* streambytesread, long long* otherbytesread) { - std::lock_guard lock(gFmodMutex); - if (samplebytesread) *samplebytesread = 0; - if (streambytesread) *streambytesread = 0; - if (otherbytesread) *otherbytesread = 0; - return FMOD_OK; - } - int APS5_VABI getSpectrum(float* spectrumarray, int numvalues, int channeloffset, FMOD_DSP_FFT_WINDOW window) { - (void)channeloffset; (void)window; - std::lock_guard lock(gFmodMutex); - if (spectrumarray && numvalues > 0) std::memset(spectrumarray, 0, sizeof(float) * static_cast(numvalues)); - return FMOD_OK; - } - int APS5_VABI getWaveData(float* wavearray, int numvalues, int channeloffset) { - (void)channeloffset; - std::lock_guard lock(gFmodMutex); - if (wavearray && numvalues > 0) std::memset(wavearray, 0, sizeof(float) * static_cast(numvalues)); - return FMOD_OK; - } - int APS5_VABI createSound(const char* name_or_data, unsigned mode, FMOD_CREATESOUNDEXINFO* exinfo, Sound** sound) { - std::lock_guard lock(gFmodMutex); - if (!sound) return FMOD_OK; - Sound* created = new Sound(); - created->mode = mode; - unsigned exLen = exinfo ? exinfo->length : 0; - if (exLen) created->length = exLen; - const bool isMem = (mode & (FMOD_OPENMEMORY_FLAG | FMOD_OPENMEMORY_POINT_FLAG)) != 0; - const bool isRaw = (mode & FMOD_OPENRAW_FLAG) != 0; - if (isMem && name_or_data && exLen > 0 && exLen < (1u << 30)) { - const uint8_t* buf = reinterpret_cast(name_or_data); - // Keep a copy of the name only for file-based sounds; for memory - // buffers the pointer is binary data (not a C string). - created->name.clear(); - std::vector pcm; - int rate = kFmodOutRate, ch = 2; - bool decoded = false; - if (!isRaw) { - decoded = FmodParseWav(buf, exLen, pcm, rate, ch); - } - if (!decoded && isRaw) { - // OPENRAW: interpret buffer using exinfo (default stereo 48k PCM16). - int rawCh = exinfo ? exinfo->numchannels : 0; - int rawRate = exinfo ? exinfo->defaultfrequency : 0; - int rawFmt = exinfo ? static_cast(exinfo->format) : 2; - if (rawCh <= 0 || rawCh > 8) rawCh = 2; - if (rawRate <= 0) rawRate = kFmodOutRate; - size_t bytesPerSamp = 2; - if (rawFmt == 1) bytesPerSamp = 1; // PCM8 - else if (rawFmt == 2) bytesPerSamp = 2; // PCM16 - else if (rawFmt == 5) bytesPerSamp = 4; // FLOAT - else bytesPerSamp = 2; - size_t frames = exLen / (static_cast(rawCh) * bytesPerSamp); - if (frames > 0 && frames < 48000u * 60u) { - pcm.reserve(frames * 2); - for (size_t f = 0; f < frames; f++) { - float l = 0, r = 0; - for (int c = 0; c < rawCh; c++) { - const uint8_t* p = buf + (f * rawCh + c) * bytesPerSamp; - float s = 0; - if (rawFmt == 1) s = (static_cast(p[0]) - 128) / 128.0f; - else if (rawFmt == 2) { int16_t v; std::memcpy(&v, p, 2); s = v / 32768.0f; } - else if (rawFmt == 5) { float v; std::memcpy(&v, p, 4); s = v; } - if (c == 0) l = s; else if (c == 1) r = s; else { l += s; r += s; } - } - if (rawCh == 1) r = l; - pcm.push_back(l); pcm.push_back(r); - } - // Resample if needed. - if (rawRate != kFmodOutRate && !pcm.empty()) { - size_t inF = pcm.size() / 2; - size_t outF = static_cast((static_cast(inF) * kFmodOutRate) / rawRate); - std::vector rs; rs.reserve(outF * 2); - for (size_t i = 0; i < outF; i++) { - double pos = (static_cast(i) * rawRate) / kFmodOutRate; - size_t i0 = static_cast(pos); - size_t i1 = i0 + 1 < inF ? i0 + 1 : i0; - float t = static_cast(pos - i0); - rs.push_back(pcm[i0 * 2] * (1 - t) + pcm[i1 * 2] * t); - rs.push_back(pcm[i0 * 2 + 1] * (1 - t) + pcm[i1 * 2 + 1] * t); - } - pcm.swap(rs); - } - decoded = !pcm.empty(); - rate = kFmodOutRate; ch = 2; - } - } - if (decoded) { - created->pcm.swap(pcm); - created->pcmRate = rate; - created->pcmCh = ch; - } else { - // Compressed (Vorbis/MP3/FSB/AT9...) or unknown: audible - // placeholder so the mixer path can be heard. Real decoders - // (Vorbis/MP3/AT9) remain future work — see docs/dev/TechnicalDebt. - FmodMakeSine(created->pcm, kFmodOutRate, 2, 440.0f, 1.0f); - created->pcmRate = kFmodOutRate; - created->pcmCh = 2; - } - } else { - if (name_or_data) { - // File-based: store name only (first bytes may still be checked - // for embedded RIFF in case game passes a path that is data). - size_t nlen = std::strlen(name_or_data); - if (nlen < 256) created->name = name_or_data; - } - // Placeholder 0.5s sine so file-based playSound is audible. - FmodMakeSine(created->pcm, kFmodOutRate, 2, 330.0f, 0.5f); - created->pcmRate = kFmodOutRate; - created->pcmCh = 2; - } - *sound = created; - return FMOD_OK; - } - int APS5_VABI createStream(const char* name_or_data, unsigned mode, FMOD_CREATESOUNDEXINFO* exinfo, Sound** sound) { - return createSound(name_or_data, mode, exinfo, sound); - } - int APS5_VABI createDSP(FMOD_DSP_DESCRIPTION* description, DSP** dsp) { - (void)description; - std::lock_guard lock(gFmodMutex); - if (dsp) *dsp = new DSP(); - return FMOD_OK; - } - int APS5_VABI createDSPByType(FMOD_DSP_TYPE type, DSP** dsp) { - std::lock_guard lock(gFmodMutex); - if (dsp) { - *dsp = new DSP(); - (*dsp)->type = type; - } - return FMOD_OK; - } - int APS5_VABI playSound(Sound* sound, ChannelGroup* channelgroup, bool paused, Channel** channel) { - std::lock_guard lock(gFmodMutex); - if (!channel) return FMOD_OK; - Channel* created = new Channel(); - created->sound = sound; - created->group = channelgroup ? channelgroup : masterChannelGroup; - created->paused = paused; - created->playing = true; - created->owner = this; - created->mixCursor = 0; - if (sound && !sound->pcm.empty()) { - created->mixHasData = true; - created->mixTotal = sound->pcm.size() / 2; - const bool loopMode = (sound->mode & FMOD_LOOP_NORMAL) != 0; - created->mixLoop = loopMode || sound->loopCount != 0; - } else { - created->mixHasData = false; - created->mixTotal = 0; - created->mixLoop = false; - } - gFmodChannels.push_back(created); - *channel = created; - return FMOD_OK; - } - int APS5_VABI playDSP(DSP* dsp, ChannelGroup* channelgroup, bool paused, Channel** channel) { - (void)dsp; - std::lock_guard lock(gFmodMutex); - if (!channel) return FMOD_OK; - Channel* created = new Channel(); - created->group = channelgroup ? channelgroup : masterChannelGroup; - created->paused = paused; - created->playing = true; - created->owner = this; - created->mixHasData = false; - gFmodChannels.push_back(created); - *channel = created; - return FMOD_OK; - } - int APS5_VABI getChannel(int channelid, Channel** channel) { - (void)channelid; - std::lock_guard lock(gFmodMutex); - if (channel) *channel = nullptr; - return FMOD_OK; - } - int APS5_VABI getMasterSoundGroup(SoundGroup** soundgroup) { - std::lock_guard lock(gFmodMutex); - if (soundgroup) *soundgroup = masterSoundGroup; - return FMOD_OK; - } - int APS5_VABI getMasterChannelGroup(ChannelGroup** channelgroup) { - std::lock_guard lock(gFmodMutex); - if (channelgroup) *channelgroup = masterChannelGroup; - return FMOD_OK; - } - int APS5_VABI setReverbProperties(int instance, FMOD_REVERB_PROPERTIES* prop) { - (void)instance; (void)prop; - return FMOD_OK; - } - int APS5_VABI getReverbProperties(int instance, FMOD_REVERB_PROPERTIES* prop) { - (void)instance; - std::lock_guard lock(gFmodMutex); - if (prop) std::memset(prop, 0, sizeof(*prop)); - return FMOD_OK; - } - int APS5_VABI createReverb3D(Reverb3D** reverb) { - std::lock_guard lock(gFmodMutex); - if (reverb) *reverb = new Reverb3D(); - return FMOD_OK; - } - int APS5_VABI createGeometry(int maxpolygons, int maxvertices, Geometry** geometry) { - (void)maxpolygons; (void)maxvertices; - std::lock_guard lock(gFmodMutex); - if (geometry) *geometry = new Geometry(); - return FMOD_OK; - } - int APS5_VABI loadGeometry(const void* data, int datasize, Geometry** geometry) { - (void)data; (void)datasize; - std::lock_guard lock(gFmodMutex); - if (geometry) *geometry = new Geometry(); - return FMOD_OK; - } - int APS5_VABI setGeometrySettings(float maxworldsize) { - (void)maxworldsize; - return FMOD_OK; - } - int APS5_VABI getGeometrySettings(float* maxworldsize) { - std::lock_guard lock(gFmodMutex); - if (maxworldsize) *maxworldsize = 0.0f; - return FMOD_OK; - } - int APS5_VABI set3DNumListeners(int numlisteners) { - std::lock_guard lock(gFmodMutex); - numListeners = numlisteners; - return FMOD_OK; - } - int APS5_VABI get3DNumListeners(int* numlisteners) { - std::lock_guard lock(gFmodMutex); - if (numlisteners) *numlisteners = numListeners; - return FMOD_OK; - } - int APS5_VABI set3DListenerAttributes(int listener, const FMOD_VECTOR* pos, const FMOD_VECTOR* vel, const FMOD_VECTOR* forward, const FMOD_VECTOR* up) { - (void)listener; (void)pos; (void)vel; (void)forward; (void)up; - return FMOD_OK; - } - int APS5_VABI mixerSuspend() { return FMOD_OK; } - int APS5_VABI mixerResume() { return FMOD_OK; } - int APS5_VABI set3DSettings(float dopplerscale, float distancefactor, float rolloffscale) { - std::lock_guard lock(gFmodMutex); - dopplerScale = dopplerscale; - distanceFactor = distancefactor; - rolloffScale = rolloffscale; - return FMOD_OK; - } - int APS5_VABI get3DSettings(float* dopplerscale, float* distancefactor, float* rolloffscale) { - std::lock_guard lock(gFmodMutex); - if (dopplerscale) *dopplerscale = dopplerScale; - if (distancefactor) *distancefactor = distanceFactor; - if (rolloffscale) *rolloffscale = rolloffScale; - return FMOD_OK; - } - int APS5_VABI createChannelGroup(const char* name, ChannelGroup** channelgroup) { - std::lock_guard lock(gFmodMutex); - if (!channelgroup) return FMOD_OK; - *channelgroup = new ChannelGroup(); - if (name) (*channelgroup)->name = name; - return FMOD_OK; - } - int APS5_VABI createSoundGroup(SoundGroup** soundgroup) { - std::lock_guard lock(gFmodMutex); - if (soundgroup) *soundgroup = new SoundGroup(); - return FMOD_OK; - } - int APS5_VABI lockDSP() { return FMOD_OK; } - int APS5_VABI unlockDSP() { return FMOD_OK; } - int APS5_VABI getSoundRAM(int* currentalloced, int* maxalloced, int* total) { - std::lock_guard lock(gFmodMutex); - if (currentalloced) *currentalloced = 0; - if (maxalloced) *maxalloced = 0; - if (total) *total = 0; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gFmodMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gFmodMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI setStreamBufferSize(unsigned filebuffersize, unsigned filebuffersizetype) { - (void)filebuffersize; (void)filebuffersizetype; - return FMOD_OK; - } - int APS5_VABI attachChannelGroupToPort(FMOD_PORT_TYPE porttype, unsigned long long portindex, ChannelGroup* channelgroup) { - (void)porttype; (void)portindex; (void)channelgroup; - return FMOD_OK; - } - int APS5_VABI detachChannelGroupFromPort(ChannelGroup* channelgroup) { - (void)channelgroup; - return FMOD_OK; - } - int APS5_VABI setNetworkProxy(const char* proxy) { - (void)proxy; - return FMOD_OK; - } -}; - -int APS5_VABI Sound::getSystemObject(System** system) { - std::lock_guard lock(gFmodMutex); - if (system) *system = nullptr; - return FMOD_OK; -} - -int APS5_VABI ChannelControl::getSystemObject(System** system) { - std::lock_guard lock(gFmodMutex); - if (system) *system = nullptr; - return FMOD_OK; -} - -} - -extern "C" { - -int APS5_VABI FMOD_System_Create(FMOD_SYSTEM** system) { - std::lock_guard lock(gFmodMutex); - if (!system) return 0; - *system = reinterpret_cast(new FMOD::System()); - return 0; -} - -int APS5_VABI FMOD_Memory_Initialize(void* poolmem, int poollen, void* useralloc, void* userrealloc, void* userfree, unsigned memtypeflags) { - (void)poolmem; (void)poollen; (void)useralloc; (void)userrealloc; (void)userfree; (void)memtypeflags; - return 0; -} - -int APS5_VABI FMOD_Memory_GetStats(int* currentalloced, int* maxalloced, int blocking) { - (void)blocking; - std::lock_guard lock(gFmodMutex); - if (currentalloced) *currentalloced = 0; - if (maxalloced) *maxalloced = 0; - return 0; -} - -namespace { - -// Round2 NID identification via real FMOD headers (const-correct manglings). -// VS1Vg5yOLH0 = FMOD::System::setFileSystem(open,close,read,seek,asyncRead,asyncCancel,blockalign) -// WAU72lnwYic = FMOD::System::setCallback(callback,mask) -// Both are ABI-compatible with void*/int (func ptrs are 8B, mask 4B); we keep -// the silent backend (return OK) — file callbacks are not needed for -// OPENMEMORY sounds used by Legends, and callbacks are stored nowhere. -__attribute__((used)) int APS5_VABI fmod_system_setfilesystem(void* self, void* a1, void* a2, void* a3, void* a4, void* a5, void* a6, int a7) { - (void)self; (void)a1; (void)a2; (void)a3; (void)a4; (void)a5; (void)a6; (void)a7; - return 0; -} -__attribute__((used)) int APS5_VABI fmod_system_setcallback(void* self, void* cb, unsigned mask) { - (void)self; (void)cb; (void)mask; - return 0; -} - -} - -APS5_EXPORT("VS1Vg5yOLH0", fmod_system_setfilesystem); -APS5_EXPORT("WAU72lnwYic", fmod_system_setcallback); - -} diff --git a/core/libs/prx/libfmodstudio/CMakeLists.txt b/core/libs/prx/libfmodstudio/CMakeLists.txt deleted file mode 100644 index 38c8ce484..000000000 --- a/core/libs/prx/libfmodstudio/CMakeLists.txt +++ /dev/null @@ -1,10 +0,0 @@ -add_library(libfmodstudio SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp -) - -target_include_directories(libfmodstudio PRIVATE - ${LIBS_INCLUDE_DIR} -) -target_link_libraries(libfmodstudio PRIVATE libkernel libc) -target_compile_options(libfmodstudio PRIVATE -fkeep-inline-functions) -configure_windows_unwind(libfmodstudio) diff --git a/core/libs/prx/libfmodstudio/Export.cpp b/core/libs/prx/libfmodstudio/Export.cpp deleted file mode 100644 index 8f7baefc3..000000000 --- a/core/libs/prx/libfmodstudio/Export.cpp +++ /dev/null @@ -1,1007 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "SceTypes.hpp" -#include "prx/libc/include/General.hpp" - -enum FMOD_STUDIO_STOP_MODE { - FMOD_STUDIO_STOP_ALLOWFADEOUT = 0, - FMOD_STUDIO_STOP_IMMEDIATE = 1, - FMOD_STUDIO_STOP_MAX = 2 -}; - -enum FMOD_STUDIO_PLAYBACK_STATE { - FMOD_STUDIO_PLAYBACK_PLAYING = 0, - FMOD_STUDIO_PLAYBACK_SUSTAINING = 1, - FMOD_STUDIO_PLAYBACK_STOPPED = 2, - FMOD_STUDIO_PLAYBACK_STARTING = 3, - FMOD_STUDIO_PLAYBACK_STOPPING = 4, - FMOD_STUDIO_PLAYBACK_MAX = 5 -}; - -enum FMOD_STUDIO_LOADING_STATE { - FMOD_STUDIO_LOADING_STATE_UNLOADING = 0, - FMOD_STUDIO_LOADING_STATE_UNLOADED = 1, - FMOD_STUDIO_LOADING_STATE_LOADING = 2, - FMOD_STUDIO_LOADING_STATE_LOADED = 3, - FMOD_STUDIO_LOADING_STATE_ERROR = 4, - FMOD_STUDIO_LOADING_STATE_MAX = 5 -}; - -enum FMOD_STUDIO_EVENT_PROPERTY { - FMOD_STUDIO_EVENT_PROPERTY_CHANNELPRIORITY = 0, - FMOD_STUDIO_EVENT_PROPERTY_SCHEDULE_DELAY = 1, - FMOD_STUDIO_EVENT_PROPERTY_SCHEDULE_LOOKAHEAD = 2, - FMOD_STUDIO_EVENT_PROPERTY_MINIMUM_DISTANCE = 3, - FMOD_STUDIO_EVENT_PROPERTY_MAXIMUM_DISTANCE = 4, - FMOD_STUDIO_EVENT_PROPERTY_MAX = 5 -}; - -enum FMOD_STUDIO_SYSTEM_CALLBACK_TYPE { - FMOD_STUDIO_SYSTEM_CALLBACK_PREUPDATE = 1, - FMOD_STUDIO_SYSTEM_CALLBACK_POSTUPDATE = 2, - FMOD_STUDIO_SYSTEM_CALLBACK_BANK_UNLOAD = 4, - FMOD_STUDIO_SYSTEM_CALLBACK_ALL = 0xFFFFFFFF -}; - -enum FMOD_STUDIO_EVENT_CALLBACK_TYPE { - FMOD_STUDIO_EVENT_CALLBACK_CREATED = 1, - FMOD_STUDIO_EVENT_CALLBACK_DESTROYED = 2, - FMOD_STUDIO_EVENT_CALLBACK_STARTING = 4, - FMOD_STUDIO_EVENT_CALLBACK_STARTED = 8, - FMOD_STUDIO_EVENT_CALLBACK_RESTARTED = 16, - FMOD_STUDIO_EVENT_CALLBACK_STOPPED = 32, - FMOD_STUDIO_EVENT_CALLBACK_START_FAILED = 64, - FMOD_STUDIO_EVENT_CALLBACK_ALL = 0xFFFFFFFF -}; - -struct FMOD_GUID { - unsigned Data1; - unsigned short Data2; - unsigned short Data3; - unsigned char Data4[8]; -}; - -struct FMOD_VECTOR { - float x; - float y; - float z; -}; - -struct FMOD_3D_ATTRIBUTES { - FMOD_VECTOR position; - FMOD_VECTOR velocity; - FMOD_VECTOR forward; - FMOD_VECTOR up; -}; - -struct FMOD_CHANNELGROUP { - int unused; -}; - -struct FMOD_DSP_DESCRIPTION { - unsigned pluginsdkversion; - char name[32]; - unsigned version; -}; - -struct FMOD_STUDIO_PARAMETER_ID { - unsigned data1; - unsigned data2; -}; - -struct FMOD_STUDIO_PARAMETER_DESCRIPTION { - char name[256]; - float minimum; - float maximum; - float defaultvalue; -}; - -struct FMOD_STUDIO_BANK_INFO { - int size; - void* userdata; - int userdatalength; -}; - -struct FMOD_STUDIO_ADVANCEDSETTINGS { - int cbsize; - unsigned commandqueuesize; - unsigned handleinitialsize; -}; - -struct FMOD_STUDIO_CPU_USAGE { - float update; - float nonstreaming; - float streaming; -}; - -struct FMOD_STUDIO_BUFFER_USAGE { - int studiocommandqueue; - int studiohandle; - int values[6]; -}; - -struct FMOD_STUDIO_MEMORY_USAGE { - int exclusive; - int inclusive; - int sampledata; -}; - -struct FMOD_STUDIO_SOUND_INFO { - const char* name_or_data; - unsigned mode; - int subsoundindex; -}; - -struct FMOD_STUDIO_SYSTEM { - int unused; -}; - -struct FMOD_STUDIO_EVENTINSTANCE { - int unused; -}; - -typedef int (*FMOD_STUDIO_SYSTEM_CALLBACK)(FMOD_STUDIO_SYSTEM* system, FMOD_STUDIO_SYSTEM_CALLBACK_TYPE type, void* commanddata, void* userdata); -typedef int (*FMOD_STUDIO_EVENT_CALLBACK)(FMOD_STUDIO_EVENT_CALLBACK_TYPE type, FMOD_STUDIO_EVENTINSTANCE* event, void* parameters); - -namespace { - -std::mutex gStudioMutex; - -constexpr int FMOD_OK = 0; - -} - -namespace FMOD { -namespace Studio { - -class System; -class Bank; -class EventDescription; -class EventInstance; -class Bus; -class VCA; -class CommandReplay; - -class ID : public FMOD_GUID { -}; - -class Bank { -public: - std::string path; - bool sampleDataLoaded = false; - void* userData = nullptr; - bool APS5_VABI isValid() { - return true; - } - int APS5_VABI getID(ID* id) { - std::lock_guard lock(gStudioMutex); - if (id) std::memset(id, 0, sizeof(*id)); - return FMOD_OK; - } - int APS5_VABI getPath(char* path, int size, int* retrieved) { - std::lock_guard lock(gStudioMutex); - if (path && size > 0) { - std::strncpy(path, this->path.c_str(), static_cast(size - 1)); - path[size - 1] = '\0'; - } - if (retrieved) *retrieved = static_cast(this->path.size() + 1); - return FMOD_OK; - } - int APS5_VABI unload() { - std::lock_guard lock(gStudioMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI loadSampleData() { - std::lock_guard lock(gStudioMutex); - sampleDataLoaded = true; - return FMOD_OK; - } - int APS5_VABI unloadSampleData() { - std::lock_guard lock(gStudioMutex); - sampleDataLoaded = false; - return FMOD_OK; - } - int APS5_VABI getLoadingState(FMOD_STUDIO_LOADING_STATE* state) { - std::lock_guard lock(gStudioMutex); - if (state) *state = FMOD_STUDIO_LOADING_STATE_LOADED; - return FMOD_OK; - } - int APS5_VABI getSampleLoadingState(FMOD_STUDIO_LOADING_STATE* state) { - std::lock_guard lock(gStudioMutex); - if (state) *state = sampleDataLoaded ? FMOD_STUDIO_LOADING_STATE_LOADED : FMOD_STUDIO_LOADING_STATE_UNLOADED; - return FMOD_OK; - } - int APS5_VABI getStringCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getEventCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getEventList(EventDescription** array, int capacity, int* count) { - (void)array; (void)capacity; - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getBusCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getBusList(Bus** array, int capacity, int* count) { - (void)array; (void)capacity; - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getVCACount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getVCAList(VCA** array, int capacity, int* count) { - (void)array; (void)capacity; - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class EventDescription { -public: - std::string path; - bool sampleDataLoaded = false; - void* userData = nullptr; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getID(ID* id) { - std::lock_guard lock(gStudioMutex); - if (id) std::memset(id, 0, sizeof(*id)); - return FMOD_OK; - } - int APS5_VABI getPath(char* path, int size, int* retrieved) { - std::lock_guard lock(gStudioMutex); - if (path && size > 0) { - std::strncpy(path, this->path.c_str(), static_cast(size - 1)); - path[size - 1] = '\0'; - } - if (retrieved) *retrieved = static_cast(this->path.size() + 1); - return FMOD_OK; - } - int APS5_VABI getInstanceCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI getInstanceList(EventInstance** array, int capacity, int* count) { - (void)array; (void)capacity; - std::lock_guard lock(gStudioMutex); - if (count) *count = 0; - return FMOD_OK; - } - int APS5_VABI createInstance(EventInstance** instance); - int APS5_VABI loadSampleData() { - std::lock_guard lock(gStudioMutex); - sampleDataLoaded = true; - return FMOD_OK; - } - int APS5_VABI unloadSampleData() { - std::lock_guard lock(gStudioMutex); - sampleDataLoaded = false; - return FMOD_OK; - } - int APS5_VABI getSampleLoadingState(FMOD_STUDIO_LOADING_STATE* state) { - std::lock_guard lock(gStudioMutex); - if (state) *state = sampleDataLoaded ? FMOD_STUDIO_LOADING_STATE_LOADED : FMOD_STUDIO_LOADING_STATE_UNLOADED; - return FMOD_OK; - } - int APS5_VABI getLength(int* length) { - std::lock_guard lock(gStudioMutex); - if (length) *length = 0; - return FMOD_OK; - } - int APS5_VABI isSnapshot(bool* snapshot) { - std::lock_guard lock(gStudioMutex); - if (snapshot) *snapshot = false; - return FMOD_OK; - } - int APS5_VABI isOneshot(bool* oneshot) { - std::lock_guard lock(gStudioMutex); - if (oneshot) *oneshot = false; - return FMOD_OK; - } - int APS5_VABI isStream(bool* isstream) { - std::lock_guard lock(gStudioMutex); - if (isstream) *isstream = false; - return FMOD_OK; - } - int APS5_VABI is3D(bool* is3d) { - std::lock_guard lock(gStudioMutex); - if (is3d) *is3d = false; - return FMOD_OK; - } - int APS5_VABI hasSustainPoint(bool* sustainpoint) { - std::lock_guard lock(gStudioMutex); - if (sustainpoint) *sustainpoint = false; - return FMOD_OK; - } - int APS5_VABI getMinimumDistance(float* distance) { - std::lock_guard lock(gStudioMutex); - if (distance) *distance = 0.0f; - return FMOD_OK; - } - int APS5_VABI getMaximumDistance(float* distance) { - std::lock_guard lock(gStudioMutex); - if (distance) *distance = 10000.0f; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class EventInstance { -public: - EventDescription* description = nullptr; - bool playing = false; - bool paused = false; - float volume = 1.0f; - float pitch = 1.0f; - void* userData = nullptr; - std::unordered_map paramsByName; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getDescription(EventDescription** description) { - std::lock_guard lock(gStudioMutex); - if (description) *description = this->description; - return FMOD_OK; - } - int APS5_VABI getVolume(float* volume, float* finalvolume) { - std::lock_guard lock(gStudioMutex); - if (volume) *volume = this->volume; - if (finalvolume) *finalvolume = this->volume; - return FMOD_OK; - } - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gStudioMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI getPitch(float* pitch, float* finalpitch) { - std::lock_guard lock(gStudioMutex); - if (pitch) *pitch = this->pitch; - if (finalpitch) *finalpitch = this->pitch; - return FMOD_OK; - } - int APS5_VABI setPitch(float pitch) { - std::lock_guard lock(gStudioMutex); - this->pitch = pitch; - return FMOD_OK; - } - int APS5_VABI getPaused(bool* paused) { - std::lock_guard lock(gStudioMutex); - if (paused) *paused = this->paused; - return FMOD_OK; - } - int APS5_VABI setPaused(bool paused) { - std::lock_guard lock(gStudioMutex); - this->paused = paused; - return FMOD_OK; - } - int APS5_VABI start() { - std::lock_guard lock(gStudioMutex); - playing = true; - return FMOD_OK; - } - int APS5_VABI stop(FMOD_STUDIO_STOP_MODE mode) { - (void)mode; - std::lock_guard lock(gStudioMutex); - playing = false; - return FMOD_OK; - } - int APS5_VABI release() { - std::lock_guard lock(gStudioMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI getPlaybackState(FMOD_STUDIO_PLAYBACK_STATE* state) { - std::lock_guard lock(gStudioMutex); - if (state) *state = playing ? FMOD_STUDIO_PLAYBACK_PLAYING : FMOD_STUDIO_PLAYBACK_STOPPED; - return FMOD_OK; - } - int APS5_VABI setTimelinePosition(int position) { - (void)position; - return FMOD_OK; - } - int APS5_VABI getTimelinePosition(int* position) { - std::lock_guard lock(gStudioMutex); - if (position) *position = 0; - return FMOD_OK; - } - int APS5_VABI setParameterByName(const char* name, float value, bool ignoreseekspeed) { - (void)ignoreseekspeed; - std::lock_guard lock(gStudioMutex); - if (name) paramsByName[name] = value; - return FMOD_OK; - } - int APS5_VABI getParameterByName(const char* name, float* value, float* finalvalue) { - std::lock_guard lock(gStudioMutex); - float stored = 0.0f; - if (name) { - auto it = paramsByName.find(name); - if (it != paramsByName.end()) stored = it->second; - } - if (value) *value = stored; - if (finalvalue) *finalvalue = stored; - return FMOD_OK; - } - int APS5_VABI setParameterByID(FMOD_STUDIO_PARAMETER_ID id, float value, bool ignoreseekspeed) { - (void)id; (void)ignoreseekspeed; - return FMOD_OK; - } - int APS5_VABI keyOff() { return FMOD_OK; } - int APS5_VABI setReverbLevel(int index, float level) { - (void)index; (void)level; - return FMOD_OK; - } - int APS5_VABI set3DAttributes(const FMOD_3D_ATTRIBUTES* attributes) { - (void)attributes; - return FMOD_OK; - } - int APS5_VABI setListenerMask(unsigned mask) { - (void)mask; - return FMOD_OK; - } - int APS5_VABI getChannelGroup(FMOD_CHANNELGROUP** group) { - std::lock_guard lock(gStudioMutex); - if (group) *group = nullptr; - return FMOD_OK; - } - int APS5_VABI setCallback(FMOD_STUDIO_EVENT_CALLBACK callback) { - (void)callback; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI hasSustainPoint(bool* sustainpoint) { - std::lock_guard lock(gStudioMutex); - if (sustainpoint) *sustainpoint = false; - return FMOD_OK; - } - int APS5_VABI triggerCue() { return FMOD_OK; } -}; - -int APS5_VABI EventDescription::createInstance(EventInstance** instance) { - std::lock_guard lock(gStudioMutex); - if (!instance) return FMOD_OK; - *instance = new EventInstance(); - (*instance)->description = this; - return FMOD_OK; -} - -class Bus { -public: - std::string path; - float volume = 1.0f; - bool paused = false; - bool mute = false; - void* userData = nullptr; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getVolume(float* volume, float* finalvolume) { - std::lock_guard lock(gStudioMutex); - if (volume) *volume = this->volume; - if (finalvolume) *finalvolume = this->volume; - return FMOD_OK; - } - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gStudioMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI getPaused(bool* paused) { - std::lock_guard lock(gStudioMutex); - if (paused) *paused = this->paused; - return FMOD_OK; - } - int APS5_VABI setPaused(bool paused) { - std::lock_guard lock(gStudioMutex); - this->paused = paused; - return FMOD_OK; - } - int APS5_VABI getMute(bool* mute) { - std::lock_guard lock(gStudioMutex); - if (mute) *mute = this->mute; - return FMOD_OK; - } - int APS5_VABI setMute(bool mute) { - std::lock_guard lock(gStudioMutex); - this->mute = mute; - return FMOD_OK; - } - int APS5_VABI stopAllEvents(FMOD_STUDIO_STOP_MODE mode) { - (void)mode; - return FMOD_OK; - } - int APS5_VABI getChannelGroup(FMOD_CHANNELGROUP** group) { - std::lock_guard lock(gStudioMutex); - if (group) *group = nullptr; - return FMOD_OK; - } - int APS5_VABI lockChannelGroup() { return FMOD_OK; } - int APS5_VABI unlockChannelGroup() { return FMOD_OK; } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class VCA { -public: - std::string path; - float volume = 1.0f; - void* userData = nullptr; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getVolume(float* volume, float* finalvolume) { - std::lock_guard lock(gStudioMutex); - if (volume) *volume = this->volume; - if (finalvolume) *finalvolume = this->volume; - return FMOD_OK; - } - int APS5_VABI setVolume(float volume) { - std::lock_guard lock(gStudioMutex); - this->volume = volume; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class CommandReplay { -public: - void* userData = nullptr; - int APS5_VABI isValid() { - return true; - } - int APS5_VABI getLength(float* length) { - std::lock_guard lock(gStudioMutex); - if (length) *length = 0.0f; - return FMOD_OK; - } - int APS5_VABI start() { return FMOD_OK; } - int APS5_VABI stop() { return FMOD_OK; } - int APS5_VABI seekToTime(float time) { - (void)time; - return FMOD_OK; - } - int APS5_VABI release() { - std::lock_guard lock(gStudioMutex); - delete this; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } -}; - -class System { -public: - bool initialized = false; - int maxChannels = 0; - int numListeners = 1; - void* userData = nullptr; - std::unordered_map paramsByName; - std::vector banks; - static int APS5_VABI create(System** system, unsigned headerversion) { - (void)headerversion; - std::lock_guard lock(gStudioMutex); - if (!system) return FMOD_OK; - *system = new System(); - return FMOD_OK; - } - int APS5_VABI isValid() { - return true; - } - int APS5_VABI release() { - std::lock_guard lock(gStudioMutex); - for (Bank* bank : banks) delete bank; - banks.clear(); - delete this; - return FMOD_OK; - } - int APS5_VABI initialize(int maxchannels, unsigned studioflags, unsigned flags, void* extradriverdata) { - (void)studioflags; (void)flags; (void)extradriverdata; - std::lock_guard lock(gStudioMutex); - maxChannels = maxchannels; - initialized = true; - return FMOD_OK; - } - int APS5_VABI update() { return FMOD_OK; } - int APS5_VABI getEvent(const char* pathOrID, EventDescription** event) { - std::lock_guard lock(gStudioMutex); - if (!event) return FMOD_OK; - *event = new EventDescription(); - if (pathOrID) (*event)->path = pathOrID; - return FMOD_OK; - } - int APS5_VABI getBankList(Bank** array, int capacity, int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = static_cast(banks.size()); - if (array && capacity > 0) { - int filled = capacity < static_cast(banks.size()) ? capacity : static_cast(banks.size()); - for (int i = 0; i < filled; i++) array[i] = banks[static_cast(i)]; - } - return FMOD_OK; - } - int APS5_VABI getBankCount(int* count) { - std::lock_guard lock(gStudioMutex); - if (count) *count = static_cast(banks.size()); - return FMOD_OK; - } - int APS5_VABI loadBankFile(const char* filename, unsigned flags, Bank** bank) { - (void)flags; - std::lock_guard lock(gStudioMutex); - if (!bank) return FMOD_OK; - *bank = new Bank(); - if (filename) (*bank)->path = filename; - banks.push_back(*bank); - return FMOD_OK; - } - int APS5_VABI loadBankMemory(const char* buffer, int length, unsigned mode, unsigned flags, Bank** bank) { - (void)buffer; (void)length; (void)mode; (void)flags; - std::lock_guard lock(gStudioMutex); - if (!bank) return FMOD_OK; - *bank = new Bank(); - banks.push_back(*bank); - return FMOD_OK; - } - int APS5_VABI unloadAll() { - std::lock_guard lock(gStudioMutex); - for (Bank* bank : banks) delete bank; - banks.clear(); - return FMOD_OK; - } - int APS5_VABI flushCommands() { return FMOD_OK; } - int APS5_VABI flushSampleLoading() { return FMOD_OK; } - int APS5_VABI setNumListeners(int numlisteners) { - std::lock_guard lock(gStudioMutex); - numListeners = numlisteners; - return FMOD_OK; - } - int APS5_VABI getNumListeners(int* numlisteners) { - std::lock_guard lock(gStudioMutex); - if (numlisteners) *numlisteners = numListeners; - return FMOD_OK; - } - int APS5_VABI setListenerAttributes(int index, const FMOD_3D_ATTRIBUTES* attributes, const FMOD_VECTOR* attenuationposition) { - (void)index; (void)attributes; (void)attenuationposition; - return FMOD_OK; - } - int APS5_VABI getBus(const char* pathOrID, Bus** bus) { - std::lock_guard lock(gStudioMutex); - if (!bus) return FMOD_OK; - *bus = new Bus(); - if (pathOrID) (*bus)->path = pathOrID; - return FMOD_OK; - } - int APS5_VABI getVCA(const char* pathOrID, VCA** vca) { - std::lock_guard lock(gStudioMutex); - if (!vca) return FMOD_OK; - *vca = new VCA(); - if (pathOrID) (*vca)->path = pathOrID; - return FMOD_OK; - } - int APS5_VABI getParameterByName(const char* name, float* value, float* finalvalue) { - std::lock_guard lock(gStudioMutex); - float stored = 0.0f; - if (name) { - auto it = paramsByName.find(name); - if (it != paramsByName.end()) stored = it->second; - } - if (value) *value = stored; - if (finalvalue) *finalvalue = stored; - return FMOD_OK; - } - int APS5_VABI setParameterByName(const char* name, float value, bool ignoreseekspeed) { - (void)ignoreseekspeed; - std::lock_guard lock(gStudioMutex); - if (name) paramsByName[name] = value; - return FMOD_OK; - } - int APS5_VABI setCallback(FMOD_STUDIO_SYSTEM_CALLBACK callback, FMOD_STUDIO_SYSTEM_CALLBACK_TYPE callbackmask) { - (void)callback; (void)callbackmask; - return FMOD_OK; - } - int APS5_VABI registerPlugin(const FMOD_DSP_DESCRIPTION* description) { - (void)description; - return FMOD_OK; - } - int APS5_VABI setUserData(void* userdata) { - std::lock_guard lock(gStudioMutex); - userData = userdata; - return FMOD_OK; - } - int APS5_VABI getUserData(void** userdata) { - std::lock_guard lock(gStudioMutex); - if (userdata) *userdata = userData; - return FMOD_OK; - } - int APS5_VABI getCPUUsage(FMOD_STUDIO_CPU_USAGE* usage) { - std::lock_guard lock(gStudioMutex); - if (usage) std::memset(usage, 0, sizeof(*usage)); - return FMOD_OK; - } - int APS5_VABI getBufferUsage(FMOD_STUDIO_BUFFER_USAGE* usage) { - std::lock_guard lock(gStudioMutex); - if (usage) std::memset(usage, 0, sizeof(*usage)); - return FMOD_OK; - } - int APS5_VABI resetBufferUsage() { return FMOD_OK; } - int APS5_VABI getMemoryUsage(FMOD_STUDIO_MEMORY_USAGE* memoryusage) { - std::lock_guard lock(gStudioMutex); - if (memoryusage) std::memset(memoryusage, 0, sizeof(*memoryusage)); - return FMOD_OK; - } -}; - -} -} - -extern "C" { - -namespace { - -// Round2: 20/25 studio stubs identified via real FMOD headers (const-correct -// Itanium manglings, verified with g++ + c++filt + NidCompute SHA1+suffix). -// Wrappers below are ABI-compatible (this+params, SysV) with the real const -// methods; most delegate to the existing non-const logic (const is -// compile-time only, same calling convention). - -// +V2QcIcvWpw = EventDescription::createInstance(EventInstance**) const -__attribute__((used)) int APS5_VABI fmodstudio_real_createInstance(const void* self, void** out) { - std::lock_guard lock(gStudioMutex); - if (!out) return 0; - auto* desc = const_cast(static_cast(self)); - auto* inst = new FMOD::Studio::EventInstance(); - inst->description = desc; - *out = inst; - return 0; -} -// 2QOSvz5hvAg = EventInstance::getPlaybackState(PLAYBACK_STATE*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getPlaybackState(const void* self, int* state) { - std::lock_guard lock(gStudioMutex); - const auto* inst = static_cast(self); - if (state) *state = (inst && inst->playing) ? 0 : 2; // PLAYING=0, STOPPED=2 - return 0; -} -// 3loRRCoqXfU = EventInstance::isValid() const -__attribute__((used)) int APS5_VABI fmodstudio_real_ei_isValid(const void* self) { - (void)self; - return 1; -} -// 6E6MUTGjxhI = System::getEvent(const char*, EventDescription**) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getEvent(const void* self, const char* path, void** out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (!out) return 0; - auto* ed = new FMOD::Studio::EventDescription(); - if (path) ed->path = path; - *out = ed; - return 0; -} -// 6nWYF1yMOl4 = Bank::getLoadingState(LOADING_STATE*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_bankLoading(const void* self, int* state) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (state) *state = 3; // LOADED - return 0; -} -// 7hd4bRJuLMg = System::getCoreSystem(CoreSystem**) const (placeholder 256B zeroed) -__attribute__((used)) int APS5_VABI fmodstudio_real_getCoreSystem(const void* self, void** out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (!out) return 0; - void* placeholder = std::calloc(1, 256); - *out = placeholder; - return 0; -} -// -7VafNNns2A = EventInstance::getUserData(void**) const -__attribute__((used)) int APS5_VABI fmodstudio_real_ei_getUserData(const void* self, void** out) { - std::lock_guard lock(gStudioMutex); - const auto* inst = static_cast(self); - if (out) *out = inst ? inst->userData : nullptr; - return 0; -} -// AdR01fo-WaM = System::getParameterByName(const char*, float*, float*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_sysGetParam(const void* self, const char* name, float* v, float* fv) { - std::lock_guard lock(gStudioMutex); - const auto* sys = static_cast(self); - float stored = 0.0f; - if (sys && name) { - auto it = sys->paramsByName.find(name); - if (it != sys->paramsByName.end()) stored = it->second; - } - if (v) *v = stored; - if (fv) *fv = stored; - return 0; -} -// alleMuKEWr8 = EventDescription::isOneshot(bool*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_isOneshot(const void* self, bool* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = false; - return 0; -} -// Bu8uFP7ME2k = System::getVCA(const char*, VCA**) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getVCA(const void* self, const char* path, void** out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (!out) return 0; - auto* vca = new FMOD::Studio::VCA(); - if (path) vca->path = path; - *out = vca; - return 0; -} -// DsFsPD0MWNc = System::getSoundInfo(const char*, SOUND_INFO*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getSoundInfo(const void* self, const char* key, void* info) { - (void)self; (void)key; - std::lock_guard lock(gStudioMutex); - if (info) std::memset(info, 0, sizeof(FMOD_STUDIO_SOUND_INFO)); - return 0; -} -// --f9RJwAh1A = EventDescription::getSampleLoadingState(LOADING_STATE*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_edSampleState(const void* self, int* state) { - std::lock_guard lock(gStudioMutex); - const auto* ed = static_cast(self); - if (state) *state = (ed && ed->sampleDataLoaded) ? 3 : 1; // LOADED=3, UNLOADED=1 - return 0; -} -// fbAVXdyBuso = Bank::isValid() const -__attribute__((used)) int APS5_VABI fmodstudio_real_bankIsValid(const void* self) { - (void)self; - return 1; -} -// LG53EJZJDnA = EventInstance::setCallback(callback, mask) (non-const, 2 params) -__attribute__((used)) int APS5_VABI fmodstudio_real_eiSetCallback(void* self, void* cb, unsigned mask) { - (void)self; (void)cb; (void)mask; - return 0; -} -// nhBPjhZ+VWs = EventDescription::is3D(bool*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_is3D(const void* self, bool* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = false; - return 0; -} -// omDBr+dcDVc = EventDescription::getInstanceCount(int*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getInstanceCount(const void* self, int* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = 0; - return 0; -} -// uWXqw4LeM5Q = EventDescription::isValid() const -__attribute__((used)) int APS5_VABI fmodstudio_real_edIsValid(const void* self) { - (void)self; - return 1; -} -// VdQjbIsdhXQ = EventDescription::getLength(int*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_edGetLength(const void* self, int* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = 0; - return 0; -} -// WP51b8simn8 = EventInstance::getTimelinePosition(int*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getTimeline(const void* self, int* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) *out = 0; - return 0; -} -// Zi1n4MvZ3-0 = System::getBufferUsage(BUFFER_USAGE*) const -__attribute__((used)) int APS5_VABI fmodstudio_real_getBufferUsage(const void* self, void* out) { - (void)self; - std::lock_guard lock(gStudioMutex); - if (out) std::memset(out, 0, sizeof(FMOD_STUDIO_BUFFER_USAGE)); - return 0; -} - -__attribute__((used)) int fmodstudio_nid_stub_10() { NotImplemented_nid_no_patch("Cm2cmtCv8cA"); return 0; } -__attribute__((used)) int fmodstudio_nid_stub_12() { NotImplemented_nid_no_patch("EfM9xXvBmxk"); return 0; } -__attribute__((used)) int fmodstudio_nid_stub_15() { NotImplemented_nid_no_patch("Js90KQXVS4s"); return 0; } -__attribute__((used)) int fmodstudio_nid_stub_18() { NotImplemented_nid_no_patch("oCYWES02VPc"); return 0; } -__attribute__((used)) int fmodstudio_nid_stub_22() { NotImplemented_nid_no_patch("vVwA2cZA5e4"); return 0; } - -} - -APS5_EXPORT("+V2QcIcvWpw", fmodstudio_real_createInstance); -APS5_EXPORT("2QOSvz5hvAg", fmodstudio_real_getPlaybackState); -APS5_EXPORT("3loRRCoqXfU", fmodstudio_real_ei_isValid); -APS5_EXPORT("6E6MUTGjxhI", fmodstudio_real_getEvent); -APS5_EXPORT("6nWYF1yMOl4", fmodstudio_real_bankLoading); -APS5_EXPORT("7hd4bRJuLMg", fmodstudio_real_getCoreSystem); -APS5_EXPORT("-7VafNNns2A", fmodstudio_real_ei_getUserData); -APS5_EXPORT("AdR01fo-WaM", fmodstudio_real_sysGetParam); -APS5_EXPORT("alleMuKEWr8", fmodstudio_real_isOneshot); -APS5_EXPORT("Bu8uFP7ME2k", fmodstudio_real_getVCA); -APS5_EXPORT("Cm2cmtCv8cA", fmodstudio_nid_stub_10); -APS5_EXPORT("DsFsPD0MWNc", fmodstudio_real_getSoundInfo); -APS5_EXPORT("EfM9xXvBmxk", fmodstudio_nid_stub_12); -APS5_EXPORT("--f9RJwAh1A", fmodstudio_real_edSampleState); -APS5_EXPORT("fbAVXdyBuso", fmodstudio_real_bankIsValid); -APS5_EXPORT("Js90KQXVS4s", fmodstudio_nid_stub_15); -APS5_EXPORT("LG53EJZJDnA", fmodstudio_real_eiSetCallback); -APS5_EXPORT("nhBPjhZ+VWs", fmodstudio_real_is3D); -APS5_EXPORT("oCYWES02VPc", fmodstudio_nid_stub_18); -APS5_EXPORT("omDBr+dcDVc", fmodstudio_real_getInstanceCount); -APS5_EXPORT("uWXqw4LeM5Q", fmodstudio_real_edIsValid); -APS5_EXPORT("VdQjbIsdhXQ", fmodstudio_real_edGetLength); -APS5_EXPORT("vVwA2cZA5e4", fmodstudio_nid_stub_22); -APS5_EXPORT("WP51b8simn8", fmodstudio_real_getTimeline); -APS5_EXPORT("Zi1n4MvZ3-0", fmodstudio_real_getBufferUsage); - -} diff --git a/core/libs/prx/libkernel/Aio/src/Aio.cpp b/core/libs/prx/libkernel/Aio/src/Aio.cpp index b592c1643..0dc7d97d8 100644 --- a/core/libs/prx/libkernel/Aio/src/Aio.cpp +++ b/core/libs/prx/libkernel/Aio/src/Aio.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -26,6 +27,10 @@ constexpr std::int32_t AioProcessing = 2; constexpr std::int32_t AioCompleted = 3; constexpr std::int32_t AioAborted = 4; constexpr std::int32_t AioMaxQueues = 512; +constexpr std::int32_t AioRequestNumMax = 128; +constexpr std::int32_t AioIdNumMax = 128; +constexpr std::uint32_t AioWaitAnd = 1; +constexpr std::uint32_t AioWaitOr = 2; std::mutex g_mutex; std::array g_states{}; @@ -100,35 +105,67 @@ std::int64_t NativePwrite(std::int32_t fd, const void* buf, std::size_t nbyte, s #endif } -int SubmitCommands(KernelAioRwRequest* req, std::int32_t size, bool write, std::int32_t* id) { +bool RunRequest(KernelAioRwRequest& req, bool write) { + const std::int64_t done = write + ? NativePwrite(req.fd, req.buf, req.nbyte, req.offset) + : NativePread(req.fd, req.buf, req.nbyte, req.offset); + if (done < 0) { + const int error = errno; + req.result->return_value = static_cast(SCE_KERNEL_ERROR_EIO); + if (error == EBADF) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EBADF); + if (error == EFAULT) req.result->return_value = static_cast(SCE_KERNEL_ERROR_EFAULT); + req.result->state = AioAborted; + return false; + } + req.result->return_value = done; + req.result->state = AioCompleted; + return true; +} + +int ValidateRequests(KernelAioRwRequest* req, std::int32_t size, const void* id) { if (req == nullptr || id == nullptr) return SCE_KERNEL_ERROR_EFAULT; if (size <= 0) return SCE_KERNEL_ERROR_EINVAL; for (std::int32_t i = 0; i < size; ++i) { if (req[i].result == nullptr) return SCE_KERNEL_ERROR_EFAULT; } + return 0; +} + +int SubmitCommands(KernelAioRwRequest* req, std::int32_t size, bool write, std::int32_t* id) { + const int error = ValidateRequests(req, size, id); + if (error != 0) return error; const std::int32_t queue = AllocateId(); bool aborted = false; for (std::int32_t i = 0; i < size; ++i) { - const std::int64_t done = write - ? NativePwrite(req[i].fd, req[i].buf, req[i].nbyte, req[i].offset) - : NativePread(req[i].fd, req[i].buf, req[i].nbyte, req[i].offset); - if (done < 0) { - const int error = errno; - req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EIO); - if (error == EBADF) req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EBADF); - if (error == EFAULT) req[i].result->return_value = static_cast(SCE_KERNEL_ERROR_EFAULT); - req[i].result->state = AioAborted; - aborted = true; - } else { - req[i].result->return_value = done; - req[i].result->state = AioCompleted; - } + if (!RunRequest(req[i], write)) aborted = true; } SetState(queue, aborted ? AioAborted : AioCompleted); *id = queue; return 0; } +int SubmitCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, bool write, std::int32_t* id, const char* name) { + const int error = ValidateRequests(req, size, id); + if (error != 0) return error; + if (size > AioRequestNumMax) throw std::runtime_error(std::string(name) + ": more than 128 requests per batch is not modelled"); + for (std::int32_t i = 0; i < size; ++i) { + const std::int32_t queue = AllocateId(); + SetState(queue, RunRequest(req[i], write) ? AioCompleted : AioAborted); + id[i] = queue; + } + return 0; +} + +int ValidateIds(const std::int32_t* id, std::int32_t num, const void* out, const char* name) { + if (id == nullptr || out == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (num < 0) return SCE_KERNEL_ERROR_EINVAL; + if (num > AioIdNumMax) throw std::runtime_error(std::string(name) + ": more than 128 ids per batch is not modelled"); + for (std::int32_t i = 0; i < num; ++i) { + if (!ValidId(id[i])) return SCE_KERNEL_ERROR_EINVAL; + } + return 0; +} + } extern "C" { @@ -141,6 +178,46 @@ int APS5_VABI sceKernelAioDeleteRequest(int32_t id, int32_t* ret) { return 0; } +int APS5_VABI sceKernelAioDeleteRequests(int32_t* id, int32_t num, int32_t* ret) { + const int error = ValidateIds(id, num, ret, "sceKernelAioDeleteRequests"); + if (error != 0) return error; + for (int32_t i = 0; i < num; ++i) { + SetState(id[i], AioAborted); + ret[i] = 0; + } + return 0; +} + +int APS5_VABI sceKernelAioCancelRequest(int32_t id, int32_t* state) { + if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (id == 0) { + *state = AioProcessing; + return 0; + } + if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL; + SetState(id, AioAborted); + *state = AioAborted; + return 0; +} + +int APS5_VABI sceKernelAioCancelRequests(int32_t* id, int32_t num, int32_t* state) { + if (id == nullptr || state == nullptr) return SCE_KERNEL_ERROR_EFAULT; + if (num < 0) return SCE_KERNEL_ERROR_EINVAL; + if (num > AioIdNumMax) throw std::runtime_error("sceKernelAioCancelRequests: more than 128 ids per batch is not modelled"); + for (int32_t i = 0; i < num; ++i) { + if (id[i] != 0 && !ValidId(id[i])) return SCE_KERNEL_ERROR_EINVAL; + } + for (int32_t i = 0; i < num; ++i) { + if (id[i] == 0) { + state[i] = AioProcessing; + continue; + } + SetState(id[i], AioAborted); + state[i] = AioAborted; + } + return 0; +} + int APS5_VABI sceKernelAioInitializeImpl(void* param, int32_t size) { (void)param; (void)size; @@ -161,6 +238,16 @@ int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest* req, int32_t s return SubmitCommands(req, size, true, id); } +int APS5_VABI sceKernelAioSubmitReadCommandsMultiple(KernelAioRwRequest* req, int32_t size, int32_t prio, int32_t* id) { + (void)prio; + return SubmitCommandsMultiple(req, size, false, id, "sceKernelAioSubmitReadCommandsMultiple"); +} + +int APS5_VABI sceKernelAioSubmitWriteCommandsMultiple(KernelAioRwRequest* req, int32_t size, int32_t prio, int32_t* id) { + (void)prio; + return SubmitCommandsMultiple(req, size, true, id, "sceKernelAioSubmitWriteCommandsMultiple"); +} + int APS5_VABI sceKernelAioPollRequest(int32_t id, int32_t* state) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL; @@ -169,6 +256,31 @@ int APS5_VABI sceKernelAioPollRequest(int32_t id, int32_t* state) { return 0; } +int APS5_VABI sceKernelAioWaitRequests(int32_t* id, int32_t num, int32_t* state, uint32_t mode, uint32_t* usec) { + const int error = ValidateIds(id, num, state, "sceKernelAioWaitRequests"); + if (error != 0) return error; + if (mode != AioWaitAnd && mode != AioWaitOr) throw std::runtime_error("sceKernelAioWaitRequests: unsupported wait mode"); + const auto start = std::chrono::steady_clock::now(); + for (;;) { + bool allDone = true; + bool anyCompleted = false; + { + std::lock_guard lock(g_mutex); + for (int32_t i = 0; i < num; ++i) { + state[i] = g_states[id[i]]; + if (state[i] == AioProcessing) allDone = false; + if (state[i] == AioCompleted) anyCompleted = true; + } + } + if (allDone || (mode == AioWaitOr && anyCompleted)) return 0; + if (usec != nullptr && *usec != 0) { + const auto elapsed = std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count(); + if (elapsed > static_cast(*usec)) return SCE_KERNEL_ERROR_ETIMEDOUT; + } + std::this_thread::sleep_for(std::chrono::microseconds(10)); + } +} + int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec) { if (state == nullptr) return SCE_KERNEL_ERROR_EFAULT; if (!ValidId(id)) return SCE_KERNEL_ERROR_EINVAL; @@ -194,4 +306,9 @@ int APS5_VABI sceKernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* usec } } +int APS5_VABI sceKernelAioPollRequests() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index a2021c427..a9521483b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -36,6 +36,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/EventFlag/src/EventFlag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/Module.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/DynamicLoader.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Module/src/ModuleInfo.cpp ${LIBS_INCLUDE_DIR}/nid/src/NidCompute.cpp ${LIBS_INCLUDE_DIR}/nid/src/Sha1.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Aio/src/Aio.cpp @@ -95,6 +96,13 @@ if(BUILD_TESTING) COMMAND $ guest_module_fixture $ VERBATIM) add_dependencies(guest_module_fixture nid_patcher) + set_target_properties(guest_module_fixture PROPERTIES BUILD_WITH_INSTALL_RPATH ON INSTALL_RPATH "$ORIGIN") + find_package(Python3 QUIET COMPONENTS Interpreter) + if(Python3_Interpreter_FOUND AND NOT WIN32) + target_link_options(guest_module_fixture PRIVATE -Wl,--no-as-needed) + add_test(NAME nid_patcher_dynamic_strings COMMAND ${Python3_EXECUTABLE} + ${LIBS_INCLUDE_DIR}/nid/tests/test_dynamic_strings.py $) + endif() add_test_executable(guest_dynamic_loader_tests ${LIBS_INCLUDE_DIR}/tests/GuestDynamicLoader.cpp) target_include_directories(guest_dynamic_loader_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_dynamic_loader_tests PRIVATE libkernel libc) @@ -107,6 +115,15 @@ if(BUILD_TESTING) set_tests_properties(guest_dynamic_loader PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(NOT WIN32) + add_test_executable(guest_module_info_tests ${LIBS_INCLUDE_DIR}/tests/GuestModuleInfo.cpp) + target_include_directories(guest_module_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_module_info_tests PRIVATE libkernel libc) + add_dependencies(guest_module_info_tests guest_module_fixture) + add_test(NAME guest_module_info COMMAND guest_module_info_tests $) + set_tests_properties(guest_module_info PROPERTIES + WORKING_DIRECTORY "$") + endif() add_test_executable(guest_filesystem_tests ${LIBS_INCLUDE_DIR}/tests/GuestFilesystem.cpp) target_include_directories(guest_filesystem_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_filesystem_tests PRIVATE libkernel libc) @@ -145,6 +162,16 @@ if(BUILD_TESTING) set_tests_properties(guest_pthread_barrier PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_kernel_is_stack_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelIsStack.cpp) + target_include_directories(guest_kernel_is_stack_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_kernel_is_stack_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_kernel_is_stack_tests) + add_test(NAME guest_kernel_is_stack COMMAND guest_kernel_is_stack_tests) + set_tests_properties(guest_kernel_is_stack PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_kernel_is_stack PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_thread_self_tests ${LIBS_INCLUDE_DIR}/tests/GuestThreadSelf.cpp) target_include_directories(guest_thread_self_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_thread_self_tests PRIVATE libkernel libc) @@ -254,6 +281,15 @@ if(BUILD_TESTING) set_tests_properties(guest_signals PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_exception_handler_tests ${LIBS_INCLUDE_DIR}/tests/GuestExceptionHandler.cpp) + target_include_directories(guest_exception_handler_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_exception_handler_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_exception_handler_tests) + add_test(NAME guest_exception_handler COMMAND guest_exception_handler_tests) + if(WIN32) + set_tests_properties(guest_exception_handler PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_sync_on_address_tests ${LIBS_INCLUDE_DIR}/tests/GuestSyncOnAddress.cpp) target_include_directories(guest_sync_on_address_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_sync_on_address_tests PRIVATE libkernel libc) @@ -274,6 +310,16 @@ if(BUILD_TESTING) set_tests_properties(guest_available_cpumask PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_getrusage_tests ${LIBS_INCLUDE_DIR}/tests/GuestGetrusage.cpp) + target_include_directories(guest_getrusage_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_getrusage_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_getrusage_tests) + add_test(NAME guest_getrusage COMMAND guest_getrusage_tests) + set_tests_properties(guest_getrusage PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_getrusage PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_error_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelErrors.cpp) target_include_directories(guest_kernel_error_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_error_tests PRIVATE libkernel libc) @@ -365,6 +411,10 @@ if(BUILD_TESTING) target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_directory_entry_tests PRIVATE libkernel libc) add_test(NAME guest_directory_entries COMMAND guest_directory_entry_tests) + add_test_executable(guest_file_vector_tests ${LIBS_INCLUDE_DIR}/tests/GuestFileVector.cpp) + target_include_directories(guest_file_vector_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_file_vector_tests PRIVATE libkernel libc) + add_test(NAME guest_file_vector COMMAND guest_file_vector_tests) else() add_test_executable(guest_directory_close_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryClose.cpp) target_include_directories(guest_directory_close_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp index 974565499..8fed7f059 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp @@ -194,13 +194,13 @@ void ValidateRange(const void* addr, size_t len, size_t alignment) { } int LinuxProtFromSce(int prot) { - if ((prot & ~0xF7) != 0) { + if ((prot & ~0x3F7) != 0) { // return SCE_KERNEL_ERROR_EINVAL; throw std::invalid_argument("Unsupported memory protection bits: " + std::to_string(prot)); } int result = PROT_NONE; - if (prot & 0x13) result |= PROT_READ; - if (prot & 0x22) result |= PROT_READ | PROT_WRITE; + if (prot & 0x113) result |= PROT_READ; + if (prot & 0x222) result |= PROT_READ | PROT_WRITE; if (prot & 4) result |= PROT_READ | PROT_EXEC; return result; } @@ -516,6 +516,31 @@ void EraseProtections(std::uintptr_t start, std::uintptr_t end) { } } +std::mutex g_reservationLock; +std::map g_reservations; + +void EraseReservations(const void* addr, size_t len) { + const auto start = reinterpret_cast(addr); + const auto end = start + len; + std::lock_guard lock(g_reservationLock); + auto it = g_reservations.lower_bound(start); + if (it != g_reservations.begin() && std::prev(it)->second > start) --it; + while (it != g_reservations.end() && it->first < end) { + const auto rangeStart = it->first; + const auto rangeEnd = it->second; + it = g_reservations.erase(it); + if (rangeStart < start) g_reservations.emplace(rangeStart, start); + if (rangeEnd > end) it = g_reservations.emplace(end, rangeEnd).first; + } +} + +void RecordReservation(const void* addr, size_t len) { + EraseReservations(addr, len); + const auto start = reinterpret_cast(addr); + std::lock_guard lock(g_reservationLock); + g_reservations.emplace(start, start + len); +} + void RecordProtection(const void* addr, size_t len, int prot) { const auto start = reinterpret_cast(addr); std::lock_guard lock(g_protectionLock); @@ -540,6 +565,26 @@ void ValidateOutput(void** addr) { } +bool Reserved(const void* addr, size_t len) { + const auto start = reinterpret_cast(addr); + const auto end = start + len; + std::lock_guard lock(g_reservationLock); + auto it = g_reservations.upper_bound(start); + if (it == g_reservations.begin()) return false; + auto cursor = start; + for (--it; it != g_reservations.end() && it->first <= cursor; ++it) { + cursor = std::max(cursor, it->second); + if (cursor >= end) return true; + } + return false; +} + +bool FixedNoOverwriteConflict(const GuestAllocations::Mutation& mutation, void* addr, size_t len, int flags) { + constexpr int GuestMapNoOverwrite = 0x80; + if (addr == nullptr || (flags & GuestMapFixedFlag) == 0 || (flags & GuestMapNoOverwrite) == 0) return false; + return mutation.Overlaps(addr, len) && !Reserved(addr, len); +} + int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; @@ -547,7 +592,9 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, return SCE_KERNEL_ERROR_EINVAL; } GuestAllocations::Mutation mutation; + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags, physStart)) { + EraseReservations(*addr, len); RecordProtection(*addr, len, prot); return 0; } @@ -564,6 +611,7 @@ int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, throw; } *addr = mapped; + EraseReservations(mapped, len); RecordProtection(mapped, len, prot); Trace("map direct %p+0x%zx phys=0x%llx prot=0x%x flags=0x%x align=0x%zx", mapped, len, static_cast(physStart), prot, flags, alignment); return 0; @@ -573,7 +621,9 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { ValidateOutput(addr); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; GuestAllocations::Mutation mutation; + if (FixedNoOverwriteConflict(mutation, *addr, len, flags)) return SCE_KERNEL_ERROR_ENOMEM; if (RemapFixedIntoRegistered(mutation, *addr, len, prot, flags)) { + EraseReservations(*addr, len); RecordProtection(*addr, len, prot); return 0; } @@ -586,6 +636,7 @@ int DoMapAnon(void** addr, size_t len, int prot, int flags) { throw; } *addr = mapped; + EraseReservations(mapped, len); RecordProtection(mapped, len, prot); Trace("map anon %p+0x%zx prot=0x%x flags=0x%x", mapped, len, prot, flags); return 0; @@ -620,6 +671,33 @@ int DoMprotect(const void* addr, size_t len, int prot) { return 0; } +int DoMtypeprotect(const void* addr, size_t len, int type, int prot) { + const int result = DoMprotect(addr, len, prot); + const auto address = reinterpret_cast(addr); + constexpr auto pageMask = static_cast(PS5_PAGE_SIZE - 1); + const auto first = address & ~pageMask; + const auto end = (address + len + pageMask) & ~pageMask; + std::vector> physical; + { + std::lock_guard lock(g_directLock); + auto it = g_directMappings.lower_bound(first); + if (it != g_directMappings.begin() && std::prev(it)->second.end > first) --it; + while (it != g_directMappings.end() && it->first < end) { + const auto base = it->first; + const auto mapping = it->second; + it = g_directMappings.erase(it); + if (base < first) g_directMappings.emplace(base, DirectMapping{first, mapping.phys, mapping.memoryType, mapping.backing}); + const auto low = std::max(base, first); + const auto high = std::min(mapping.end, end); + g_directMappings.emplace(low, DirectMapping{high, mapping.phys + low - base, type, mapping.backing}); + physical.emplace_back(mapping.phys + low - base, high - low); + if (mapping.end > end) it = g_directMappings.emplace(end, DirectMapping{mapping.end, mapping.phys + end - base, mapping.memoryType, mapping.backing}).first; + } + } + for (const auto& [phys, bytes] : physical) DirectMemoryRetype(static_cast(phys), bytes, type); + return result; +} + int DoMunmap(void* addr, size_t len) { Trace("unmap %p+0x%zx", addr, len); if (len == 0 || (len & (PS5_PAGE_SIZE - 1)) != 0 || !addr) return SCE_KERNEL_ERROR_EINVAL; @@ -636,6 +714,7 @@ int DoMunmap(void* addr, size_t len) { else munmap(pieceAddress, pieceBytes); #endif }); + EraseReservations(addr, len); RecordProtection(addr, len, -1); return 0; } @@ -647,7 +726,11 @@ int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment) { const bool fixed = *addr != nullptr && (flags & GuestMapFixedFlag) != 0; if (fixed && mutation.Covers(*addr, len)) { constexpr int GuestMapNoOverwrite = 0x80; - if ((flags & GuestMapNoOverwrite) == 0 && RemapFixedIntoRegistered(mutation, *addr, len, 0, GuestMapFixedFlag)) return 0; + if ((flags & GuestMapNoOverwrite) == 0 && RemapFixedIntoRegistered(mutation, *addr, len, 0, GuestMapFixedFlag)) { + RecordProtection(*addr, len, -1); + RecordReservation(*addr, len); + return 0; + } mutation.RequireAvailable(*addr, len); } if (fixed) mutation.RequireAvailable(*addr, len); @@ -660,6 +743,7 @@ int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment) { throw; } *addr = mapped; + RecordReservation(mapped, len); Trace("reserve %p+0x%zx flags=0x%x align=0x%zx", mapped, len, flags, alignment); return 0; } @@ -686,6 +770,17 @@ void ForgetDirectMemory(int64_t start, size_t len) { Trace("release physical 0x%llx+0x%zx", static_cast(first), len); } +bool GuestReservation(std::uintptr_t addr, std::uintptr_t* start, std::uintptr_t* end) { + std::lock_guard lock(g_reservationLock); + const auto next = g_reservations.upper_bound(addr); + if (next == g_reservations.begin()) return false; + const auto containing = std::prev(next); + if (addr >= containing->second) return false; + *start = containing->first; + *end = containing->second; + return true; +} + bool GuestProtection(uintptr_t addr, int* prot) { std::lock_guard lock(g_protectionLock); auto next = g_protections.upper_bound(addr); diff --git a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp index badcadce6..e2310818d 100644 --- a/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp +++ b/core/libs/prx/libkernel/DirectMemory/DirectMemory.hpp @@ -11,16 +11,20 @@ static constexpr size_t PS5_PAGE_SIZE = 0x4000; int DirectMemoryAlloc(int64_t searchStart, int64_t searchEnd, size_t len, size_t alignment, int memoryType, int64_t* physOut); void DirectMemoryFree(int64_t start, size_t len); +bool DirectMemoryCheckedFree(int64_t start, size_t len); void CreateDirectMemoryBacking(int64_t start, size_t len, int memoryType); bool QueryDirectMapping(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end, std::uint64_t* offset, int* memoryType); void ForgetDirectMemory(int64_t start, size_t len); bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType); +void DirectMemoryRetype(int64_t start, size_t len, int memoryType); size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit); int DoMapDirect(void** addr, size_t len, int prot, int flags, int64_t physStart, size_t alignment); int DoMapAnon(void** addr, size_t len, int prot, int flags); int DoMprotect(const void* addr, size_t len, int prot); +int DoMtypeprotect(const void* addr, size_t len, int type, int prot); int DoMunmap(void* addr, size_t len); int DoReserveVirtual(void** addr, size_t len, int flags, size_t alignment); bool GuestProtection(uintptr_t addr, int* prot); +bool GuestReservation(std::uintptr_t addr, std::uintptr_t* start, std::uintptr_t* end); #endif \ No newline at end of file diff --git a/core/libs/prx/libkernel/DirectMemory/Export.cpp b/core/libs/prx/libkernel/DirectMemory/Export.cpp index 373de6c2a..0b20146d0 100644 --- a/core/libs/prx/libkernel/DirectMemory/Export.cpp +++ b/core/libs/prx/libkernel/DirectMemory/Export.cpp @@ -5,9 +5,12 @@ #include "prx/libc/include/General.hpp" #include "prx/libc/include/GuestAllocations.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include "prx/libkernel/Pthread/include/Pthread.hpp" #ifdef _WIN32 #define WIN32_LEAN_AND_MEAN +#ifndef NOMINMAX #define NOMINMAX +#endif #include #else #include @@ -148,6 +151,18 @@ int APS5_VABI sceKernelDirectMemoryQuery(int64_t offset, int flags, void* info, return 0; } +int APS5_VABI sceKernelGetDirectMemoryType(int64_t offset, int* memory_type, int64_t* start, int64_t* end) { + if (!memory_type || !start || !end) return SCE_KERNEL_ERROR_EINVAL; + int64_t blockStart = 0; + int64_t blockEnd = 0; + int blockType = 0; + if (!DirectMemoryFind(offset, false, &blockStart, &blockEnd, &blockType)) return SCE_KERNEL_ERROR_ENOENT; + *memory_type = blockType; + *start = blockStart; + *end = blockEnd; + return 0; +} + size_t APS5_VABI sceKernelGetDirectMemorySize(void) { return DIRECT_MEMORY_SIZE; } @@ -194,6 +209,10 @@ int APS5_VABI sceKernelMunmap(uint64_t vaddr, size_t len) { return result; } +int APS5_VABI sceKernelReleaseFlexibleMemory(void* addr, size_t len) { + return sceKernelMunmap(reinterpret_cast(addr), len); +} + int APS5_VABI sceKernelReleaseDirectMemory(int64_t start, size_t len) { if (start < 0 || len == 0) return SCE_KERNEL_ERROR_EINVAL; DirectMemoryFree(start, len); @@ -212,6 +231,13 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf // mapping at or above it: titles walk their mappings and check that a mapping covers a whole // allocation, so the answer must be the registered allocation, not a page. constexpr int findNext = 1; + std::uintptr_t reservedStart = 0; + std::uintptr_t reservedEnd = 0; + if (GuestReservation(address, &reservedStart, &reservedEnd)) { + info->start = reservedStart; + info->end = reservedEnd; + return 0; + } const auto lease = GuestAllocations::GuestAllocationsAcquire_nid_postfix(); const GuestAllocations::Range* best = nullptr; for (const auto& range : lease) { @@ -284,15 +310,13 @@ int APS5_VABI sceKernelVirtualQuery(const void* addr, int flags, VirtualQueryInf // --------------------------------------------------------------------------- int APS5_VABI sceKernelCheckedReleaseDirectMemory(int64_t start, size_t len) { - (void)start; - (void)len; - NotImplemented_nid_no_patch(__func__); - return 0; + if (start < 0 || (static_cast(start) & (PS5_PAGE_SIZE - 1)) != 0 || (len & (PS5_PAGE_SIZE - 1)) != 0) return SCE_KERNEL_ERROR_EINVAL; + if (len == 0) return 0; + return DirectMemoryCheckedFree(start, len) ? 0 : SCE_KERNEL_ERROR_ENOENT; } int APS5_VABI sceKernelMtypeprotect(const void* addr, size_t len, int type, int prot) { - (void)type; - return DoMprotect(addr, len, prot); + return DoMtypeprotect(addr, len, type, prot); } int APS5_VABI sceKernelQueryMemoryProtection(void* addr, void** start, void** end, int* prot) { @@ -306,10 +330,14 @@ int APS5_VABI sceKernelQueryMemoryProtection(void* addr, void** start, void** en } int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end) { - (void)addr; - (void)start; - (void)end; - NotImplemented_nid_no_patch(__func__); + std::uintptr_t stackStart = 0; + std::uintptr_t stackEnd = 0; + if (!GuestThreadStack(reinterpret_cast(addr), &stackStart, &stackEnd)) { + VirtualQueryInfo info{}; + if (sceKernelVirtualQuery(addr, 0, &info, sizeof(info)) != 0) return SCE_KERNEL_ERROR_EACCES; + } + if (start) *start = reinterpret_cast(stackStart); + if (end) *end = reinterpret_cast(stackEnd); return 0; } @@ -385,6 +413,10 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, int result = 0; for (; processed < num_entries; ++processed) { auto& entry = entries[processed]; + if (entry.length == 0 || entry.operation < OpMapDirect || entry.operation > OpTypeProtect) { + result = SCE_KERNEL_ERROR_EINVAL; + break; + } switch (entry.operation) { case OpMapDirect: result = DoMapDirect(&entry.start, entry.length, static_cast(entry.protection), flags, static_cast(entry.offset), 0); @@ -393,14 +425,14 @@ int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry* entries, int num_entries, result = sceKernelMunmap(reinterpret_cast(entry.start), entry.length); break; case OpProtect: - case OpTypeProtect: result = DoMprotect(entry.start, entry.length, static_cast(entry.protection)); break; + case OpTypeProtect: + result = DoMtypeprotect(entry.start, entry.length, static_cast(entry.type), static_cast(entry.protection)); + break; case OpMapFlexible: result = _mapFlexible(&entry.start, entry.length, static_cast(entry.protection), flags); break; - default: - throw std::invalid_argument("sceKernelBatchMap2: unsupported operation " + std::to_string(entry.operation)); } if (result != 0) break; } diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp index 1175ada11..645098b71 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.cpp @@ -1,5 +1,6 @@ #include "prx/libkernel/DirectMemory/MemoryPool.hpp" #include "prx/libkernel/DirectMemory/DirectMemory.hpp" +#include #include #include @@ -32,18 +33,17 @@ struct PhysicalMemoryPool { void Free(uint64_t start, size_t len) { std::lock_guard lock(_mutex); - ForgetDirectMemory(static_cast(start), len); - _mark(start, len, false); - const uint64_t end = start + len; - auto it = _blocks.upper_bound(start); - if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; - while (it != _blocks.end() && it->first < end) { - const uint64_t blockStart = it->first; - const Block block = it->second; - it = _blocks.erase(it); - if (blockStart < start) _blocks[blockStart] = {start, block.type}; - if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; - } + _free(start, len); + } + + bool CheckedFree(uint64_t start, size_t len) { + std::lock_guard lock(_mutex); + if (start > DIRECT_MEMORY_SIZE || len > DIRECT_MEMORY_SIZE - start) return false; + const size_t first = start / PS5_PAGE_SIZE; + for (size_t i = 0; i < len / PS5_PAGE_SIZE; ++i) + if (!_used[first + i]) return false; + _free(start, len); + return true; } bool Find(uint64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { @@ -57,6 +57,21 @@ struct PhysicalMemoryPool { return true; } + void Retype(uint64_t start, size_t len, int memoryType) { + std::lock_guard lock(_mutex); + const uint64_t end = start + len; + auto it = _blocks.upper_bound(start); + if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; + while (it != _blocks.end() && it->first < end) { + const uint64_t blockStart = it->first; + const Block block = it->second; + it = _blocks.erase(it); + if (blockStart < start) _blocks[blockStart] = {start, block.type}; + _blocks[std::max(blockStart, start)] = {std::min(block.end, end), memoryType}; + if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + } + } + size_t FreeRun(uint64_t offset, uint64_t limit) { std::lock_guard lock(_mutex); uint64_t cur = offset & ~static_cast(PS5_PAGE_SIZE - 1); @@ -70,6 +85,21 @@ struct PhysicalMemoryPool { } private: + void _free(uint64_t start, size_t len) { + ForgetDirectMemory(static_cast(start), len); + _mark(start, len, false); + const uint64_t end = start + len; + auto it = _blocks.upper_bound(start); + if (it != _blocks.begin() && std::prev(it)->second.end > start) --it; + while (it != _blocks.end() && it->first < end) { + const uint64_t blockStart = it->first; + const Block block = it->second; + it = _blocks.erase(it); + if (blockStart < start) _blocks[blockStart] = {start, block.type}; + if (block.end > end) it = _blocks.emplace(end, Block{block.end, block.type}).first; + } + } + bool _isFree(uint64_t offset, size_t len) const { size_t first = offset / PS5_PAGE_SIZE; size_t count = len / PS5_PAGE_SIZE; @@ -103,10 +133,18 @@ void DirectMemoryFree(int64_t start, size_t len) { PhysicalMemoryPool::Instance().Free(static_cast(start), len); } +bool DirectMemoryCheckedFree(int64_t start, size_t len) { + return PhysicalMemoryPool::Instance().CheckedFree(static_cast(start), len); +} + bool DirectMemoryFind(int64_t offset, bool findNext, int64_t* start, int64_t* end, int* memoryType) { return PhysicalMemoryPool::Instance().Find(static_cast(offset), findNext, start, end, memoryType); } +void DirectMemoryRetype(int64_t start, size_t len, int memoryType) { + PhysicalMemoryPool::Instance().Retype(static_cast(start), len, memoryType); +} + size_t DirectMemoryFreeRun(uint64_t offset, uint64_t limit) { return PhysicalMemoryPool::Instance().FreeRun(offset, limit); } diff --git a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp index b6e799e68..5882f0c2e 100644 --- a/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp +++ b/core/libs/prx/libkernel/DirectMemory/MemoryPool.hpp @@ -1,5 +1,5 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP -#define CORE_LIBS_PRX_LINKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP +#define CORE_LIBS_PRX_LIBKERNEL_DIRECTMEMORY_MEMORYPOOL_HPP #include #include diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 50b5c7faa..7d4769d6e 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -46,7 +46,14 @@ void KernelEqueuePrivate::Close() { void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) { for (auto& ev : m_events) { - if (!ev.triggered && ev.deadlineNs != 0 && ev.deadlineNs <= nowNs) { + if (ev.deadlineNs != 0 && ev.deadlineNs <= nowNs) { + if (ev.event.filter == EVFILT_TIMER) { + const uint64_t count = ev.intervalNs == 0 + ? (ev.triggered ? 0 : 1) + : 1 + (nowNs - ev.deadlineNs) / ev.intervalNs; + ev.event.data += static_cast(count); + ev.deadlineNs += count * ev.intervalNs; + } ev.triggered = true; } } @@ -151,7 +158,12 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros return SCE_KERNEL_ERROR_EBADF; } if (micros == 0) { - m_cond.Wait(lock); + uint32_t timerWait = 0; + if (NextTimerWaitMicros(MonotonicNs(), &timerWait)) { + m_cond.WaitUntil(lock, TimedWait::NowNanos() + static_cast(timerWait) * 1000ULL); + } else { + m_cond.Wait(lock); + } } else { uint32_t timerWait = 0; const bool hasTimer = NextTimerWaitMicros(MonotonicNs(), &timerWait); @@ -176,7 +188,11 @@ int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) { } ); if (it != m_events.end()) { + if (event.event.filter == EVFILT_TIMER) { + TriggerExpiredTimers(MonotonicNs()); + } it->deadlineNs = event.deadlineNs; + it->intervalNs = event.intervalNs; it->event.udata = event.event.udata; for (auto& pending : it->pendingEvents) { pending.udata = event.event.udata; @@ -403,6 +419,22 @@ int APS5_VABI sceKernelDeleteHRTimerEvent(KernelEqueue eq, int id) { return EqueueDeleteEvent_nid_postfix(eq, static_cast(id), EVFILT_HRTIMER); } +int APS5_VABI sceKernelAddTimerEvent(KernelEqueue eq, int id, KernelUseconds usec, void* udata) { + const uint64_t intervalNs = static_cast(usec) * 1000ULL; + KernelEqueueEvent event{}; + event.deadlineNs = KernelEqueuePrivate::MonotonicNs() + intervalNs; + event.intervalNs = intervalNs; + event.event.ident = static_cast(id); + event.event.filter = EVFILT_TIMER; + event.event.flags = EV_ADD | EV_CLEAR; + event.event.udata = udata; + return EqueueAddEvent_nid_postfix(eq, event); +} + +int APS5_VABI sceKernelDeleteTimerEvent(KernelEqueue eq, int id) { + return EqueueDeleteEvent_nid_postfix(eq, static_cast(id), EVFILT_TIMER); +} + int APS5_VABI sceKernelAddAmprEvent(KernelEqueue eq, int id, void* udata) { if (eq == 0) { return EQUEUE_OK; diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 48085e6ac..c5d4f3e5d 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -14,6 +14,7 @@ #include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" +static constexpr int16_t EVFILT_TIMER = -7; static constexpr int16_t EVFILT_USER = -11; static constexpr int16_t EVFILT_VIDEO_OUT = -13; static constexpr int16_t EVFILT_HRTIMER = -15; @@ -42,6 +43,7 @@ struct KernelFilter { struct KernelEqueueEvent { bool triggered = false; uint64_t deadlineNs = 0; + uint64_t intervalNs = 0; KernelEvent event; KernelFilter filter; std::deque pendingEvents; @@ -64,9 +66,9 @@ public: int GetTriggeredEvents(KernelEvent* ev, int num); int WaitForEvents(KernelEvent* ev, int num, uint32_t micros); void Close(); + static uint64_t MonotonicNs(); private: - static uint64_t MonotonicNs(); void TriggerExpiredTimers(uint64_t nowNs); bool NextTimerWaitMicros(uint64_t nowNs, uint32_t* out) const; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 49d2b44cb..2c93a44c4 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -184,4 +184,9 @@ int APS5_VABI sceKernelUnlink(const char* path) { return 0; } +int APS5_VABI sceKernelFcntl() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 5785a3c9b..7defbc421 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -17,6 +17,14 @@ #include #include #include + +struct KernelIovec { + void* base; + std::size_t length; +}; + +static constexpr int KERNEL_IOV_MAX = 1024; + #ifdef _WIN32 #include #include @@ -119,6 +127,8 @@ static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nb #include #include #include +#include +#include #include static int NativeRmdir(const std::filesystem::path& path) { return ::rmdir(path.c_str()); @@ -156,6 +166,12 @@ static std::int64_t NativePread(int descriptor, void* buf, std::size_t nbytes, s static std::int64_t NativePwrite(int descriptor, const void* buf, std::size_t nbytes, std::int64_t offset) { return static_cast(::pwrite(descriptor, buf, nbytes, static_cast(offset))); } +static_assert(sizeof(KernelIovec) == sizeof(struct iovec)); +static_assert(offsetof(KernelIovec, base) == offsetof(struct iovec, iov_base)); +static_assert(offsetof(KernelIovec, length) == offsetof(struct iovec, iov_len)); +static const struct iovec* NativeIovecs(const KernelIovec* iov) { + return reinterpret_cast(iov); +} #endif static constexpr int GUEST_ENOENT = 2; @@ -471,6 +487,22 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return pwrite_nid_disambig1_nid_postfix(d, buf, nbytes, offset); } +int64_t APS5_VABI sceKernelReadv(int, const KernelIovec*, int) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelWritev(int, const KernelIovec*, int) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelPreadv(int, const KernelIovec*, int, int64_t) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + +int64_t APS5_VABI sceKernelPwritev(int, const KernelIovec*, int, int64_t) { + throw std::runtime_error(std::string(__func__) + ": not implemented on Windows"); +} + #else int64_t APS5_VABI sceKernelPread(int d, void* buf, size_t nbytes, int64_t offset) { @@ -489,6 +521,49 @@ int64_t APS5_VABI sceKernelPwrite(int d, const void* buf, size_t nbytes, int64_t return result < 0 ? SceErrorFromErrno(errno) : result; } +static int CheckIovecs(const KernelIovec* iov, int iovcnt) { + if (iovcnt < 0 || iovcnt > KERNEL_IOV_MAX) return SceErrorFromErrno(GUEST_EINVAL); + if (iov == nullptr && iovcnt != 0) return SceErrorFromErrno(GUEST_EFAULT); + return 0; +} + +static bool OpenIovecs(const KernelIovec* iov, int iovcnt, std::deque& destinations) { + for (int i = 0; i < iovcnt; ++i) { + if (!destinations.emplace_back(iov[i].base, iov[i].length).Open()) return false; + } + return true; +} + +int64_t APS5_VABI sceKernelReadv(int d, const KernelIovec* iov, int iovcnt) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + std::deque destinations; + if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); + const auto result = static_cast(::readv(d, NativeIovecs(iov), iovcnt)); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelWritev(int d, const KernelIovec* iov, int iovcnt) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + const auto result = static_cast(::writev(d, NativeIovecs(iov), iovcnt)); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelPreadv(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + std::deque destinations; + if (!OpenIovecs(iov, iovcnt, destinations)) return SceErrorFromErrno(GUEST_EFAULT); + const auto result = static_cast(::preadv(d, NativeIovecs(iov), iovcnt, static_cast(offset))); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + +int64_t APS5_VABI sceKernelPwritev(int d, const KernelIovec* iov, int iovcnt, int64_t offset) { + if (const int error = CheckIovecs(iov, iovcnt)) return error; + if (offset < 0) return SceErrorFromErrno(GUEST_EINVAL); + const auto result = static_cast(::pwritev(d, NativeIovecs(iov), iovcnt, static_cast(offset))); + return result < 0 ? SceErrorFromErrno(errno) : result; +} + #endif int APS5_VABI sceKernelRename(const char* from, const char* to) { diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index d8d0bae8d..d2604c94d 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -9,7 +9,9 @@ #include #include #ifdef _WIN32 +#ifndef NOMINMAX #define NOMINMAX +#endif #include #else #include @@ -39,6 +41,7 @@ struct Module { std::mutex modulesMutex; std::map> modules; std::uintptr_t nextHandle = 0x20000000; +std::map imageIds; void* Symbol(Module& module, const char* name) { #ifdef _WIN32 return reinterpret_cast(GetProcAddress(static_cast(module.native), name)); @@ -139,4 +142,13 @@ int APS5_VABI dlclose_nid_postfix(void* handle) { module.reset(); return 0; } +std::int32_t ModuleIdForImage_nid_no_patch(const void* native) { + std::lock_guard lock(modulesMutex); + for (const auto& [handle, module] : modules) { + if (module->native == native) return static_cast(handle); + } + const auto [found, inserted] = imageIds.emplace(native, nextHandle); + if (inserted) ++nextHandle; + return static_cast(found->second); +} } diff --git a/core/libs/prx/libkernel/Module/src/Module.cpp b/core/libs/prx/libkernel/Module/src/Module.cpp index 3e5ba3656..1d52dc2ea 100644 --- a/core/libs/prx/libkernel/Module/src/Module.cpp +++ b/core/libs/prx/libkernel/Module/src/Module.cpp @@ -84,14 +84,6 @@ int APS5_VABI sceKernelGetModuleInfoForUnwind(uint64_t addr, int flags, ModuleIn #endif } -int APS5_VABI sceKernelGetModuleInfoFromAddr(uint64_t addr, int n, ModuleInfo* r) { - (void)addr; - (void)n; - (void)r; - NotImplemented_nid_no_patch(__func__); - return 0; -} - KernelModule APS5_VABI sceKernelLoadStartModule(const char* module_file_name, size_t args, const void* argp, uint32_t flags, const KernelLoadModuleOpt* opt, int* res) { (void)args; (void)argp; diff --git a/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp new file mode 100644 index 000000000..926f25ae5 --- /dev/null +++ b/core/libs/prx/libkernel/Module/src/ModuleInfo.cpp @@ -0,0 +1,186 @@ +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#ifndef _WIN32 +#include +#include +#include +#endif + +extern "C" std::int32_t ModuleIdForImage_nid_no_patch(const void* native); + +namespace { + +#ifndef _WIN32 +constexpr char GuestModuleSuffix[] = ".guest.prx"; +constexpr std::int32_t ProtRead = 1; +constexpr std::int32_t ProtWrite = 2; +constexpr std::int32_t ProtExecute = 4; + +struct ImageSearch { + std::uintptr_t address; + ModuleInfoEx* info; + bool found; +}; + +bool Contains(const dl_phdr_info& image, std::uintptr_t begin, std::uint64_t size) { + for (std::uint16_t i = 0; i < image.dlpi_phnum; ++i) { + const auto& header = image.dlpi_phdr[i]; + if (header.p_type != PT_LOAD) continue; + const std::uintptr_t start = image.dlpi_addr + header.p_vaddr; + if (begin >= start && begin - start <= header.p_memsz && size <= header.p_memsz - (begin - start)) return true; + } + return false; +} + +std::uint32_t ToU32(std::uint64_t value, const char* field) { + if (value > std::numeric_limits::max()) + throw std::runtime_error(std::string("sceKernelGetModuleInfoFromAddr: ") + field + " exceeds 32 bits"); + return static_cast(value); +} + +std::uintptr_t EhFrameAddress(const dl_phdr_info& image, std::uintptr_t header) { + if (!Contains(image, header, 8)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr outside the image"); + const auto* bytes = reinterpret_cast(header); + if (bytes[0] != 1) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_hdr version"); + const std::uintptr_t field = header + 4; + switch (bytes[1]) { + case 0x1b: { + std::int32_t offset; + std::memcpy(&offset, bytes + 4, sizeof(offset)); + return field + static_cast(offset); + } + case 0x03: { + std::uint32_t value; + std::memcpy(&value, bytes + 4, sizeof(value)); + return value; + } + case 0x00: { + std::uint64_t value; + if (!Contains(image, header, 12)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame_hdr outside the image"); + std::memcpy(&value, bytes + 4, sizeof(value)); + return static_cast(value); + } + default: + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: unsupported eh_frame_ptr encoding"); + } +} + +std::uint64_t EhFrameSize(const dl_phdr_info& image, std::uintptr_t frame) { + std::uintptr_t position = frame; + for (;;) { + if (!Contains(image, position, 4)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame outside the image"); + std::uint32_t length; + std::memcpy(&length, reinterpret_cast(position), sizeof(length)); + position += 4; + if (length == 0) return position - frame; + std::uint64_t recordLength = length; + if (length == 0xffffffffu) { + if (!Contains(image, position, 8)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame outside the image"); + std::memcpy(&recordLength, reinterpret_cast(position), sizeof(recordLength)); + position += 8; + } + if (!Contains(image, position, recordLength)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: eh_frame record outside the image"); + position += recordLength; + } +} + +std::string ImageName(const dl_phdr_info& image) { + std::string path = image.dlpi_name ? image.dlpi_name : ""; + if (path.empty()) { + char executable[4096]; + const auto length = ::readlink("/proc/self/exe", executable, sizeof(executable) - 1); + if (length < 0) throw std::runtime_error("sceKernelGetModuleInfoFromAddr: cannot resolve the executable path"); + path.assign(executable, static_cast(length)); + } + std::string name = path.substr(path.find_last_of('/') + 1); + if (name.size() > sizeof(GuestModuleSuffix) - 1 && name.ends_with(GuestModuleSuffix)) + name.resize(name.size() - (sizeof(GuestModuleSuffix) - 1)); + return name; +} + +void Fill(const dl_phdr_info& image, ModuleInfoEx& info) { + const auto name = ImageName(image); + if (name.size() >= sizeof(info.name)) + throw std::runtime_error("sceKernelGetModuleInfoFromAddr: module name too long: " + name); + std::memcpy(info.name, name.c_str(), name.size() + 1); + info.tls_index = ToU32(image.dlpi_tls_modid, "TLS module index"); + for (std::uint16_t i = 0; i < image.dlpi_phnum; ++i) { + const auto& header = image.dlpi_phdr[i]; + const std::uintptr_t address = image.dlpi_addr + header.p_vaddr; + if (header.p_type == PT_LOAD && info.segment_count < std::size(info.segments)) { + auto& segment = info.segments[info.segment_count++]; + segment.address = address; + segment.size = ToU32(header.p_memsz, "segment size"); + segment.prot = ((header.p_flags & PF_R) ? ProtRead : 0) | ((header.p_flags & PF_W) ? ProtWrite : 0) | ((header.p_flags & PF_X) ? ProtExecute : 0); + } else if (header.p_type == PT_TLS) { + info.tls_init_addr = address; + info.tls_init_size = ToU32(header.p_filesz, "TLS image size"); + info.tls_size = ToU32(header.p_memsz, "TLS size"); + info.tls_align = ToU32(header.p_align, "TLS alignment"); + } else if (header.p_type == PT_GNU_EH_FRAME) { + info.eh_frame_hdr_addr = address; + info.eh_frame_hdr_size = ToU32(header.p_memsz, "eh_frame_hdr size"); + info.eh_frame_addr = EhFrameAddress(image, address); + info.eh_frame_size = ToU32(EhFrameSize(image, info.eh_frame_addr), "eh_frame size"); + } else if (header.p_type == PT_DYNAMIC) { + for (const auto* entry = reinterpret_cast(address); entry->d_tag != DT_NULL; ++entry) { + if (entry->d_tag == DT_INIT) info.init_proc_addr = image.dlpi_addr + entry->d_un.d_ptr; + else if (entry->d_tag == DT_FINI) info.fini_proc_addr = image.dlpi_addr + entry->d_un.d_ptr; + } + } + } + info.ref_count = 1; +} + +int FindImage(dl_phdr_info* image, std::size_t, void* data) { + auto& search = *static_cast(data); + if (!Contains(*image, search.address, 1)) return 0; + Fill(*image, *search.info); + search.found = true; + return 1; +} +#endif + +} + +extern "C" { + +int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t address, int flags, ModuleInfoEx* info) { + if (!info) return SCE_KERNEL_ERROR_EFAULT; + if (flags != 2) throw std::invalid_argument("sceKernelGetModuleInfoFromAddr: unsupported flags " + std::to_string(flags)); + if (info->st_size != sizeof(ModuleInfoEx)) + throw std::invalid_argument("sceKernelGetModuleInfoFromAddr: unsupported st_size " + std::to_string(info->st_size)); +#ifdef _WIN32 + (void)address; + NotImplemented_nid_no_patch(__func__); + return 0; +#else + Dl_info symbol{}; + link_map* native = nullptr; + if (!dladdr1(reinterpret_cast(address), &symbol, reinterpret_cast(&native), RTLD_DL_LINKMAP) || !native) + return SCE_KERNEL_ERROR_ESRCH; + ModuleInfoEx result{}; + result.st_size = sizeof(ModuleInfoEx); + ImageSearch search{static_cast(address), &result, false}; + dl_iterate_phdr(FindImage, &search); + if (!search.found) return SCE_KERNEL_ERROR_ESRCH; + result.id = ModuleIdForImage_nid_no_patch(native); + *info = result; + return 0; +#endif +} + +} diff --git a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp index d9308e40c..5a027d874 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Attr.cpp @@ -110,4 +110,9 @@ int APS5_VABI pthread_attr_setstacksize_nid_postfix(PthreadAttr* attr, size_t st return PosixThread::ToErrno(scePthreadAttrSetstacksize(attr, stack_size)); } +int APS5_VABI pthread_attr_setsolosched_np_nid_postfix() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp index 1c363ef32..140b6900e 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp @@ -54,8 +54,7 @@ int APS5_VABI pthread_barrierattr_getpshared_nid_postfix(PthreadBarrierattrPriva int APS5_VABI pthread_barrierattr_setpshared_nid_postfix(PthreadBarrierattrPrivate** attr, int pshared) { if (!attr || !*attr) return 22; - if (pshared == PROCESS_SHARED) NotImplemented_nid_no_patch("pthread_barrierattr_setpshared: PTHREAD_PROCESS_SHARED"); - if (pshared != PROCESS_PRIVATE) return 22; + if (pshared != PROCESS_PRIVATE && pshared != PROCESS_SHARED) return 22; (*attr)->pshared = pshared; return 0; } diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index b2281ac47..66800ba33 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -6,6 +6,7 @@ #include "../include/Pthread.hpp" #include "Common.hpp" #include "prx/libkernel/KernelErrors.hpp" +#include #include extern "C" { @@ -16,6 +17,7 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadRename(Pthread thread, const char* name); Pthread APS5_VABI scePthreadSelf(); int APS5_VABI scePthreadSetcancelstate(int state, int* old_state); +void APS5_VABI scePthreadTestcancel(); int APS5_VABI scePthreadSetprio(Pthread thread, int prio); int APS5_VABI scePthreadGetprio(Pthread thread, int* prio); } @@ -93,8 +95,17 @@ int APS5_VABI pthread_setschedparam_nid_postfix(Pthread thread, int policy, cons return PosixThread::ToErrno(scePthreadSetprio(thread, param->sched_priority)); } +void APS5_VABI pthread_testcancel_nid_postfix(void) { + scePthreadTestcancel(); +} + void APS5_VABI pthread_yield_nid_postfix(void) { std::this_thread::yield(); } +unsigned int APS5_VABI sleep_nid_postfix(unsigned int seconds) { + std::this_thread::sleep_for(std::chrono::seconds(seconds)); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index b95ca4f87..ffd954255 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -99,4 +99,6 @@ struct PthreadPrivate { PthreadPrivate() : _finished(false), _retval(nullptr), _detached(false), _adopted(false) {} }; +bool GuestThreadStack(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end); + #endif diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index 5bd606db6..67f3ad67b 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -1,5 +1,6 @@ #include "../include/Pthread.hpp" #include "../include/Mutex.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include "prx/libkernel/Time/include/TimedWait.hpp" #include #include @@ -20,8 +21,12 @@ PthreadMutex destroyedMutex() { return reinterpret_cast(std::uintptr_t{2}); } +PthreadMutex adaptiveInitializer() { + return reinterpret_cast(std::uintptr_t{1}); +} + bool isInitializedMutex(PthreadMutex mutex) { - return mutex && mutex != destroyedMutex() && reinterpret_cast(mutex) != 1; + return mutex && mutex != destroyedMutex() && mutex != adaptiveInitializer(); } PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { @@ -34,13 +39,13 @@ PthreadMutex resolveMutex(PthreadMutex* mutex, bool initialize) { const auto current = slot.load(std::memory_order_acquire); if (current == destroyedMutex()) throw std::runtime_error("Mutex has been destroyed"); - if (reinterpret_cast(current) == 1) - throw std::runtime_error("Adaptive mutex initializer is unsupported"); - if (current) + if (current && current != adaptiveInitializer()) return current; if (!initialize) throw std::runtime_error("Mutex is not initialized"); auto* created = new PthreadMutexPrivate(); + if (current == adaptiveInitializer()) + created->_type = MutexType::Adaptive; slot.store(created, std::memory_order_release); return created; } @@ -110,7 +115,7 @@ int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type) { case 2: (*attr)->type = MutexType::Recursive; break; case 3: (*attr)->type = MutexType::Normal; break; case 4: (*attr)->type = MutexType::Adaptive; break; - default: throw std::invalid_argument("Invalid mutex type"); + default: return SCE_KERNEL_ERROR_EINVAL; } return 0; } @@ -144,8 +149,10 @@ int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex) { std::lock_guard lock(initializationMutex); if (*mutex == destroyedMutex()) throw std::runtime_error("Mutex has already been destroyed"); - if (reinterpret_cast(*mutex) == 1) - throw std::runtime_error("Adaptive mutex initializer is unsupported"); + if (*mutex == adaptiveInitializer()) { + std::atomic_ref(*mutex).store(destroyedMutex(), std::memory_order_release); + return 0; + } if (*mutex && (*mutex)->_owner.load(std::memory_order_acquire) != std::thread::id{}) throw std::runtime_error("Cannot destroy a locked mutex"); delete *mutex; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index a4798f886..d71ca02e8 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include #include #include #include @@ -42,26 +44,80 @@ static std::atomic threadAtexitCount{nullptr}; static std::atomic threadAtexitReport{nullptr}; static thread_local PthreadPrivate* currentThread = nullptr; static thread_local bool threadFinishing = false; +static std::mutex stackLock; +static std::map> liveStacks; -static void SetStackFromHost(PthreadPrivate* thread) { +static bool HostStackLimits(std::uintptr_t* low, std::uintptr_t* high) { #ifdef _WIN32 - ULONG_PTR low = 0; - ULONG_PTR high = 0; - GetCurrentThreadStackLimits(&low, &high); - if (high <= low) throw std::runtime_error("Cannot query the host thread stack"); - thread->stackAddress = reinterpret_cast(low); - thread->stackSize = static_cast(high - low); + ULONG_PTR lowLimit = 0; + ULONG_PTR highLimit = 0; + GetCurrentThreadStackLimits(&lowLimit, &highLimit); + *low = lowLimit; + *high = highLimit; #else pthread_attr_t attr; - if (pthread_getattr_np(pthread_self(), &attr) != 0) throw std::runtime_error("Cannot query the host thread stack"); + if (pthread_getattr_np(pthread_self(), &attr) != 0) + return false; void* address = nullptr; std::size_t size = 0; - const int result = pthread_attr_getstack(&attr, &address, &size); + const bool queried = pthread_attr_getstack(&attr, &address, &size) == 0; pthread_attr_destroy(&attr); - if (result != 0 || size == 0) throw std::runtime_error("Cannot query the host thread stack"); - thread->stackAddress = address; - thread->stackSize = size; + if (!queried) + return false; + *low = reinterpret_cast(address); + *high = *low + size; #endif + return *high > *low; +} + +static void SetStackFromHost(PthreadPrivate* thread) { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + if (!HostStackLimits(&low, &high)) throw std::runtime_error("Cannot query the host thread stack"); + thread->stackAddress = reinterpret_cast(low); + thread->stackSize = static_cast(high - low); +} + +static bool CurrentStack(std::uintptr_t* low, std::uintptr_t* high) { + if (currentThread && currentThread->stackAddress) { + *low = reinterpret_cast(currentThread->stackAddress); + *high = *low + currentThread->stackSize; + return true; + } + return HostStackLimits(low, high); +} + +static void RegisterStack(PthreadPrivate* self) { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + if (!CurrentStack(&low, &high)) + throw std::runtime_error("Cannot query guest thread stack"); + std::lock_guard lock(stackLock); + liveStacks[self] = {low, high}; +} + +static void UnregisterStack(PthreadPrivate* self) { + std::lock_guard lock(stackLock); + liveStacks.erase(self); +} + +bool GuestThreadStack(std::uintptr_t address, std::uintptr_t* start, std::uintptr_t* end) { + std::uintptr_t low = 0; + std::uintptr_t high = 0; + if (CurrentStack(&low, &high) && address >= low && address < high) { + *start = low; + *end = high; + return true; + } + std::lock_guard lock(stackLock); + for (const auto& [thread, range] : liveStacks) { + if (address >= range.first && address < range.second) { + *start = range.first; + *end = range.second; + return true; + } + } + return false; } #ifndef _WIN32 static thread_local std::unique_ptr adoptedThread; @@ -102,6 +158,7 @@ static void finishThread(PthreadPrivate* self, void* retval) { if (threadFinishing) throw std::runtime_error("Guest thread is already finishing"); threadFinishing = true; + UnregisterStack(self); if (const auto callback = threadDtors.load()) callback(); { @@ -119,6 +176,7 @@ static void RunThread(std::unique_ptr args) { PthreadPrivate* self = args->self; if (!self->stackAddress) SetStackFromHost(self); currentThread = self; + RegisterStack(self); args.reset(); finishThread(self, entry(arg)); currentThread = nullptr; @@ -444,6 +502,9 @@ int APS5_VABI scePthreadSetcanceltype(int type, int* old_type) { return SCE_OK; } +void APS5_VABI scePthreadTestcancel() { +} + int APS5_VABI scePthreadSetprio(Pthread thread, int prio) { if (!thread) return SCE_KERNEL_ERROR_EINVAL; thread->priority.store(prio, std::memory_order_relaxed); diff --git a/core/libs/prx/libkernel/System/src/Coredump.cpp b/core/libs/prx/libkernel/System/src/Coredump.cpp index 27efa3e65..84b33fec8 100644 --- a/core/libs/prx/libkernel/System/src/Coredump.cpp +++ b/core/libs/prx/libkernel/System/src/Coredump.cpp @@ -61,4 +61,14 @@ int APS5_VABI sceCoredumpGetStopInfoCpu(void) { NotImplemented_nid_no_patch("kK0DUW1Ukgc"); return 0; } + +int APS5_VABI sceCoredumpAttachUserMemoryFile(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceCoredumpAttachMemoryRegion(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libkernel/System/src/Exception.cpp b/core/libs/prx/libkernel/System/src/Exception.cpp index 8e59a6b68..ba53dbdba 100644 --- a/core/libs/prx/libkernel/System/src/Exception.cpp +++ b/core/libs/prx/libkernel/System/src/Exception.cpp @@ -2,19 +2,39 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include + +namespace { + +constexpr std::array AllowedSignals{1, 4, 8, 10, 11, 30}; + +std::mutex handlersLock; +std::array handlers{}; + +bool Allowed(int signum) { + for (const int allowed : AllowedSignals) + if (allowed == signum) return true; + return false; +} + +} extern "C" { int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler) { - (void)signum; - (void)handler; - NotImplemented_nid_no_patch(__func__); + if (!Allowed(signum) || handler == nullptr) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(handlersLock); + if (handlers[signum] != nullptr) return SCE_KERNEL_ERROR_EAGAIN; + handlers[signum] = handler; return 0; } int APS5_VABI sceKernelRemoveExceptionHandler(int signum) { - (void)signum; - NotImplemented_nid_no_patch(__func__); + if (!Allowed(signum)) return SCE_KERNEL_ERROR_EINVAL; + std::lock_guard lock(handlersLock); + handlers[signum] = nullptr; return 0; } diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index a3838db51..cc9e1301f 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -290,10 +290,8 @@ int APS5_VABI getrusage_nid_postfix(int who, GuestResourceUsage* usage) { usage->ru_nvcsw = 0; usage->ru_nivcsw = 0; #else - if (who == 1) - throw std::invalid_argument("getrusage: RUSAGE_THREAD is not supported on this platform"); struct rusage native{}; - if (::getrusage(RUSAGE_SELF, &native) != 0) + if (::getrusage(who == 0 ? RUSAGE_SELF : RUSAGE_THREAD, &native) != 0) throw std::system_error(errno, std::generic_category(), "getrusage: getrusage failed"); usage->ru_utime.tv_sec = static_cast(native.ru_utime.tv_sec); usage->ru_utime.tv_usec = static_cast(native.ru_utime.tv_usec); diff --git a/core/libs/prx/libkernel/System/src/Rtld.cpp b/core/libs/prx/libkernel/System/src/Rtld.cpp index 7a2f27d81..ad3fa2a65 100644 --- a/core/libs/prx/libkernel/System/src/Rtld.cpp +++ b/core/libs/prx/libkernel/System/src/Rtld.cpp @@ -2,16 +2,78 @@ #include #include #include +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Pthread/include/ThreadLifecycle.hpp" +#ifdef _WIN32 +#define NOMINMAX +#include +#else +#include +#include +#endif namespace { std::mutex rtldHeapMutex; std::array rtldHeapApi{}; +std::mutex threadAtexitMutex; +std::multimap threadAtexitReferences; + +const void* imageContaining(const void* address, bool reference, void*& handle, const char* caller) { + handle = nullptr; +#ifdef _WIN32 + const DWORD flags = GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | (reference ? 0 : GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT); + HMODULE module = nullptr; + if (!GetModuleHandleExW(flags, reinterpret_cast(address), &module)) + throw std::runtime_error(std::string(caller) + ": address outside every loaded image"); + if (reference) handle = module; + return module; +#else + Dl_info info{}; + link_map* image = nullptr; + if (!dladdr1(address, &info, reinterpret_cast(&image), RTLD_DL_LINKMAP) || !image) + throw std::runtime_error(std::string(caller) + ": address outside every loaded image"); + if (reference && image->l_name && *image->l_name) { + handle = dlopen(image->l_name, RTLD_LAZY | RTLD_NOLOAD); + if (!handle) throw std::runtime_error(std::string(caller) + ": cannot reference " + image->l_name); + } + return image; +#endif +} + +int referenceThreadAtexitImage(const void* address, const char* caller) { + void* handle = nullptr; + const void* image = imageContaining(address, true, handle, caller); + std::lock_guard lock(threadAtexitMutex); + threadAtexitReferences.emplace(image, handle); + return 0; +} + +int releaseThreadAtexitImage(const void* address, const char* caller) { + void* handle = nullptr; + const void* image = imageContaining(address, false, handle, caller); + { + std::lock_guard lock(threadAtexitMutex); + const auto found = threadAtexitReferences.find(image); + if (found == threadAtexitReferences.end()) + throw std::runtime_error(std::string(caller) + ": image holds no thread atexit reference"); + handle = found->second; + threadAtexitReferences.erase(found); + } + if (!handle) return 0; +#ifdef _WIN32 + if (!FreeLibrary(static_cast(handle))) +#else + if (dlclose(handle) != 0) +#endif + throw std::runtime_error(std::string(caller) + ": cannot release image"); + return 0; +} void registerApplicationHeapApi(void* const* api) { if (api == nullptr) @@ -34,16 +96,12 @@ void APS5_VABI sceKernelRtldSetApplicationHeapAPI(void* api[]) { registerApplicationHeapApi(api); } -int APS5_VABI sceKernelRtldThreadAtexitDecrement(uint64_t* c) { - (void)c; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelRtldThreadAtexitDecrement(const void* dsoSymbol) { + return releaseThreadAtexitImage(dsoSymbol, __func__); } -int APS5_VABI sceKernelRtldThreadAtexitIncrement(uint64_t* c) { - (void)c; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceKernelRtldThreadAtexitIncrement(const void* dsoSymbol) { + return referenceThreadAtexitImage(dsoSymbol, __func__); } void APS5_VABI sceKernelSetThreadAtexitCount(get_thread_atexit_count_func_t func) { @@ -66,16 +124,12 @@ void APS5_VABI _sceKernelRtldSetApplicationHeapAPI_nid_postfix(void* api[]) { registerApplicationHeapApi(api); } -int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(std::uint64_t* counter) { - (void)counter; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void* dsoSymbol) { + return releaseThreadAtexitImage(dsoSymbol, "_sceKernelRtldThreadAtexitDecrement"); } -int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(std::uint64_t* counter) { - (void)counter; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void* dsoSymbol) { + return referenceThreadAtexitImage(dsoSymbol, "_sceKernelRtldThreadAtexitIncrement"); } void APS5_VABI _sceKernelSetThreadAtexitCount_nid_postfix(get_thread_atexit_count_func_t callback) { diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 96466573a..18df01488 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -15,11 +15,6 @@ int APS5_VABI __tls_get_addr_nid_postfix(void) { return 0; } -int APS5_VABI sceKernelReleaseFlexibleMemory(void) { - NotImplemented_nid_no_patch("teiItL2boFw"); - return 0; -} - // Canonical lib is libScePosix (dead import of Cyberpunk 2077): 35 of its // 36 sibling imports resolve to libkernel, and libScePosix is not in the // game's NEEDED list so only a NEEDED module can satisfy the loader here. diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 0f08924bb..8d882fdb9 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -4,12 +4,16 @@ #include #include #include +#include #include +#include #include extern "C" { +int APS5_VABI posix_memalign_nid_postfix(void**, std::size_t, std::size_t); void* APS5_VABI _Znwm_nid_postfix(std::size_t); -void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*); +void* APS5_VABI _ZnwmRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; +void* APS5_VABI _ZnamRKSt9nothrow_t_nid_postfix(std::size_t, const void*) noexcept; void APS5_VABI _ZdlPv_nid_postfix(void*); void APS5_VABI _ZdaPv_nid_postfix(void*); void APS5_VABI _ZdlPvSt11align_val_t_nid_postfix(void*, std::size_t); @@ -17,6 +21,7 @@ void* ApplicationHeapRealign_nid_no_patch(void*, std::size_t, std::size_t); char* APS5_VABI strdup_nid_postfix(const char*); char* APS5_VABI strndup_nid_postfix(const char*, std::size_t); int* APS5_VABI __error_nid_postfix(); +int APS5_VABI atexit_nid_postfix(void (APS5_VABI*)()); } namespace { @@ -24,10 +29,12 @@ namespace { alignas(64) std::array storage{}; std::size_t lastSize = 0; std::size_t lastAlignment = 0; +unsigned posixCalls = 0; unsigned initializes = 0; unsigned frees = 0; bool fail = false; bool recurse = false; +bool nullPosixResult = false; void require(bool condition) { if (!condition) throw std::runtime_error("application heap test failed"); @@ -42,6 +49,9 @@ void reject(TAction action) { void APS5_VABI initialize() { ++initializes; } void APS5_VABI finalize() { require(initializes == 1); } +void APS5_VABI exitCallbackAllocates() { + if (ApplicationHeapAllocate_nid_no_patch(16) != storage.data()) throw std::runtime_error("application heap test failed"); +} void* APS5_VABI allocate(std::size_t bytes) { lastSize = bytes; @@ -75,7 +85,9 @@ void* APS5_VABI realign(void* pointer, std::size_t bytes, std::size_t alignment) } int APS5_VABI posixAlign(void** pointer, std::size_t alignment, std::size_t bytes) { - if (fail) return 12; + ++posixCalls; + if (fail) { *__error_nid_postfix() = 12; *pointer = nullptr; return 12; } + if (nullPosixResult) { *pointer = nullptr; return 0; } *pointer = align(alignment, bytes); return 0; } @@ -110,6 +122,12 @@ int main(int argc, char** argv) { write(replacement, 0x40, &align); write(replacement, 0x48, &realign); write(replacement, 0x50, &posixAlign); + if (argc > 1 && std::strcmp(argv[1], "exit-order") == 0) { + require(atexit_nid_postfix(exitCallbackAllocates) == 0); + ApplicationHeapInitialize_nid_no_patch(process.data()); + require(initializes == 1); + return 0; + } if (argc > 1 && std::strcmp(argv[1], "default") == 0) { std::array partial{}; partial[0] = reinterpret_cast(&allocate); @@ -184,13 +202,43 @@ int main(int argc, char** argv) { _ZdlPvSt11align_val_t_nid_postfix(nullptr, 64); require(frees == 5); fail = true; + require(_ZnwmRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); + require(_ZnamRKSt9nothrow_t_nid_postfix(8, nullptr) == nullptr); reject([] { _Znwm_nid_postfix(8); }); reject([] { ApplicationHeapAlign_nid_no_patch(4, 64); }); reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); void* unchanged = storage.data(); - reject([&] { ApplicationHeapPosixAlign_nid_no_patch(&unchanged, 64, 64); }); + *__error_nid_postfix() = 77; + try { + const int error = posix_memalign_nid_postfix(&unchanged, 64, 64); + if (error != 12) { std::fprintf(stderr, "posix_memalign callback: expected ENOMEM 12, received %d\n", error); return 1; } + } catch (const std::exception&) { + std::fputs("posix_memalign callback: expected ENOMEM 12, received exception\n", stderr); + return 1; + } + require(*__error_nid_postfix() == 77); require(unchanged == storage.data()); fail = false; + nullPosixResult = true; + bool rejectedNull = false; + try { + const int error = posix_memalign_nid_postfix(&unchanged, 64, 64); + std::fprintf(stderr, "posix_memalign callback: success with null pointer must throw, received %d\n", error); + return 1; + } catch (const std::bad_alloc&) { rejectedNull = true; } + require(rejectedNull && unchanged == storage.data()); + nullPosixResult = false; + const auto beforeInvalid = posixCalls; + for (const std::size_t alignment : {std::size_t{0}, std::size_t{1}, std::size_t{4}, std::size_t{24}}) { + require(posix_memalign_nid_postfix(&unchanged, alignment, 32) == 22); + require(unchanged == storage.data() && *__error_nid_postfix() == 77); + } + require(posix_memalign_nid_postfix(nullptr, 64, 32) == 22 && posixCalls == beforeInvalid); + require(posix_memalign_nid_postfix(&unchanged, 64, 31) == 0 && unchanged == storage.data()); + require(lastSize == 31 && lastAlignment == 64 && *__error_nid_postfix() == 77); + require(_ZnwmRKSt9nothrow_t_nid_postfix(16, nullptr) == storage.data() && lastSize == 16); + require(_ZnwmRKSt9nothrow_t_nid_postfix(0, nullptr) == storage.data() && lastSize == 1); + require(_ZnamRKSt9nothrow_t_nid_postfix(0, nullptr) == storage.data() && lastSize == 1); recurse = true; reject([] { ApplicationHeapAllocate_nid_no_patch(64); }); recurse = false; diff --git a/core/libs/tests/Formatting.cpp b/core/libs/tests/Formatting.cpp index 7550e68a9..12a89c00f 100644 --- a/core/libs/tests/Formatting.cpp +++ b/core/libs/tests/Formatting.cpp @@ -38,6 +38,34 @@ static int APS5_VABI PrintList(const char* format, ...) { return result; } +static bool CheckWidePrecision() { + const char16_t input[] = u"A\u00e9\u20ac\U0001f600Z"; + const char* expected[] = {"", "A", "A", "A\xc3\xa9", "A\xc3\xa9", "A\xc3\xa9", "A\xc3\xa9\xe2\x82\xac", + "A\xc3\xa9\xe2\x82\xac", "A\xc3\xa9\xe2\x82\xac", "A\xc3\xa9\xe2\x82\xac", + "A\xc3\xa9\xe2\x82\xac\xf0\x9f\x98\x80", "A\xc3\xa9\xe2\x82\xac\xf0\x9f\x98\x80Z"}; + bool correct = true; + for (int precision = 0; precision < 12; ++precision) { + char output[32]; + std::memset(output, '!', sizeof(output)); + const int count = snprintf_nid_postfix(output, sizeof(output), "%.*ls", precision, input); + const auto expectedSize = std::strlen(expected[precision]); + const bool matches = count == expectedSize && std::strcmp(output, expected[precision]) == 0 && output[expectedSize + 1] == '!'; + if (!matches) std::fprintf(stderr, "wide precision %d: expected %zu complete UTF-8 bytes, received %d\n", precision, expectedSize, count); + correct &= matches; + } + if (!correct) return false; + char output[32]; + Require(snprintf_nid_postfix(output, sizeof(output), "[%5.1ls]", u"\u00e9") == 7 && std::strcmp(output, "[ ]") == 0); + Require(snprintf_nid_postfix(output, sizeof(output), "[%-5.2ls]", u"\u00e9") == 7 && std::strcmp(output, "[\xc3\xa9 ]") == 0); + Require(FormatList(output, sizeof(output), "%.3ls", u"\U0001f600") == 0 && output[0] == 0); + Require(FormatList(output, sizeof(output), "%.*ls:%d", -1, u"\u00e9", 7) == 4 && std::strcmp(output, "\xc3\xa9:7") == 0); + const char16_t bounded[] = {u'A', u'B'}; + Require(snprintf_nid_postfix(output, sizeof(output), "%.2ls", bounded) == 2 && std::strcmp(output, "AB") == 0); + Require(snprintf_nid_postfix(nullptr, 0, "%.1ls", u"\u00e9") == 0); + Require(snprintf_nid_postfix(output, sizeof(output), "%.1s", "\xc3\xa9") == 1 && static_cast(output[0]) == 0xc3 && output[1] == 0); + return correct; +} + __attribute__((noinline)) static void APS5_VABI RunChecks() { char buffer[1024]; Require(FormatList(buffer, sizeof(buffer), "Mount requested: %d", 0) == 18); @@ -85,4 +113,4 @@ __attribute__((noinline)) static void APS5_VABI RunChecks() { std::puts("Formatting checks passed: 10000 iterations"); } -int main() { RunChecks(); } +int main() { RunChecks(); return CheckWidePrecision() ? 0 : 1; } diff --git a/core/libs/tests/GuestAccess.cpp b/core/libs/tests/GuestAccess.cpp index 723b9c090..0cce786ad 100644 --- a/core/libs/tests/GuestAccess.cpp +++ b/core/libs/tests/GuestAccess.cpp @@ -3,7 +3,9 @@ #include #include #ifdef _WIN32 +#ifndef NOMINMAX #define NOMINMAX +#endif #include #include #endif diff --git a/core/libs/tests/GuestAgcDriverResources.cpp b/core/libs/tests/GuestAgcDriverResources.cpp index d476831c1..eb97484c7 100644 --- a/core/libs/tests/GuestAgcDriverResources.cpp +++ b/core/libs/tests/GuestAgcDriverResources.cpp @@ -12,6 +12,7 @@ int APS5_VABI sceAgcDriverUnregisterAllResourcesForOwner(std::uint32_t); int APS5_VABI sceAgcDriverRegisterWorkloadStream(std::uint32_t, const void*); int APS5_VABI sceAgcDriverUnregisterWorkloadStream(std::uint32_t); int APS5_VABI sceAgcDriverGetDefaultOwner(std::uint32_t*); +int APS5_VABI sceAgcDriverGetResourceRegistrationMaxNameLength(std::uint32_t*); int APS5_VABI sceAgcDriverGetOwnerName(std::uint32_t, const char**); int APS5_VABI sceAgcDriverGetResourceName(std::uint32_t, const char**); int APS5_VABI sceAgcDriverGetResourceType(std::uint32_t, std::uint32_t*); @@ -37,6 +38,8 @@ int main() { Require(sceAgcDriverRegisterGdsResource(&resource, owner, 0u, 64u, "gds", 1u, 2u) == Unavailable); Require(owner == 7u && resource == 9u); Require(sceAgcDriverGetDefaultOwner(&owner) == Unavailable && owner == 7u); + std::uint32_t maxNameLength = 8u; + Require(sceAgcDriverGetResourceRegistrationMaxNameLength(&maxNameLength) == 0 && maxNameLength == 0xfcu); const char* name = "unchanged"; Require(sceAgcDriverGetOwnerName(owner, &name) == Unavailable); Require(sceAgcDriverGetResourceName(resource, &name) == Unavailable); diff --git a/core/libs/tests/GuestAio.cpp b/core/libs/tests/GuestAio.cpp index 36cad273a..7193da905 100644 --- a/core/libs/tests/GuestAio.cpp +++ b/core/libs/tests/GuestAio.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include extern "C" { int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode); @@ -20,6 +21,12 @@ int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest* req, std::int32 int APS5_VABI sceKernelAioSubmitWriteCommands(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); int APS5_VABI sceKernelAioPollRequest(std::int32_t id, std::int32_t* state); int APS5_VABI sceKernelAioWaitRequest(std::int32_t id, std::int32_t* state, std::uint32_t* usec); +int APS5_VABI sceKernelAioSubmitReadCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); +int APS5_VABI sceKernelAioSubmitWriteCommandsMultiple(KernelAioRwRequest* req, std::int32_t size, std::int32_t prio, std::int32_t* id); +int APS5_VABI sceKernelAioWaitRequests(std::int32_t* id, std::int32_t num, std::int32_t* state, std::uint32_t mode, std::uint32_t* usec); +int APS5_VABI sceKernelAioCancelRequest(std::int32_t id, std::int32_t* state); +int APS5_VABI sceKernelAioCancelRequests(std::int32_t* id, std::int32_t num, std::int32_t* state); +int APS5_VABI sceKernelAioDeleteRequests(std::int32_t* id, std::int32_t num, std::int32_t* ret); } static void Require(bool value, int line) { if (!value) { @@ -86,5 +93,90 @@ int main() { Check(sceKernelAioPollRequest(id, nullptr) == SCE_KERNEL_ERROR_EFAULT); Check(sceKernelAioPollRequest(0, &polled) == SCE_KERNEL_ERROR_EINVAL); Check(sceKernelAioDeleteRequest(id, nullptr) == SCE_KERNEL_ERROR_EFAULT); + const int batchFd = sceKernelOpen(file.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Check(batchFd >= 0); + std::array patch = {'W', 'X', 'Y', 'Z'}; + KernelAioResult patchResult{-1, 0}; + KernelAioResult badResult{-1, 0}; + KernelAioRwRequest writeBatch[2] = { + {8, patch.size(), patch.data(), &patchResult, batchFd}, + {0, patch.size(), patch.data(), &badResult, -1}, + }; + std::int32_t writeIds[2] = {0, 0}; + Check(sceKernelAioSubmitWriteCommandsMultiple(writeBatch, 2, 0, writeIds) == 0); + Check(writeIds[0] > 0 && writeIds[1] > 0 && writeIds[0] != writeIds[1]); + Check(patchResult.state == 3 && patchResult.return_value == 4); + Check(badResult.state == 4 && badResult.return_value == SCE_KERNEL_ERROR_EBADF); + std::int32_t writeStates[2] = {0, 0}; + Check(sceKernelAioWaitRequests(writeIds, 2, writeStates, 1, nullptr) == 0); + Check(writeStates[0] == 3 && writeStates[1] == 4); + std::array readBack{}; + std::array tail{}; + KernelAioResult readBackResult{-1, 0}; + KernelAioResult tailResult{-1, 0}; + KernelAioRwRequest readBatch[2] = { + {8, readBack.size(), readBack.data(), &readBackResult, batchFd}, + {60, tail.size(), tail.data(), &tailResult, batchFd}, + }; + std::int32_t readIds[2] = {0, 0}; + Check(sceKernelAioSubmitReadCommandsMultiple(readBatch, 2, 0, readIds) == 0); + Check(readIds[0] > 0 && readIds[1] > 0 && readIds[0] != readIds[1]); + Check(readBackResult.state == 3 && readBackResult.return_value == 4); + Check(std::memcmp(readBack.data(), "WXYZ", 4) == 0); + Check(tailResult.state == 3 && tailResult.return_value == 4); + Check(std::memcmp(tail.data(), seed.data() + 60, 4) == 0); + std::int32_t readStates[2] = {0, 0}; + std::uint32_t timeout = 1000; + Check(sceKernelAioWaitRequests(readIds, 2, readStates, 2, &timeout) == 0); + Check(readStates[0] == 3 && readStates[1] == 3); + std::int32_t cancelled = 0; + Check(sceKernelAioCancelRequest(readIds[0], &cancelled) == 0); + Check(cancelled == 4); + Check(sceKernelAioPollRequest(readIds[0], &polled) == 0); + Check(polled == 4); + Check(sceKernelAioCancelRequest(0, &cancelled) == 0); + Check(cancelled == 2); + std::int32_t cancelIds[2] = {0, readIds[1]}; + std::int32_t cancelStates[2] = {0, 0}; + Check(sceKernelAioCancelRequests(cancelIds, 2, cancelStates) == 0); + Check(cancelStates[0] == 2 && cancelStates[1] == 4); + Check(sceKernelAioPollRequest(readIds[1], &polled) == 0); + Check(polled == 4); + std::int32_t deletedRets[2] = {-1, -1}; + Check(sceKernelAioDeleteRequests(writeIds, 2, deletedRets) == 0); + Check(deletedRets[0] == 0 && deletedRets[1] == 0); + Check(sceKernelAioPollRequest(writeIds[0], &polled) == 0); + Check(polled == 4); + Check(sceKernelAioDeleteRequests(writeIds, 0, deletedRets) == 0); + Check(sceKernelClose(batchFd) == 0); + std::int32_t badIds[2] = {readIds[0], 9999}; + std::int32_t untouched[2] = {7, 7}; + Check(sceKernelAioSubmitReadCommandsMultiple(nullptr, 1, 0, readIds) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioSubmitReadCommandsMultiple(readBatch, 1, 0, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioSubmitWriteCommandsMultiple(writeBatch, 0, 0, writeIds) == SCE_KERNEL_ERROR_EINVAL); + KernelAioRwRequest noResult{0, 1, readBack.data(), nullptr, batchFd}; + Check(sceKernelAioSubmitWriteCommandsMultiple(&noResult, 1, 0, writeIds) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioWaitRequests(readIds, 2, nullptr, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioWaitRequests(nullptr, 2, readStates, 1, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioWaitRequests(readIds, -1, readStates, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelAioWaitRequests(badIds, 2, untouched, 1, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Check(untouched[0] == 7 && untouched[1] == 7); + bool threw = false; + try { sceKernelAioWaitRequests(readIds, 2, readStates, 3, nullptr); } catch (const std::runtime_error&) { threw = true; } + Check(threw); + threw = false; + std::array manyIds{}; + std::array manyStates{}; + manyIds.fill(readIds[0]); + try { sceKernelAioDeleteRequests(manyIds.data(), 129, manyStates.data()); } catch (const std::runtime_error&) { threw = true; } + Check(threw); + Check(sceKernelAioCancelRequest(9999, &cancelled) == SCE_KERNEL_ERROR_EINVAL); + Check(sceKernelAioCancelRequest(readIds[0], nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioCancelRequests(badIds, 2, untouched) == SCE_KERNEL_ERROR_EINVAL); + Check(untouched[0] == 7 && untouched[1] == 7); + Check(sceKernelAioCancelRequests(cancelIds, 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); + Check(sceKernelAioDeleteRequests(badIds, 2, untouched) == SCE_KERNEL_ERROR_EINVAL); + Check(untouched[0] == 7 && untouched[1] == 7); + Check(sceKernelAioDeleteRequests(writeIds, 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); Check(std::filesystem::remove_all(root) > 0); } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 0e9bb2483..b2878b8a9 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -21,11 +21,16 @@ int APS5_VABI sceAjmBatchJobDecode(AjmBatchInfo*, std::uint32_t, const void*, st int APS5_VABI sceAjmBatchJobDecodeSingle(AjmBatchInfo*, std::uint32_t, const void*, std::size_t, void*, std::size_t, void*); int APS5_VABI sceAjmBatchJobRun(AjmBatchInfo*, std::uint32_t, std::uint64_t, const void*, std::size_t, void*, std::size_t, void*, std::size_t); int APS5_VABI sceAjmBatchJobSetGaplessDecode(AjmBatchInfo*, std::uint32_t, const void*, int, void*); +int APS5_VABI sceAjmBatchJobControl(AjmBatchInfo*, std::uint32_t, std::uint64_t, const void*, std::size_t, void*, std::size_t); int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo*, std::uint32_t, void*, std::size_t); +int APS5_VABI sceAjmBatchJobGetInfo(AjmBatchInfo*, std::uint32_t, void*); int APS5_VABI sceAjmBatchStart(std::uint32_t, const AjmBatchInfo*, int, AjmBatchError*, std::uint32_t*); int APS5_VABI sceAjmBatchWait(std::uint32_t, std::uint32_t, std::uint32_t, AjmBatchError*); int APS5_VABI sceAjmBatchCancel(std::uint32_t, std::uint32_t); +int APS5_VABI sceAjmBatchJobClearContext(AjmBatchInfo*, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobSetResampleParameters(AjmBatchInfo*, std::uint32_t, float, std::uint32_t, void*); +int APS5_VABI sceAjmBatchJobGetResampleInfo(AjmBatchInfo*, std::uint32_t, void*); } static void Require(bool value) { if (!value) std::abort(); } @@ -427,6 +432,52 @@ void TestCodecInfo(std::uint32_t context) { Require(sceAjmInstanceDestroy(context, instance) == 0); } +struct FormatSideband { + std::int32_t result; + std::int32_t internalResult; + std::uint32_t numChannels; + std::uint32_t channelMask; + std::uint32_t sampleRate; + std::uint32_t sampleEncoding; + std::uint32_t bitrate; + std::uint32_t reserved; +}; + +void TestGetInfo(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::vector batch(4096); + AjmBatchInfo info{}; + FormatSideband early{-1, -1, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau, 0xaaaaaaaau}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobGetInfo(&info, instance, &early) == 0); + Submit(context, info); + Require(early.result == 1 && early.numChannels == 0xaaaaaaaau && early.sampleRate == 0xaaaaaaaau); + + const std::uint8_t config[8] = {0xFE, 0x72, 0x1F, 0xF0}; + std::int32_t initResult[2] = {-1, -1}; + FormatSideband at9{-1, -1, 0, 0, 0, 0xaaaaaaaau, 0, 0xaaaaaaaau}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, config, sizeof(config), initResult) == 0); + Require(sceAjmBatchJobGetInfo(&info, instance, &at9) == 0); + Submit(context, info); + Require(initResult[0] == 0); + Require(at9.result == 0 && at9.internalResult == 0 && at9.numChannels == 2 && at9.channelMask == 0x3 && at9.sampleRate == 48000 && at9.sampleEncoding == 0 && at9.reserved == 0); + Require(sceAjmInstanceDestroy(context, instance) == 0); + + Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); + std::vector pcm(1152); + DecodeSideband decoded{}; + FormatSideband mp3{-1, -1, 0, 0, 0, 0xaaaaaaaau, 0, 0xaaaaaaaau}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobDecodeSingle(&info, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcm.size() * sizeof(std::int16_t), &decoded) == 0); + Require(sceAjmBatchJobGetInfo(&info, instance, &mp3) == 0); + Submit(context, info); + Require(decoded.result == 0 && decoded.inputConsumed == 96); + Require(mp3.result == 0 && mp3.internalResult == 0 && mp3.numChannels == 1 && mp3.channelMask == 0x4 && mp3.sampleRate == 48000 && mp3.sampleEncoding == 0 && mp3.bitrate == 32000 && mp3.reserved == 0); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + void TestBatchWaitRelease(std::uint32_t context) { constexpr int invalidBatch = static_cast(0x80930004); std::vector batch(64); @@ -464,6 +515,367 @@ void TestBatchCancel(std::uint32_t context) { Require(sceAjmBatchCancel(context, id + 1) == invalidBatch); } +constexpr std::uint64_t CONTROL_RESET = 1ull << 13; +constexpr std::uint64_t CONTROL_INITIALIZE = 1ull << 14; +constexpr std::uint64_t SIDEBAND_GAPLESS_DECODE = 1ull << 45; +constexpr std::uint64_t CONTROL_START = CONTROL_RESET | CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE; + +struct At9Control { + GaplessDecode gapless; + std::uint8_t config[4]; + std::uint32_t reserved; +}; + +struct OpusControl { + GaplessDecode gapless; + std::uint32_t channels; + std::uint32_t sampleRate; + std::uint32_t third; +}; + +void RunControl(std::uint32_t context, std::uint32_t instance, std::uint64_t flags, const void* input, std::size_t inputSize, std::int32_t* result) { + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, flags, input, inputSize, result, 2 * sizeof(std::int32_t)) == 0); + Submit(context, info); +} + +bool ControlThrows(std::uint32_t instance, std::uint64_t flags, const void* input, std::size_t inputSize, std::size_t outputSize) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[4] = {}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + try { + static_cast(sceAjmBatchJobControl(&info, instance, flags, input, inputSize, result, outputSize)); + } catch (const std::runtime_error&) { + return info.offset == 0; + } + return false; +} + +bool ControlRefused(std::uint32_t context, std::uint32_t instance, std::uint64_t flags, const void* input, std::size_t inputSize) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, flags, input, inputSize, result, sizeof(result)) == 0); + return Refused(context, info); +} + +void TestControlAt9(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + std::vector batch(4096); + const At9Control start{{2000, 100, 0}, {0xFE, 0x72, 0x1F, 0xF0}, 0xDEADBEEFu}; + std::int32_t result[4] = {-1, -1, -1, -1}; + At9CodecInfoSideband codec{-1, -1, 0, 0, 0, 0}; + GaplessSideband gapless{-1, -1, 0, 0, 0xffff}; + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, CONTROL_START, &start, sizeof(start), result, 2 * sizeof(std::int32_t)) == 0); + Require(sceAjmBatchJobGetCodecInfo(&info, instance, &codec, sizeof(codec)) == 0); + Require(sceAjmBatchJobGetGaplessDecode(&info, instance, &gapless) == 0); + Submit(context, info); + Require(result[0] == 0 && result[1] == 0 && result[2] == -1 && result[3] == -1); + Require(codec.result == 0 && codec.superframeSize == 1024 && codec.framesInSuperframe == 4 && codec.nextFrameSize == 1024 && codec.frameSamples == 256); + Require(gapless.result == 0 && gapless.totalSamples == 2000 && gapless.skipSamples == 100 && gapless.skippedSamples == 0); + + At9Control broken = start; + broken.config[0] = 0xFD; + At9CodecInfoSideband uninitialized{-1, -1, 0, 0, 0, 0}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobControl(&info, instance, CONTROL_START, &broken, sizeof(broken), result, 2 * sizeof(std::int32_t)) == 0); + Require(sceAjmBatchJobGetCodecInfo(&info, instance, &uninitialized, sizeof(uninitialized)) == 0); + Submit(context, info); + Require(result[0] == 4 && uninitialized.result == 1); + + result[0] = -1; + RunControl(context, instance, CONTROL_INITIALIZE, start.config, sizeof(start) - sizeof(start.gapless), result); + Require(result[0] == 0); + + Require(ControlRefused(context, instance, CONTROL_INITIALIZE, &start, sizeof(start))); + Require(ControlRefused(context, instance, CONTROL_RESET | SIDEBAND_GAPLESS_DECODE, &start, sizeof(start))); + Require(ControlThrows(instance, CONTROL_INITIALIZE | SIDEBAND_GAPLESS_DECODE, &start, sizeof(start), 8)); + Require(ControlThrows(instance, CONTROL_START | (1ull << 15), &start, sizeof(start), 8)); + Require(ControlThrows(instance, CONTROL_START | (1ull << 46), &start, sizeof(start), 8)); + Require(ControlThrows(instance, CONTROL_START | (1ull << 12), &start, sizeof(start), 8)); + Require(ControlThrows(instance, 0, nullptr, 0, 8)); + Require(ControlThrows(instance, CONTROL_START, &start, sizeof(start), 16)); + Require(ControlThrows(instance, CONTROL_START, nullptr, sizeof(start), 8)); + Require(sceAjmInstanceDestroy(context, instance) == 0); + + result[0] = -1; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 4); + + const std::uint32_t aac[4] = {0, 0, 1, 0}; + Require(sceAjmInstanceCreate(context, 2, 0, &instance) == 0); + Require(ControlRefused(context, instance, CONTROL_START, aac, sizeof(aac))); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestControlMp3(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &instance) == 0); + std::vector pcm(1152); + const std::size_t pcmBytes = pcm.size() * sizeof(std::int16_t); + DecodeSideband decoded{}; + RunDecode(context, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcmBytes, decoded); + RunDecode(context, instance, MP3_MONO + 96, sizeof(MP3_MONO) - 96, pcm.data(), pcmBytes, decoded); + Require(decoded.result == 0 && decoded.totalDecodedSamples == 2 * 1152); + + const GaplessDecode skip{0, 100, 0}; + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &skip, sizeof(skip), result); + RunDecode(context, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcmBytes, decoded); + Require(result[0] == 0 && decoded.result == 0 && decoded.inputConsumed == 96 && decoded.outputWritten == (1152 - 100) * 2 && decoded.totalDecodedSamples == 1152 - 100); + + result[0] = -1; + RunControl(context, instance, CONTROL_RESET, nullptr, 0, result); + RunDecode(context, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcmBytes, decoded); + Require(result[0] == 0 && decoded.result == 0 && decoded.outputWritten == (1152 - 100) * 2 && decoded.totalDecodedSamples == 1152 - 100); + + const GaplessDecode none{0, 0, 0}; + Require(ControlThrows(instance, SIDEBAND_GAPLESS_DECODE, &none, sizeof(none), 8)); + + const GaplessDecode longer{5000, 200, 0}; + result[0] = -1; + RunControl(context, instance, CONTROL_RESET | SIDEBAND_GAPLESS_DECODE, &longer, sizeof(longer), result); + RunDecode(context, instance, MP3_MONO, sizeof(MP3_MONO), pcm.data(), pcmBytes, decoded); + Require(result[0] == 0 && decoded.result == 0 && decoded.outputWritten == (1152 - 200) * 2 && decoded.totalDecodedSamples == 1152 - 200); + + const std::uint32_t parameters[2] = {1, 0}; + Require(ControlRefused(context, instance, CONTROL_INITIALIZE, parameters, sizeof(parameters))); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestControlOpus(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const OpusControl start{{0, 0, 0}, 2, 48000, 0}; + std::int32_t result[2] = {-1, -1}; + RunControl(context, instance, CONTROL_START, &start, sizeof(start), result); + Require(result[0] == 0); + std::vector pcm(960 * 2); + DecodeSideband decoded{}; + RunDecode(context, instance, OPUS_STEREO, sizeof(OPUS_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t), decoded); + const std::size_t bytes = OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u); + Require(decoded.result == 0 && static_cast(decoded.inputConsumed) == 2 + bytes && decoded.outputWritten == 960 * 2 * 2); + Require(ControlRefused(context, instance, CONTROL_START, &start, sizeof(start) - sizeof(start.third))); + Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +struct ResampleInfo { + std::int32_t result; + std::int32_t internalResult; + float ratio; + std::int32_t samples; + std::uint32_t reserved[8]; +}; +static_assert(sizeof(ResampleInfo) == 48); + +std::uint32_t CreateOpus(std::uint32_t context) { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 24, 0, &instance) == 0); + const std::uint32_t parameters[3] = {2, 48000, 0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, parameters, sizeof(parameters), result) == 0); + Submit(context, info); + Require(result[0] == 0); + return instance; +} + +ResampleInfo GetResampleInfo(std::uint32_t context, std::uint32_t instance) { + ResampleInfo resample; + std::memset(&resample, 0xAA, sizeof(resample)); + std::vector batch(4096); + AjmBatchInfo info{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobGetResampleInfo(&info, instance, &resample) == 0); + Submit(context, info); + return resample; +} + +struct ResampledStream { + std::vector pcm; + std::size_t jobs = 0; + std::size_t shortJobs = 0; + ResampleInfo afterFirstJob{}; + ResampleInfo atEnd{}; +}; + +ResampledStream Stream(std::uint32_t context, std::uint32_t instance, const std::uint8_t* stream, std::size_t size, std::size_t channels, std::size_t frames, float ratio) { + ResampledStream out; + std::vector pcm(frames * channels); + std::size_t offset = 0; + for (int guard = 0; guard < 4096; ++guard) { + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t setResult[2] = {-1, -1}; + DecodeSideband sideband{}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + if (ratio > 0) Require(sceAjmBatchJobSetResampleParameters(&info, instance, ratio, 1, setResult) == 0); + Require(sceAjmBatchJobDecode(&info, instance, stream + offset, size - offset, pcm.data(), pcm.size() * sizeof(std::int16_t), &sideband) == 0); + Submit(context, info); + if (ratio > 0) Require(setResult[0] == 0); + Require(sideband.result == 0 || (offset == size && sideband.outputWritten == 0)); + offset += static_cast(sideband.inputConsumed); + if (offset < size && static_cast(sideband.outputWritten) < pcm.size() * sizeof(std::int16_t)) ++out.shortJobs; + out.pcm.insert(out.pcm.end(), pcm.begin(), pcm.begin() + sideband.outputWritten / static_cast(sizeof(std::int16_t))); + if (++out.jobs == 1) out.afterFirstJob = GetResampleInfo(context, instance); + if (offset == size && sideband.outputWritten == 0) break; + } + Require(offset == size); + out.atEnd = GetResampleInfo(context, instance); + return out; +} + +void RequireDecimated(const std::vector& reference, const std::vector& resampled, std::size_t channels, std::size_t step) { + const std::size_t frames = reference.size() / channels; + Require(resampled.size() / channels == (frames - 3) / step + 1); + for (std::size_t frame = 0; frame < resampled.size() / channels; ++frame) { + for (std::size_t channel = 0; channel < channels; ++channel) Require(resampled[frame * channels + channel] == reference[frame * step * channels + channel]); + } +} + +void RequireInterpolated(const std::vector& reference, const std::vector& resampled, std::size_t channels) { + const std::size_t frames = reference.size() / channels; + Require(resampled.size() / channels == 2 * frames - 4); + const auto at = [&](std::ptrdiff_t frame, std::size_t channel) { return static_cast(reference[static_cast(std::max(frame, 0)) * channels + channel]); }; + for (std::size_t frame = 0; frame < resampled.size() / channels; ++frame) { + for (std::size_t channel = 0; channel < channels; ++channel) { + const auto source = static_cast(frame / 2); + const double midpoint = (-at(source - 1, channel) + 9.0 * at(source, channel) + 9.0 * at(source + 1, channel) - at(source + 2, channel)) / 16.0; + if (frame % 2 == 0) Require(resampled[frame * channels + channel] == reference[frame / 2 * channels + channel]); + else Require(std::fabs(resampled[frame * channels + channel] - midpoint) <= 1.5); + } + } +} + +void TestResampleOpus(std::uint32_t context) { + const std::uint32_t plain = CreateOpus(context); + const auto reference = Stream(context, plain, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 960, 0); + Require(reference.pcm.size() / 2 >= 960 * 4); + + const std::uint32_t unity = CreateOpus(context); + const auto passthrough = Stream(context, unity, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 1.0f); + Require(passthrough.pcm == reference.pcm); + Require(passthrough.afterFirstJob.result == 0 && passthrough.afterFirstJob.internalResult == 0 && passthrough.afterFirstJob.ratio == 1.0f && passthrough.afterFirstJob.samples == 960 - 512); + for (const auto word : passthrough.afterFirstJob.reserved) Require(word == 0); + Require(passthrough.atEnd.samples == 0); + + const std::uint32_t faster = CreateOpus(context); + const auto decimated = Stream(context, faster, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 2.0f); + RequireDecimated(reference.pcm, decimated.pcm, 2, 2); + Require(decimated.afterFirstJob.ratio == 2.0f && decimated.afterFirstJob.samples == 2); + Require(decimated.atEnd.result == 0 && decimated.atEnd.samples >= 0 && decimated.atEnd.samples <= 2); + + const std::uint32_t slower = CreateOpus(context); + const auto interpolated = Stream(context, slower, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 2); + Require(interpolated.shortJobs == 0 && passthrough.shortJobs == 0); + Require(interpolated.afterFirstJob.ratio == 0.5f && interpolated.afterFirstJob.samples == 960 - 256); + Require(interpolated.atEnd.samples >= 0 && interpolated.atEnd.samples <= 2); + + const std::uint32_t hades = CreateOpus(context); + const auto pitched = Stream(context, hades, OPUS_STEREO, sizeof(OPUS_STEREO), 2, 512, 0.890899f); + const double expected = static_cast(reference.pcm.size() / 2 - 2) / 0.890899; + Require(std::fabs(static_cast(pitched.pcm.size() / 2) - expected) <= 2.0); + + std::vector batch(4096); + AjmBatchInfo info{}; + std::int64_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + for (const float bad : {0.0f, -1.0f, std::nanf(""), INFINITY}) Require(sceAjmBatchJobSetResampleParameters(&info, slower, bad, 1, result) == static_cast(0x80930005)); + Require(info.offset == 0); + + const std::size_t firstPacket = 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)); + const auto clear = [&] { + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobClearContext(&info, slower, result) == 0); + Submit(context, info); + Require(result[0] == 0 && GetResampleInfo(context, slower).samples == 0); + }; + clear(); + const auto before = Stream(context, slower, OPUS_STEREO, firstPacket, 2, 512, 0.5f); + Require(before.pcm.size() / 2 == 2 * 960 - 4); + clear(); + const auto after = Stream(context, slower, OPUS_STEREO, firstPacket, 2, 512, 0.5f); + Require(after.pcm == before.pcm); + + const std::uint32_t switched = CreateOpus(context); + const auto head = Stream(context, switched, OPUS_STEREO, 2 + (OPUS_STEREO[0] | (std::size_t{OPUS_STEREO[1]} << 8u)), 2, 512, 1.0f); + Require(head.jobs >= 1 && head.pcm.size() == 960 * 2); + const std::uint32_t resumed = CreateOpus(context); + std::vector pcm(512 * 2); + DecodeSideband first{}; + RunDecode(context, resumed, OPUS_STEREO, sizeof(OPUS_STEREO), pcm.data(), pcm.size() * sizeof(std::int16_t), first); + Require(first.result == 0 && first.outputWritten == 512 * 2 * 2 && GetResampleInfo(context, resumed).samples == 960 - 512); + std::vector rest(pcm.begin(), pcm.end()); + const auto tail = Stream(context, resumed, OPUS_STEREO + first.inputConsumed, sizeof(OPUS_STEREO) - static_cast(first.inputConsumed), 2, 512, 2.0f); + const std::vector later(reference.pcm.begin() + 512 * 2, reference.pcm.end()); + RequireDecimated(later, tail.pcm, 2, 2); + Require(std::equal(rest.begin(), rest.end(), reference.pcm.begin())); + + const auto unknown = GetResampleInfo(context, 0x3FFF); + Require(unknown.result == 4); + + for (const std::uint32_t instance : {plain, unity, faster, slower, hades, switched, resumed}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +void TestResampleMp3(std::uint32_t context) { + std::uint32_t plain = 0; + std::uint32_t faster = 0; + std::uint32_t slower = 0; + Require(sceAjmInstanceCreate(context, 0, 0, &plain) == 0 && sceAjmInstanceCreate(context, 0, 0, &faster) == 0 && sceAjmInstanceCreate(context, 0, 0, &slower) == 0); + const auto reference = Stream(context, plain, MP3_MONO, sizeof(MP3_MONO), 1, 1152, 0); + Require(reference.pcm.size() == 1152 * 6); + RequireDecimated(reference.pcm, Stream(context, faster, MP3_MONO, sizeof(MP3_MONO), 1, 512, 2.0f).pcm, 1, 2); + const auto interpolated = Stream(context, slower, MP3_MONO, sizeof(MP3_MONO), 1, 512, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 1); + Require(interpolated.shortJobs == 0); + for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + +const std::uint8_t AT9_MONO_SILENT_SUPERFRAME[] = { + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +void TestResampleAt9(std::uint32_t context) { + const auto create = [&] { + std::uint32_t instance = 0; + Require(sceAjmInstanceCreate(context, 1, 0, &instance) == 0); + const std::uint8_t config[8] = {0xFE, 0x70, 0x1F, 0xF0}; + std::vector batch(4096); + AjmBatchInfo info{}; + std::int32_t result[2] = {-1, -1}; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchJobInitialize(&info, instance, config, sizeof(config), result) == 0); + Submit(context, info); + Require(result[0] == 0); + return instance; + }; + std::vector stream(1024 * 3, 0); + for (std::size_t superframe = 0; superframe < 3; ++superframe) std::memcpy(stream.data() + superframe * 1024, AT9_MONO_SILENT_SUPERFRAME, sizeof(AT9_MONO_SILENT_SUPERFRAME)); + const std::uint32_t plain = create(); + const std::uint32_t faster = create(); + const std::uint32_t slower = create(); + const auto reference = Stream(context, plain, stream.data(), stream.size(), 1, 256, 0); + Require(reference.pcm.size() == 3 * 4 * 256); + const auto decimated = Stream(context, faster, stream.data(), stream.size(), 1, 512, 2.0f); + RequireDecimated(reference.pcm, decimated.pcm, 1, 2); + Require(decimated.atEnd.samples == 2); + const auto interpolated = Stream(context, slower, stream.data(), stream.size(), 1, 256, 0.5f); + RequireInterpolated(reference.pcm, interpolated.pcm, 1); + Require(interpolated.shortJobs == 0 && interpolated.afterFirstJob.samples == 256 - 256 / 2 && interpolated.atEnd.samples == 2); + for (const std::uint32_t instance : {plain, faster, slower}) Require(sceAjmInstanceDestroy(context, instance) == 0); +} + } int main() { @@ -487,7 +899,14 @@ int main() { TestDecodeSingle(context); TestGaplessDecode(context); TestCodecInfo(context); + TestGetInfo(context); TestBatchWaitRelease(context); TestBatchCancel(context); + TestControlAt9(context); + TestControlMp3(context); + TestControlOpus(context); + TestResampleOpus(context); + TestResampleMp3(context); + TestResampleAt9(context); Require(sceAjmFinalize(context) == 0); } diff --git a/core/libs/tests/GuestAppContent.cpp b/core/libs/tests/GuestAppContent.cpp index d78335075..67b4a6c68 100644 --- a/core/libs/tests/GuestAppContent.cpp +++ b/core/libs/tests/GuestAppContent.cpp @@ -5,10 +5,13 @@ extern "C" { int APS5_VABI sceAppContentAddcontMount(uint32_t, const NpUnifiedEntitlementLabel*, AppContentMountPoint*); int APS5_VABI sceAppContentAddcontUnmount(const AppContentMountPoint*); +int APS5_VABI sceAppContentGetAddcontInfo(uint32_t, const NpUnifiedEntitlementLabel*, void*); +int APS5_VABI sceAppContentGetAddcontInfoList(uint32_t, void*, uint32_t, uint32_t*); } static constexpr int ErrorParameter = static_cast(0x80D90002); static constexpr int ErrorNotFound = static_cast(0x80D90005); +static constexpr int ErrorDrmNoEntitlement = static_cast(0x80D90007); static void Require(bool value) { if (!value) std::abort(); } int main() { @@ -24,4 +27,31 @@ int main() { std::memcpy(mountPoint.data, "/addcont0", 10); Require(sceAppContentAddcontUnmount(&mountPoint) == ErrorNotFound); Require(sceAppContentAddcontUnmount(nullptr) == ErrorParameter); + + unsigned char info[24]; + std::memset(info, 0x5a, sizeof(info)); + unsigned char untouchedInfo[24]; + std::memcpy(untouchedInfo, info, sizeof(info)); + Require(sceAppContentGetAddcontInfo(0, &label, info) == ErrorDrmNoEntitlement); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetAddcontInfo(0, nullptr, info) == ErrorParameter); + Require(sceAppContentGetAddcontInfo(0, &label, nullptr) == ErrorParameter); + + uint32_t hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, &hitNum) == 0); + Require(hitNum == 0); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoList(0, info, 0, &hitNum) == 0); + Require(hitNum == 0); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, &hitNum) == 0); + Require(hitNum == 0); + hitNum = 0x5a5a5a5a; + Require(sceAppContentGetAddcontInfoList(0, info, 1, &hitNum) == 0); + Require(hitNum == 0); + Require(std::memcmp(info, untouchedInfo, sizeof(info)) == 0); + Require(sceAppContentGetAddcontInfoList(0, info, 1, nullptr) == 0); + Require(sceAppContentGetAddcontInfoList(0, nullptr, 0, nullptr) == ErrorParameter); + Require(sceAppContentGetAddcontInfoList(0, info, 0, nullptr) == ErrorParameter); + Require(sceAppContentGetAddcontInfoList(0, nullptr, 1, nullptr) == ErrorParameter); } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp index c0e58ad57..30b8c7d8a 100644 --- a/core/libs/tests/GuestAudio3dPort.cpp +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -9,21 +9,26 @@ extern "C" { void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); int APS5_VABI sceAudio3dInitialize(std::int64_t reserved); +int APS5_VABI sceAudio3dTerminate(); int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, std::uint32_t* id); int APS5_VABI sceAudio3dPortClose(std::uint32_t port_id); int APS5_VABI sceAudio3dPortSetAttribute(std::uint32_t port_id, std::uint32_t attribute_id, const void* attribute, std::size_t attribute_size); int APS5_VABI sceAudio3dPortGetQueueLevel(std::uint32_t port_id, std::uint32_t* queue_level, std::uint32_t* queue_available); int APS5_VABI sceAudio3dPortAdvance(std::uint32_t port_id); int APS5_VABI sceAudio3dPortPush(std::uint32_t port_id, std::uint32_t blocking); +int APS5_VABI sceAudio3dObjectReserve(std::uint32_t port_id, std::uint32_t* object_id); +int APS5_VABI sceAudio3dObjectUnreserve(std::uint32_t port_id, std::uint32_t object_id); } namespace { constexpr int INVALID_PORT = static_cast(0x80EA0002); +constexpr int INVALID_OBJECT = static_cast(0x80EA0003); constexpr int INVALID_PARAMETER = static_cast(0x80EA0004); constexpr int OUT_OF_RESOURCES = static_cast(0x80EA0006); constexpr int NOT_READY = static_cast(0x80EA0007); constexpr int SYSTEM_USER = 0xFF; +constexpr std::uint32_t OBJECT_INVALID = 0xFFFFFFFF; void Require(bool value, const char* message) { if (value) return; @@ -62,6 +67,7 @@ void RequireLevel(std::uint32_t level, std::uint32_t available, const char* mess void CheckBeforeInitialize() { std::uint32_t id = 7; Audio3dOpenParameters parameters = Defaults(); + Require(sceAudio3dTerminate() == NOT_READY, "terminate before initialize must be NOT_READY"); Require(sceAudio3dInitialize(1) == INVALID_PARAMETER, "non-zero reserved must be rejected"); Require(Open(parameters, &id) == NOT_READY, "open before initialize must be NOT_READY"); Require(id == 7, "a failed open must not write the port id"); @@ -138,6 +144,35 @@ void CheckAttributes() { RequireThrows([&] { sceAudio3dPortSetAttribute(0, 0x10001, &value, 8); }, "wrong attribute size must throw"); } +void CheckObjects() { + std::uint32_t object = 7; + Require(sceAudio3dObjectReserve(0, nullptr) == INVALID_PARAMETER, "null object id"); + Require(sceAudio3dObjectReserve(0, &object) == INVALID_PORT && object == OBJECT_INVALID, "reserve needs an open port"); + Require(sceAudio3dObjectUnreserve(0, 1) == INVALID_PORT, "unreserve needs an open port"); + Audio3dOpenParameters parameters = Defaults(); + parameters.max_objects = 2; + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + object = 7; + Require(sceAudio3dObjectReserve(1, &object) == INVALID_PORT && object == OBJECT_INVALID, "reserve on port 1"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 1, "first object is 1"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 2, "second object is 2"); + Require(sceAudio3dObjectReserve(0, &object) == OUT_OF_RESOURCES && object == OBJECT_INVALID, "max_objects is the limit"); + Require(sceAudio3dObjectUnreserve(1, 1) == INVALID_PORT, "unreserve on port 1"); + Require(sceAudio3dObjectUnreserve(0, 3) == INVALID_OBJECT, "unreserve of an unknown object"); + Require(sceAudio3dObjectUnreserve(0, OBJECT_INVALID) == INVALID_OBJECT, "unreserve of the invalid object"); + Require(sceAudio3dObjectUnreserve(0, 1) == 0, "unreserve"); + Require(sceAudio3dObjectUnreserve(0, 1) == INVALID_OBJECT, "double unreserve"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 3, "ids are not reused while the port is open"); + Require(sceAudio3dObjectReserve(0, &object) == OUT_OF_RESOURCES, "full again"); + Require(sceAudio3dPortClose(0) == 0, "close"); + Require(Open(parameters, &id) == 0 && id == 0, "reopen"); + Require(sceAudio3dObjectUnreserve(0, 2) == INVALID_OBJECT, "close drops the objects"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 1, "ids restart on a new port"); + Require(sceAudio3dObjectReserve(0, &object) == 0 && object == 2, "a new port has the full limit"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + void CheckQueue() { using namespace std::chrono; constexpr std::uint32_t granularity = 0x1800; @@ -177,11 +212,29 @@ void CheckQueue() { Require(sceAudio3dPortClose(0) == 0, "close"); } +void CheckTerminate() { + std::uint32_t id = 7; + const Audio3dOpenParameters parameters = Defaults(); + Require(Open(parameters, &id) == 0 && id == 0, "open before terminate"); + Require(sceAudio3dTerminate() == NOT_READY, "terminate with an open port must be NOT_READY"); + Require(sceAudio3dPortClose(0) == 0, "close before terminate"); + Require(sceAudio3dTerminate() == 0, "terminate"); + Require(sceAudio3dTerminate() == NOT_READY, "double terminate must be NOT_READY"); + id = 7; + Require(Open(parameters, &id) == NOT_READY && id == 7, "open after terminate must be NOT_READY"); + Require(sceAudio3dInitialize(0) == 0, "initialize after terminate"); + Require(Open(parameters, &id) == 0 && id == 0, "open after reinitialize"); + Require(sceAudio3dPortClose(0) == 0, "close after reinitialize"); + Require(sceAudio3dTerminate() == 0, "terminate after reinitialize"); +} + } int main() { CheckBeforeInitialize(); CheckOpenParameters(); CheckOpenClose(); + CheckObjects(); CheckQueue(); + CheckTerminate(); } diff --git a/core/libs/tests/GuestAvPlayer.cpp b/core/libs/tests/GuestAvPlayer.cpp index e9981ef53..b38ee2895 100644 --- a/core/libs/tests/GuestAvPlayer.cpp +++ b/core/libs/tests/GuestAvPlayer.cpp @@ -756,6 +756,13 @@ void TestPlayback() { Check(!sceAvPlayerGetVideoDataEx(player, &frame) && !sceAvPlayerGetAudioData(player, &sound), "data delivered while paused"); Check(sceAvPlayerCurrentTime(player) == paused, "clock moved while paused"); Check(sceAvPlayerIsActive(player), "inactive while paused"); + Check(sceAvPlayerJumpToTime(player, 300) == 0, "jump while paused failed"); + Check(WaitFor([&] { return events.Seen(EventWarning, JumpComplete); }), "jump completion missing while paused"); + Check(WaitFor([&] { return sceAvPlayerGetVideoDataEx(player, &frame) != 0; }), "no frame at a jump while paused"); + CheckVideoFrame(frame); + Check(frame.timestamp >= 300 && frame.timestamp <= 367, "paused jump landed at " + std::to_string(frame.timestamp)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + Check(!sceAvPlayerGetVideoDataEx(player, &frame) && !sceAvPlayerGetAudioData(player, &sound), "data delivered after the frame at a paused jump"); events.Clear(); Check(sceAvPlayerResume(player) == 0, "resume failed"); Check(sceAvPlayerResume(player) == OperationFailed, "resume accepted while playing"); diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp new file mode 100644 index 000000000..ed4b1976d --- /dev/null +++ b/core/libs/tests/GuestCollate.cpp @@ -0,0 +1,156 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; +std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +} + +namespace { + +std::size_t allocations = 0; +std::size_t frees = 0; +std::size_t lastSize = 0; +void* lastAllocation = nullptr; +void* lastFree = nullptr; + +void require(bool condition) { + if (!condition) std::abort(); +} + +template +void reject(TAction action) { + bool rejected = false; + try { action(); } catch (const std::exception&) { rejected = true; } + require(rejected); +} + +void* APS5_VABI allocate(std::size_t bytes) { + ++allocations; + lastSize = bytes; + lastAllocation = std::malloc(bytes); + return lastAllocation; +} + +void APS5_VABI release(void* pointer) { + ++frees; + lastFree = pointer; + std::free(pointer); +} + +void* APS5_VABI allocateZeroed(std::size_t, std::size_t) { std::abort(); } +void* APS5_VABI reallocate(void*, std::size_t) { std::abort(); } +void* APS5_VABI align(std::size_t, std::size_t) { std::abort(); } +void* APS5_VABI realign(void*, std::size_t, std::size_t) { std::abort(); } +int APS5_VABI posixAlign(void**, std::size_t, std::size_t) { std::abort(); } + +const GuestLocale::CollateVtable& Vtable(const GuestLocale::Facet* facet) { + return *reinterpret_cast(facet->vtable); +} + +int Compare(const GuestLocale::CollateFacet* facet, const std::string& left, const std::string& right) { + return Vtable(&facet->base).compare(facet, left.data(), left.data() + left.size(), right.data(), right.data() + right.size()); +} + +std::uint64_t Hash(const GuestLocale::CollateFacet* facet, const std::string& text) { + return static_cast(Vtable(&facet->base).hash(facet, text.data(), text.data() + text.size())); +} + +} + +int main() { + const std::array api{reinterpret_cast(&allocate), reinterpret_cast(&release), reinterpret_cast(&allocateZeroed), reinterpret_cast(&reallocate), reinterpret_cast(&align), reinterpret_cast(&realign), reinterpret_cast(&posixAlign)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + + const GuestLocale::Implementation* const* classic = &_ZSt21_sceLibcClassicLocale_nid_postfix; + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(nullptr, classic) == 1); + GuestLocale::Facet existing{}; + GuestLocale::Facet* facet = &existing; + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet == &existing && allocations == 0); + + GuestLocale::Implementation french = *_ZSt21_sceLibcClassicLocale_nid_postfix; + french.name = "fr_FR"; + const GuestLocale::Implementation* frenchPointer = &french; + facet = nullptr; + reject([&] { _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, &frenchPointer); }); + require(facet == nullptr && allocations == 0); + + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet != nullptr && allocations == 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); + const auto* collate = reinterpret_cast(facet); + require(facet->references == 0 && collate->collation == nullptr && collate->wideCollation == nullptr); + + require(Compare(collate, "abc", "abd") == -1); + require(Compare(collate, "abd", "abc") == 1); + require(Compare(collate, "a", "c") == -1); + require(Compare(collate, "abc", "abc") == 0); + require(Compare(collate, "ab", "abc") == -1); + require(Compare(collate, "abc", "ab") == 1); + require(Compare(collate, "", "") == 0); + require(Compare(collate, "", "a") == -1); + require(Compare(collate, "\x80", "a") == 1); + require(Compare(collate, std::string("a\0", 2), "a") == 1); + require(Compare(collate, std::string("a\0b", 3), std::string("a\0c", 3)) == -1); + require(Vtable(facet).compare(collate, nullptr, nullptr, nullptr, nullptr) == 0); + const char text[] = "abc"; + reject([&] { Vtable(facet).compare(collate, text + 2, text, text, text + 1); }); + reject([&] { Vtable(facet).compare(collate, nullptr, text, text, text + 1); }); + + require(Hash(collate, "") == 0xcbf29ce484222325ull); + require(Hash(collate, "a") == 0xaf63dc4c8601ec8cull); + require(Hash(collate, "foobar") == 0x85944171f73967e8ull); + require(Hash(collate, "\xff") == ((0xcbf29ce484222325ull ^ 0xffull) * 0x100000001b3ull)); + reject([&] { Vtable(facet).hash(collate, text + 1, text); }); + + GuestLocale::String result; + std::memset(&result, 0xcd, sizeof(result)); + const std::string shortText = "hello"; + require(Vtable(facet).transform(&result, collate, shortText.data(), shortText.data() + shortText.size()) == &result); + require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 5 && result.capacity == 15 && std::strcmp(result.buffer, "hello") == 0); + require(allocations == 1); + + std::memset(&result, 0xcd, sizeof(result)); + require(Vtable(facet).transform(&result, collate, nullptr, nullptr) == &result); + require(result.size == 0 && result.capacity == 15 && result.buffer[0] == 0); + + const std::string fifteen(15, 'q'); + require(Vtable(facet).transform(&result, collate, fifteen.data(), fifteen.data() + fifteen.size()) == &result); + require(result.size == 15 && result.capacity == 15 && std::string(result.buffer) == fifteen && allocations == 1); + + const std::string longText = "collation of a longer text"; + require(Vtable(facet).transform(&result, collate, longText.data(), longText.data() + longText.size()) == &result); + require(allocations == 2 && lastSize == longText.size() + 1 && result.pointer == lastAllocation); + require(result.size == longText.size() && result.capacity == longText.size() && std::string(result.pointer) == longText); + std::free(result.pointer); + + const std::string largest(4094, 'z'); + require(Vtable(facet).transform(&result, collate, largest.data(), largest.data() + largest.size()) == &result); + require(allocations == 3 && lastSize == 4095 && result.capacity == 4094 && std::string(result.pointer) == largest); + std::free(result.pointer); + + const std::string tooLong(4095, 'z'); + reject([&] { Vtable(facet).transform(&result, collate, tooLong.data(), tooLong.data() + tooLong.size()); }); + const std::string embedded("a\0b", 3); + reject([&] { Vtable(facet).transform(&result, collate, embedded.data(), embedded.data() + embedded.size()); }); + reject([&] { Vtable(facet).transform(nullptr, collate, text, text + 1); }); + require(allocations == 3); + + Vtable(facet).facet.retain(facet); + Vtable(facet).facet.retain(facet); + require(facet->references == 2); + require(Vtable(facet).facet.release(facet) == nullptr && facet->references == 1); + require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); + Vtable(facet).facet.destroy(facet); + require(frees == 0); + Vtable(facet).facet.deleteObject(facet); + require(frees == 1 && lastFree == collate); + return 0; +} diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 0b27282bb..1b3172e67 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -2,14 +2,26 @@ #include #include #include +#include #include extern "C" { void* APS5_VABI dlopen_nid_postfix(const char*, int); void* APS5_VABI dlsym_nid_postfix(void*, const char*); int APS5_VABI dlclose_nid_postfix(void*); char* APS5_VABI dlerror_nid_postfix(); +int APS5_VABI _sceKernelRtldThreadAtexitIncrement_nid_postfix(const void*); +int APS5_VABI _sceKernelRtldThreadAtexitDecrement_nid_postfix(const void*); } static void Require(bool value) { if (!value) std::abort(); } +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} int main(int argc, char** argv) { Require(argc == 2); Require(dlerror_nid_postfix() == nullptr); @@ -61,4 +73,19 @@ int main(int argc, char** argv) { Require(dlclose_nid_postfix(redirected) == 0); std::filesystem::remove(guest); std::filesystem::remove(relinked); + + void* pinned = dlopen_nid_postfix(argv[1], 2); + add = reinterpret_cast(dlsym_nid_postfix(pinned, "GuestModuleAdd")); + Require(add && _sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); + Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(add)) == 0); + Require(dlclose_nid_postfix(pinned) == 0); + Require(add(20, 22) == 42); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(add)) == 0); + Require(add(40, 2) == 42); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(add)) == 0); + Require(_sceKernelRtldThreadAtexitIncrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); + Require(_sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&ThrowsRuntimeError)) == 0); + Require(ThrowsRuntimeError([] { _sceKernelRtldThreadAtexitDecrement_nid_postfix(reinterpret_cast(&Require)); })); + int local = 0; + Require(ThrowsRuntimeError([&] { _sceKernelRtldThreadAtexitIncrement_nid_postfix(&local); })); } diff --git a/core/libs/tests/GuestEqueueEvents.cpp b/core/libs/tests/GuestEqueueEvents.cpp index 4731605d0..b0f74d8cc 100644 --- a/core/libs/tests/GuestEqueueEvents.cpp +++ b/core/libs/tests/GuestEqueueEvents.cpp @@ -1,7 +1,9 @@ #include "SceTypes.hpp" +#include #include #include #include +#include extern "C" { int APS5_VABI sceKernelCreateEqueue(KernelEqueue* eq, const char* name); @@ -11,6 +13,8 @@ int APS5_VABI sceKernelAddUserEvent(KernelEqueue eq, int id); int APS5_VABI sceKernelTriggerUserEvent(KernelEqueue eq, int id, void* udata); int APS5_VABI sceKernelDeleteUserEvent(KernelEqueue eq, int id); int APS5_VABI sceKernelAddHRTimerEvent(KernelEqueue eq, int id, const KernelTimespec* ts, void* udata); +int APS5_VABI sceKernelAddTimerEvent(KernelEqueue eq, int id, KernelUseconds usec, void* udata); +int APS5_VABI sceKernelDeleteTimerEvent(KernelEqueue eq, int id); intptr_t APS5_VABI sceKernelGetEventData(const KernelEvent* ev); intptr_t APS5_VABI sceKernelGetEventFflags(const KernelEvent* ev); int APS5_VABI sceKernelGetEventFilter(const KernelEvent* ev); @@ -19,6 +23,10 @@ void* APS5_VABI sceKernelGetEventUserData(const KernelEvent* ev); } static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_ENOENT = static_cast(0x80020002); +static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); +static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003c); +static constexpr int EVFILT_TIMER = -7; static constexpr int EVFILT_USER = -11; static constexpr int EVFILT_HRTIMER = -15; @@ -34,7 +42,91 @@ static bool RejectsNull(TResult (APS5_VABI *accessor)(const KernelEvent*)) { return false; } +static void SleepAtLeast(std::chrono::steady_clock::duration duration) { + const auto start = std::chrono::steady_clock::now(); + while (std::chrono::steady_clock::now() - start < duration) { + std::this_thread::sleep_for(duration); + } +} + +static void VerifyPeriodicTimer() { + using namespace std::chrono_literals; + int first = 0; + int second = 0; + Require(sceKernelAddTimerEvent(0, 10, 1000, &first) == SCE_KERNEL_ERROR_EBADF); + Require(sceKernelDeleteTimerEvent(0, 10) == SCE_KERNEL_ERROR_EBADF); + + KernelEqueue eq = 0; + Require(sceKernelCreateEqueue(&eq, "timer") == SCE_OK); + Require(sceKernelDeleteTimerEvent(eq, 10) == SCE_KERNEL_ERROR_ENOENT); + + KernelEvent events[2]{}; + int count = 0; + const KernelUseconds poll = 0; + const KernelUseconds wait = 1000000; + Require(sceKernelAddTimerEvent(eq, 10, 1000, &first) == SCE_OK); + SleepAtLeast(5ms); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_OK); + Require(count == 1); + Require(sceKernelGetEventId(&events[0]) == 10); + Require(sceKernelGetEventFilter(&events[0]) == EVFILT_TIMER); + Require(sceKernelGetEventData(&events[0]) >= 5); + Require(sceKernelGetEventUserData(&events[0]) == &first); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &wait) == SCE_OK); + Require(count == 1 && sceKernelGetEventData(&events[0]) >= 1); + + SleepAtLeast(5ms); + Require(sceKernelAddTimerEvent(eq, 10, 3000000000u, &second) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_OK); + Require(count == 1); + Require(sceKernelGetEventData(&events[0]) >= 5); + Require(sceKernelGetEventUserData(&events[0]) == &second); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + Require(sceKernelDeleteTimerEvent(eq, 10) == SCE_OK); + Require(sceKernelDeleteTimerEvent(eq, 10) == SCE_KERNEL_ERROR_ENOENT); + + Require(sceKernelAddTimerEvent(eq, 13, 3000000000u, &first) == SCE_OK); + const auto readded = std::chrono::steady_clock::now(); + Require(sceKernelAddTimerEvent(eq, 13, 200000, &first) == SCE_OK); + SleepAtLeast(250ms); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_OK); + Require(count == 1 && sceKernelGetEventData(&events[0]) >= 1); + const intptr_t expirations = sceKernelGetEventData(&events[0]); + const int again = sceKernelWaitEqueue(eq, events, 2, &count, &poll); + if (std::chrono::steady_clock::now() - readded < 400ms) { + Require(expirations == 1 && again == SCE_KERNEL_ERROR_ETIMEDOUT); + } + Require(sceKernelWaitEqueue(eq, events, 2, &count, &wait) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 13); + Require(sceKernelDeleteTimerEvent(eq, 13) == SCE_OK); + + Require(sceKernelAddTimerEvent(eq, 14, 1000, &first) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, nullptr) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 14 && sceKernelGetEventData(&events[0]) >= 1); + Require(sceKernelDeleteTimerEvent(eq, 14) == SCE_OK); + const KernelTimespec soon{0, 1000000}; + Require(sceKernelAddHRTimerEvent(eq, 15, &soon, &second) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, nullptr) == SCE_OK); + Require(count == 1 && sceKernelGetEventId(&events[0]) == 15 && sceKernelGetEventFilter(&events[0]) == EVFILT_HRTIMER); + + Require(sceKernelAddTimerEvent(eq, 11, 0, &first) == SCE_OK); + Require(sceKernelAddTimerEvent(eq, 11, 0, &first) == SCE_OK); + for (int i = 0; i < 2; ++i) { + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_OK); + Require(count == 1 && sceKernelGetEventData(&events[0]) == 1); + } + Require(sceKernelDeleteTimerEvent(eq, 11) == SCE_OK); + Require(sceKernelWaitEqueue(eq, events, 2, &count, &poll) == SCE_KERNEL_ERROR_ETIMEDOUT); + + const KernelTimespec delay{0, 1000000}; + Require(sceKernelAddHRTimerEvent(eq, 12, &delay, &first) == SCE_OK); + Require(sceKernelDeleteTimerEvent(eq, 12) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelDeleteEqueue(eq) == SCE_OK); +} + int main() { + VerifyPeriodicTimer(); + KernelEqueue eq = 0; Require(sceKernelCreateEqueue(&eq, "events") == SCE_OK); const KernelUseconds timeout = 1000000; diff --git a/core/libs/tests/GuestExceptionHandler.cpp b/core/libs/tests/GuestExceptionHandler.cpp new file mode 100644 index 000000000..fbb5dfd8f --- /dev/null +++ b/core/libs/tests/GuestExceptionHandler.cpp @@ -0,0 +1,33 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceKernelInstallExceptionHandler(int signum, void* handler); +int APS5_VABI sceKernelRemoveExceptionHandler(int signum); +} + +static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_EAGAIN = static_cast(0x80020023); + +static void APS5_VABI Handler(int, void*) {} +static void APS5_VABI Other(int, void*) {} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + auto* handler = reinterpret_cast(&Handler); + auto* other = reinterpret_cast(&Other); + for (const int rejected : {0, 2, 9, 31, 128, -1}) + Require(sceKernelInstallExceptionHandler(rejected, handler) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelInstallExceptionHandler(30, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelRemoveExceptionHandler(9) == SCE_KERNEL_ERROR_EINVAL); + for (const int signum : {1, 4, 8, 10, 11, 30}) { + Require(sceKernelInstallExceptionHandler(signum, handler) == 0); + Require(sceKernelInstallExceptionHandler(signum, other) == SCE_KERNEL_ERROR_EAGAIN); + Require(sceKernelRemoveExceptionHandler(signum) == 0); + Require(sceKernelInstallExceptionHandler(signum, other) == 0); + Require(sceKernelRemoveExceptionHandler(signum) == 0); + } + Require(sceKernelRemoveExceptionHandler(30) == 0); +} diff --git a/core/libs/tests/GuestFileVector.cpp b/core/libs/tests/GuestFileVector.cpp new file mode 100644 index 000000000..a3cb50f35 --- /dev/null +++ b/core/libs/tests/GuestFileVector.cpp @@ -0,0 +1,111 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct GuestIovec { + void* base; + std::size_t length; +}; + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI sceKernelLseek(int, std::int64_t, int); +std::int64_t APS5_VABI sceKernelReadv(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelWritev(int, const GuestIovec*, int); +std::int64_t APS5_VABI sceKernelPreadv(int, const GuestIovec*, int, std::int64_t); +std::int64_t APS5_VABI sceKernelPwritev(int, const GuestIovec*, int, std::int64_t); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "File vector check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static constexpr std::int64_t ErrorEbadf = static_cast(0x80020009u); +static constexpr std::int64_t ErrorEfault = static_cast(0x8002000Eu); +static constexpr std::int64_t ErrorEinval = static_cast(0x80020016u); + +static std::string Contents(const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + return std::string(std::istreambuf_iterator(stream), std::istreambuf_iterator()); +} + +int main() { + const auto root = std::filesystem::path("anyps5-file-vector-test-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + const auto path = root / "data.bin"; + { std::ofstream stream(path, std::ios::binary); stream << "0123456789"; } + + const int file = sceKernelOpen(path.string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(file >= 0); + + char first[3] = {}; + char second[4] = {}; + GuestIovec reads[2] = {{first, sizeof(first)}, {second, sizeof(second)}}; + Require(sceKernelReadv(file, reads, 2) == 7); + Require(std::memcmp(first, "012", 3) == 0 && std::memcmp(second, "3456", 4) == 0); + Require(sceKernelLseek(file, 0, 1) == 7); + Require(sceKernelReadv(file, reads, 2) == 3); + Require(std::memcmp(first, "789", 3) == 0); + Require(sceKernelReadv(file, reads, 2) == 0); + + Require(sceKernelPreadv(file, reads, 2, 1) == 7); + Require(std::memcmp(first, "123", 3) == 0 && std::memcmp(second, "4567", 4) == 0); + Require(sceKernelLseek(file, 0, 1) == 10); + Require(sceKernelPreadv(file, reads, 2, 8) == 2); + Require(std::memcmp(first, "89", 2) == 0); + + char ab[] = "AB"; + char cde[] = "CDE"; + GuestIovec writes[2] = {{ab, 2}, {cde, 3}}; + Require(sceKernelPwritev(file, writes, 2, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 10); + Require(Contents(path) == "01ABCDE789"); + Require(sceKernelLseek(file, 8, 0) == 8); + Require(sceKernelWritev(file, writes, 2) == 5); + Require(sceKernelLseek(file, 0, 1) == 13); + Require(Contents(path) == "01ABCDE7ABCDE"); + + GuestIovec empty[1] = {{nullptr, 0}}; + Require(sceKernelReadv(file, empty, 1) == 0); + Require(sceKernelReadv(file, reads, 0) == 0); + Require(sceKernelWritev(file, nullptr, 0) == 0); + + Require(sceKernelReadv(file, reads, -1) == ErrorEinval); + Require(sceKernelWritev(file, writes, -1) == ErrorEinval); + Require(sceKernelPreadv(file, reads, -1, 0) == ErrorEinval); + Require(sceKernelPwritev(file, writes, -1, 0) == ErrorEinval); + Require(sceKernelReadv(file, reads, 1025) == ErrorEinval); + Require(sceKernelWritev(file, writes, 1025) == ErrorEinval); + Require(sceKernelReadv(file, nullptr, 1) == ErrorEfault); + Require(sceKernelWritev(file, nullptr, 1) == ErrorEfault); + Require(sceKernelPreadv(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelPwritev(file, nullptr, 1, 0) == ErrorEfault); + Require(sceKernelPreadv(file, reads, 2, -1) == ErrorEinval); + Require(sceKernelPwritev(file, writes, 2, -1) == ErrorEinval); + Require(Contents(path) == "01ABCDE7ABCDE"); + + Require(sceKernelClose(file) == 0); + Require(sceKernelReadv(file, reads, 2) == ErrorEbadf); + Require(sceKernelWritev(file, writes, 2) == ErrorEbadf); + Require(sceKernelPreadv(file, reads, 2, 0) == ErrorEbadf); + Require(sceKernelPwritev(file, writes, 2, 0) == ErrorEbadf); + + std::filesystem::remove_all(root); + return 0; +} diff --git a/core/libs/tests/GuestFiopen.cpp b/core/libs/tests/GuestFiopen.cpp index 379cda811..6c2636ae3 100644 --- a/core/libs/tests/GuestFiopen.cpp +++ b/core/libs/tests/GuestFiopen.cpp @@ -1,15 +1,136 @@ +#include "prx/libc/include/FileStream.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include #include -#include -#include +#include +#include +#include +#include -extern "C" std::FILE* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); +extern "C" { +FileStream* APS5_VABI _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(const char*, int, int); +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +int APS5_VABI fclose_nid_postfix(FileStream*); +std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Fiopen check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +constexpr int In = 0x01, Out = 0x02, Ate = 0x04, App = 0x08, Trunc = 0x10, Nocreate = 0x20, Noreplace = 0x40, + Binary = 0x80; + +static FileStream* Open(const char* name, int mode) { + return _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix(name, mode, 0x1b6); +} + +static std::string Contents(const char* name) { + auto* stream = fopen_nid_postfix(name, "rb"); + Require(stream != nullptr); + std::string text; + for (int c; (c = std::fgetc(stream->GetHandle())) != EOF;) text += static_cast(c); + fclose_nid_postfix(stream); + return text; +} + +static void Write(FileStream* stream, const char* text) { + Require(stream != nullptr); + Require(std::fputs(text, stream->GetHandle()) >= 0); + fclose_nid_postfix(stream); +} + +static bool Exists(const char* name) { + auto* stream = fopen_nid_postfix(name, "r"); + if (stream) fclose_nid_postfix(stream); + return stream != nullptr; +} int main() { - try { - _ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi_nid_postfix("unused", 1, 0); - } catch (const std::runtime_error& error) { - return std::string_view(error.what()) == "std::_Fiopen not implemented" ? 0 : 1; + const std::filesystem::path dir = "guest_fiopen_dir"; + std::filesystem::remove_all(dir); + std::filesystem::create_directory(dir); + const char* file = "guest_fiopen_dir/a"; + const char* missing = "guest_fiopen_dir/missing"; + + for (int mode : {0, Ate, Trunc, Binary, Nocreate, Noreplace, In | Trunc, In | Trunc | Binary, Out | App | Trunc, + In | Out | App | Trunc, Trunc | Binary, App | Trunc}) { + Require(Open(missing, mode) == nullptr); + Require(!Exists(missing)); } - return 1; + + Write(Open(file, Out), "abc"); + Require(Contents(file) == "abc"); + + auto* stream = Open(file, In); + Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'a'); + fclose_nid_postfix(stream); + stream = Open(file, In); + Require(stream != nullptr); + std::fputc('x', stream->GetHandle()); + fclose_nid_postfix(stream); + Require(Contents(file) == "abc"); + + Write(Open(file, App), "d"); + Require(Contents(file) == "abcd"); + Write(Open(file, Out | App | Binary), "e"); + Require(Contents(file) == "abcde"); + + stream = Open(file, In | Ate); + Require(stream != nullptr && ftello_nid_postfix(stream) == 5); + fclose_nid_postfix(stream); + stream = Open(file, In | Binary); + Require(stream != nullptr && ftello_nid_postfix(stream) == 0); + fclose_nid_postfix(stream); + + Require(Open(file, Out | Noreplace) == nullptr); + Require(Open(file, In | Out | Binary | Noreplace) == nullptr); + Require(Open(file, App | Noreplace) == nullptr); + Require(Contents(file) == "abcde"); + stream = Open(file, In | Noreplace); + Require(stream != nullptr); + fclose_nid_postfix(stream); + + Require(Open(missing, Out | Nocreate) == nullptr); + Require(!Exists(missing)); + Write(Open(file, Out | Nocreate), "X"); + Require(Contents(file) == "Xbcde"); + stream = Open(file, Out | Nocreate | Binary | Ate); + Require(stream != nullptr && ftello_nid_postfix(stream) == 5); + Write(stream, "f"); + Require(Contents(file) == "Xbcdef"); + + stream = Open(file, In | Out); + Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'X'); + fclose_nid_postfix(stream); + Require(Contents(file) == "Xbcdef"); + + Write(Open(file, In | Out | App), "g"); + Require(Contents(file) == "Xbcdefg"); + + Write(Open(file, In | Out | Trunc | Binary), "h"); + Require(Contents(file) == "h"); + Write(Open(file, Out | Trunc), "ij"); + Require(Contents(file) == "ij"); + Write(Open(file, Out | Binary), "k"); + Require(Contents(file) == "k"); + + Write(Open(missing, Out | Noreplace), "new"); + Require(Contents(missing) == "new"); + Write(Open(missing, Out | Trunc | Nocreate), "w+"); + Require(Contents(missing) == "w+"); + std::filesystem::remove(dir / "missing"); + Write(Open(missing, Out | Trunc | Nocreate), "created"); + Require(Contents(missing) == "created"); + + stream = Open(file, In | 0x100 | 0x8000); + Require(stream != nullptr && std::fgetc(stream->GetHandle()) == 'k'); + fclose_nid_postfix(stream); + + std::filesystem::remove_all(dir); + return 0; } diff --git a/core/libs/tests/GuestFont.cpp b/core/libs/tests/GuestFont.cpp index 74ebc5694..7fd375b21 100644 --- a/core/libs/tests/GuestFont.cpp +++ b/core/libs/tests/GuestFont.cpp @@ -29,6 +29,9 @@ int APS5_VABI sceFontUnbindRenderer(FontHandle); int APS5_VABI sceFontSetupRenderScalePixel(FontHandle, float, float); int APS5_VABI sceFontSetupRenderScalePoint(FontHandle, float, float); int APS5_VABI sceFontGetKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); +int APS5_VABI sceFontGetFontGlyphsCount(FontHandle, std::uint32_t*); +int APS5_VABI sceFontGetCharGlyphCode(FontHandle, std::uint32_t, std::uint32_t*); +int APS5_VABI sceFontGetFontResolution(FontHandle, std::uint32_t*, float*); int APS5_VABI sceFontGetRenderScaledKerning(FontHandle, std::uint32_t, std::uint32_t, FontKerning*); const void* APS5_VABI sceFontSelectLibraryFt(int); const void* APS5_VABI sceFontSelectRendererFt(int); @@ -171,6 +174,32 @@ int main() { hDpi = 1; vDpi = 1; Require(sceFontGetResolutionDpi(&unopened, &hDpi, &vDpi) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && hDpi == 0 && vDpi == 0); + std::uint32_t glyphsCount = 0; + Require(sceFontGetFontGlyphsCount(font, &glyphsCount) == SCE_FONT_OK && glyphsCount == 1); + Require(sceFontGetFontGlyphsCount(font, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + glyphsCount = 1; + Require(sceFontGetFontGlyphsCount(nullptr, &glyphsCount) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphsCount == 0); + glyphsCount = 1; + Require(sceFontGetFontGlyphsCount(&unopened, &glyphsCount) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphsCount == 0); + std::uint32_t glyphCode = 1; + Require(sceFontGetCharGlyphCode(font, 'A', &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH && glyphCode == 0); + Require(sceFontGetCharGlyphCode(font, 'A', nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + glyphCode = 1; + Require(sceFontGetCharGlyphCode(nullptr, 'A', &glyphCode) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphCode == 0); + glyphCode = 1; + Require(sceFontGetCharGlyphCode(&unopened, 'A', &glyphCode) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && glyphCode == 0); + std::uint32_t resolution = 0; + float scalePixel = 0.0f; + Require(sceFontGetFontResolution(font, &resolution, &scalePixel) == SCE_FONT_OK && resolution == 1000 && scalePixel == 15.625f); + resolution = 0; + Require(sceFontGetFontResolution(font, &resolution, nullptr) == SCE_FONT_OK && resolution == 1000); + scalePixel = 0.0f; + Require(sceFontGetFontResolution(font, nullptr, &scalePixel) == SCE_FONT_OK && scalePixel == 15.625f); + Require(sceFontGetFontResolution(font, nullptr, nullptr) == SCE_FONT_ERROR_INVALID_PARAMETER); + Require(sceFontGetFontResolution(nullptr, &resolution, &scalePixel) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && resolution == 0 && scalePixel == 0.0f); + resolution = 1; + scalePixel = 1.0f; + Require(sceFontGetFontResolution(&unopened, &resolution, &scalePixel) == SCE_FONT_ERROR_INVALID_FONT_HANDLE && resolution == 0 && scalePixel == 0.0f); Require(sceFontCloseFont(font) == SCE_FONT_OK); FontRenderer renderer = nullptr; @@ -179,6 +208,13 @@ int main() { const std::vector kerningData = KerningFont(); font = nullptr; Require(sceFontOpenFontMemory(library, kerningData.data(), static_cast(kerningData.size()), nullptr, &font) == SCE_FONT_OK && font != nullptr); + Require(sceFontGetFontGlyphsCount(font, &glyphsCount) == SCE_FONT_OK && glyphsCount == 3); + Require(sceFontGetCharGlyphCode(font, 'A', &glyphCode) == SCE_FONT_OK && glyphCode == 1); + Require(sceFontGetCharGlyphCode(font, 'V', &glyphCode) == SCE_FONT_OK && glyphCode == 2); + glyphCode = 1; + Require(sceFontGetCharGlyphCode(font, 'B', &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH && glyphCode == 0); + glyphCode = 1; + Require(sceFontGetCharGlyphCode(font, 0, &glyphCode) == SCE_FONT_ERROR_NO_SUPPORT_CODE && glyphCode == 0); Require(sceFontSetResolutionDpi(font, 144, 144) == SCE_FONT_OK); Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); FontKerning kerning{1.0f, 1.0f, 1.0f, 1.0f}; diff --git a/core/libs/tests/GuestFontWritingLine.cpp b/core/libs/tests/GuestFontWritingLine.cpp index 40281cd0f..7d5e40f08 100644 --- a/core/libs/tests/GuestFontWritingLine.cpp +++ b/core/libs/tests/GuestFontWritingLine.cpp @@ -11,6 +11,7 @@ int APS5_VABI sceFontWritingLineClear(void*); int APS5_VABI sceFontWritingLineWritesOrder(void*, std::uint64_t, const FontWritingMetrics*, void*); const FontWritingLineStep* APS5_VABI sceFontWritingLineRefersRenderStep(void*); int APS5_VABI sceFontWritingLineGetRenderMetrics(void*, FontWritingMetrics*); +int APS5_VABI sceFontWritingLineGetOrderingSpace(void*, float*, float*, float*, float*); } static void Check(bool value, int line) { @@ -42,6 +43,15 @@ static bool Same(const FontWritingMetrics& a, const FontWritingMetrics& b) { a.Extent.bottom == b.Extent.bottom && a.Extent.left == b.Extent.left && a.Extent.right == b.Extent.right; } +static bool NoSpace(void* line) { + float head = 1.0f; + float inner = 1.0f; + float tail = 1.0f; + float advance = 1.0f; + return sceFontWritingLineGetOrderingSpace(line, &head, &inner, &tail, &advance) == SCE_FONT_OK && head == 0.0f && inner == 0.0f && + tail == 0.0f && advance == 0.0f; +} + static bool StepIs(const FontWritingLineStep* step, float x, const FontWritingMetrics& run, void* orderer) { return step && step->x == x && step->y == 0.0f && step->advanceX == run.advanceX && step->advanceY == run.advanceY && step->spacingProgress == 0.0f && step->writingOrderer == orderer && step->Adjusting.x == 0.0f && @@ -62,6 +72,7 @@ int main() { Require(sceFontCreateWritingLine(&memory, 0x12, nullptr, <r) == SCE_FONT_OK && ltr != nullptr && ltr != line); FontWritingMetrics metrics{}; + Require(NoSpace(line)); Require(sceFontWritingLineRefersRenderStep(line) == nullptr); Require(Throws([&] { sceFontWritingLineGetRenderMetrics(line, &metrics); })); @@ -74,6 +85,10 @@ int main() { Require(sceFontWritingLineWritesOrder(line, 0, &runB, &ordererB) == SCE_FONT_OK); Require(sceFontWritingLineGetRenderMetrics(line, &metrics) == SCE_FONT_OK); Require(Same(metrics, {16.0f, 0.0f, {-12.0f, 3.0f, 1.0f, 15.0f}})); + Require(NoSpace(line)); + float space = 0.0f; + Require(Throws([&] { sceFontWritingLineGetOrderingSpace(line, nullptr, &space, &space, &space); })); + Require(Throws([&] { sceFontWritingLineGetOrderingSpace(line, &space, &space, &space, nullptr); })); Require(StepIs(sceFontWritingLineRefersRenderStep(line), 0.0f, runA, &ordererA)); Require(StepIs(sceFontWritingLineRefersRenderStep(line), 10.0f, runB, &ordererB)); @@ -109,6 +124,7 @@ int main() { Require(Throws([&] { sceFontWritingLineClear(nullptr); })); Require(Throws([&] { sceFontWritingLineClear(¬ALine); })); Require(Throws([&] { sceFontWritingLineRefersRenderStep(¬ALine); })); + Require(Throws([&] { sceFontWritingLineGetOrderingSpace(¬ALine, &space, &space, &space, &space); })); void* bogus = ¬ALine; Require(Throws([&] { sceFontDestroyWritingLine(&bogus); }) && bogus == ¬ALine); diff --git a/core/libs/tests/GuestGetrusage.cpp b/core/libs/tests/GuestGetrusage.cpp new file mode 100644 index 000000000..a549808cf --- /dev/null +++ b/core/libs/tests/GuestGetrusage.cpp @@ -0,0 +1,42 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +struct Timeval { + std::int64_t tv_sec; + std::int64_t tv_usec; +}; +struct ResourceUsage { + Timeval ru_utime; + Timeval ru_stime; + std::int64_t rest[14]; +}; +extern "C" int APS5_VABI getrusage_nid_postfix(int who, ResourceUsage* usage); +static void Require(bool value) { if (!value) std::abort(); } +static std::int64_t CpuMicros(int who) { + ResourceUsage usage{}; + Require(getrusage_nid_postfix(who, &usage) == 0); + return (usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * 1000000 + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; +} +static void SpinUntil(int who, std::int64_t micros) { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + volatile std::uint64_t sink = 0; + while (CpuMicros(who) < micros) { + for (int i = 0; i < 100000; ++i) sink = sink + i; + Require(std::chrono::steady_clock::now() < deadline); + } +} +int main() { + SpinUntil(0, 400000); + const std::int64_t mainThread = CpuMicros(1); + Require(mainThread >= 200000); + std::thread worker([] { + const std::int64_t fresh = CpuMicros(1); + Require(fresh < 100000); + SpinUntil(1, fresh + 100000); + Require(CpuMicros(0) >= CpuMicros(1) + 300000); + }); + worker.join(); + Require(CpuMicros(0) >= 500000); +} diff --git a/core/libs/tests/GuestHttp.cpp b/core/libs/tests/GuestHttp.cpp index 1a5d01074..498b72e3c 100644 --- a/core/libs/tests/GuestHttp.cpp +++ b/core/libs/tests/GuestHttp.cpp @@ -7,10 +7,12 @@ extern "C" { int APS5_VABI sceHttpUriParse(SceHttpUriElement*, const char*, void*, std::size_t*, std::size_t); +int APS5_VABI sceHttpUriMerge(char*, const char*, const char*, std::size_t*, std::size_t, std::uint32_t); int APS5_VABI sceHttpSetInflateGZIPEnabled(int, int); int APS5_VABI sceHttpUriBuild(char*, std::size_t*, std::size_t, const SceHttpUriElement*, std::uint32_t); int APS5_VABI sceHttpUriEscape(char*, std::size_t*, std::size_t, const char*); int APS5_VABI sceHttpUriUnescape(char*, std::size_t*, std::size_t, const char*); +int APS5_VABI sceHttpUriSweepPath(char*, const char*, std::size_t); int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); int APS5_VABI sceHttpReadData(int, void*, std::size_t); @@ -18,6 +20,9 @@ int APS5_VABI sceHttpCreateRequest2(int, const char*, const char*, std::uint64_t int APS5_VABI sceHttpsEnableOption(int, std::uint32_t); int APS5_VABI sceHttpsLoadCert(int, int, void*, void*, void*); int APS5_VABI sceHttpGetLastErrno(int, int*); +int APS5_VABI sceHttpSetResponseHeaderMaxSize(int, std::uint64_t); +int APS5_VABI sceHttpRedirectCacheFlush(int); +int APS5_VABI sceHttpsUnloadCert(int); int APS5_VABI sceHttpParseStatusLine(const char*, std::size_t, std::int32_t*, std::int32_t*, std::int32_t*, const char**, std::size_t*); } @@ -122,6 +127,57 @@ int main() { Require(sceHttpUriUnescape(decodedBytes, &required, sizeof(decodedBytes), encodedBytes) == 0); Require(required == sizeof(bytes) && std::memcmp(bytes, decodedBytes, sizeof(bytes)) == 0); + const char* base = "http://foo.com/foo/index.html"; + const std::size_t baseMergeSize = 5 + 1 + 1 + 8 + 16 + 1 + 1 + 2; + char merged[512]; + auto merge = [&](const char* mergeBase, const char* relative, const char* expected) { + std::memset(merged, 'Z', sizeof(merged)); + return sceHttpUriMerge(merged, mergeBase, relative, &required, sizeof(merged), 0) == 0 && Equal(merged, expected); + }; + Require(merge(base, "./default.html", "http://foo.com/foo/./default.html")); + Require(required == baseMergeSize + 2 * (29 + 14)); + Require(merge(base, "../sibling.html", "http://foo.com/foo/../sibling.html")); + Require(merge(base, "", "http://foo.com/foo/")); + Require(merge(base, "/root.html", "http://foo.com/root.html")); + Require(merge(base, "a?q=1#f", "http://foo.com/foo/a?q=1#f")); + Require(merge("https://u:p@foo.com:8443/a/b?x=1#top", "c", "https://u:p@foo.com:8443/a/c")); + Require(merge("http://foo.com", "x", "http://foo.com/x")); + Require(merge("http://foo.com:80/", "x", "http://foo.com/x")); + Require(merge("http://foo.com/a/b", "mailto:x", "http://foo.com/a/mailto:x")); + Require(merge(base, "a%20b.html", "http://foo.com/foo/a%20b.html")); + Require(merge(base, "~user/x", "http://foo.com/foo/~user/x")); + Require(merge(base, "file(1).png", "http://foo.com/foo/file(1).png")); + Require(merge(base, "a+b=c;d!e", "http://foo.com/foo/a+b=c;d!e")); + + Require(merge(base, "http://bar.com/other", "http://bar.com/other") && required == 21); + const std::size_t absoluteSize = baseMergeSize + 2 * (29 + 20); + for (std::size_t i = 21; i < absoluteSize; ++i) Require(merged[i] == '\0'); + Require(merged[absoluteSize] == 'Z'); + Require(merge(base, "//bar.com/x", "//bar.com/x") && required == 12); + + required = 0; + Require(sceHttpUriMerge(nullptr, base, "./default.html", &required, 0, 0) == 0); + Require(required == baseMergeSize + 2 * (29 + 14)); + Require(sceHttpUriMerge(nullptr, base, "http://bar.com/other", &required, 0, 0) == 0); + Require(required == absoluteSize); + Require(sceHttpUriMerge(nullptr, base, "x", nullptr, 0, 0) == 0); + std::memset(merged, 'Z', sizeof(merged)); + Require(sceHttpUriMerge(merged, base, "./default.html", &required, baseMergeSize + 2 * (29 + 14) - 1, 0) == outOfMemory); + Require(required == baseMergeSize + 2 * (29 + 14) && merged[0] == 'Z'); + Require(sceHttpUriMerge(merged, base, "http://bar.com/other", &required, absoluteSize - 1, 0) == outOfMemory); + Require(merged[0] == 'Z'); + Require(sceHttpUriMerge(merged, base, "./default.html", nullptr, baseMergeSize + 2 * (29 + 14), 0) == 0); + Require(Equal(merged, "http://foo.com/foo/./default.html")); + + required = 123; + Require(sceHttpUriMerge(merged, nullptr, "./x", &required, sizeof(merged), 0) == invalidValue); + Require(sceHttpUriMerge(merged, base, nullptr, &required, sizeof(merged), 0) == invalidValue); + Require(sceHttpUriMerge(merged, base, "./x", &required, sizeof(merged), 1) == invalidValue); + Require(sceHttpUriMerge(nullptr, nullptr, nullptr, &required, 0, 1) == invalidValue); + Require(sceHttpUriMerge(merged, "http://bad host/", "./x", &required, sizeof(merged), 0) == invalidUrl); + Require(sceHttpUriMerge(merged, base, "http://bad host/", &required, sizeof(merged), 0) == invalidUrl); + Require(required == 123); + HttpEpollHandle epoll = nullptr; Require(sceHttpCreateEpoll(1, nullptr) == invalidValue); Require(sceHttpCreateEpoll(1, &epoll) == 0 && epoll != nullptr); @@ -137,6 +193,9 @@ int main() { Require(sceHttpSetInflateGZIPEnabled(1, -1) == invalidValue); Require(sceHttpsEnableOption(1, 0) == 0); Require(sceHttpsLoadCert(1, 0, nullptr, nullptr, nullptr) == 0); + Require(sceHttpsUnloadCert(1) == 0); + Require(sceHttpSetResponseHeaderMaxSize(1, 8192) == 0); + Require(sceHttpRedirectCacheFlush(1) == 0); int httpErrno = -1; Require(sceHttpGetLastErrno(1, &httpErrno) == 0); Require(httpErrno == 0); @@ -188,4 +247,30 @@ int main() { Require(parse(response, 15) == parseInvalidResponse); Require(parse(response, 16) == parseInvalidResponse); Require(phrase == nullptr && phraseLength == 0); + auto sweep = [](const char* src, const char* expected) { + char swept[128]; + std::memset(swept, 'x', sizeof(swept)); + return sceHttpUriSweepPath(swept, src, std::strlen(src) + 1) == 0 && Equal(swept, expected); + }; + Require(sceHttpUriSweepPath(nullptr, nullptr, 0) == 0); + Require(sceHttpUriSweepPath(nullptr, "/foo", 5) == invalidValue); + char sweptPath[16]; + Require(sceHttpUriSweepPath(sweptPath, nullptr, 5) == invalidValue); + Require(sweep("foo/../bar", "foo/../bar")); + Require(sweep("/foo/../bar", "/bar")); + Require(sweep("/foo/./bar", "/foo/bar")); + Require(sweep("/foo/.", "/foo/.")); + Require(sweep("/foo/..", "/foo/..")); + Require(sweep("/foo/bar/../foo/././../../../test/index.html", "/test/index.html")); + Require(sweep("/", "/")); + Require(sweep("", "")); + Require(sweep("/a/b/c/../../d/", "/a/d/")); + Require(sweep("/../a", "/a")); + Require(sweep("/a/..b/.c", "/a/..b/.c")); + Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 6) == 0 && Equal(sweptPath, "/a/b/")); + Require(sceHttpUriSweepPath(sweptPath, "/ab/c", 3) == 0 && Equal(sweptPath, "/a")); + Require(sceHttpUriSweepPath(sweptPath, "/a/./b", 5) == 0 && Equal(sweptPath, "/a/.")); + Require(sceHttpUriSweepPath(sweptPath, "/a/b/../c", 8) == 0 && Equal(sweptPath, "/a/b/..")); + char sweptByte[2] = {'x', 'x'}; + Require(sceHttpUriSweepPath(sweptByte, "/", 1) == 0 && sweptByte[0] == '\0' && sweptByte[1] == 'x'); } diff --git a/core/libs/tests/GuestHttp2.cpp b/core/libs/tests/GuestHttp2.cpp new file mode 100644 index 000000000..7be4800b5 --- /dev/null +++ b/core/libs/tests/GuestHttp2.cpp @@ -0,0 +1,36 @@ +#include "SceTypes.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceHttp2Init(int, int, std::size_t, int); +int APS5_VABI sceHttp2CreateTemplate(int, const char*, int, int); +int APS5_VABI sceHttp2CreateRequestWithURL(int, const char*, const char*, std::uint64_t); +int APS5_VABI sceHttp2CreateCookieBox(int); +int APS5_VABI sceHttp2SetCookieBox(int, int); +int APS5_VABI sceHttp2CookieFlush(int); +int APS5_VABI sceHttp2SetRequestNoContentLength(int); +int APS5_VABI sceHttp2SendRequest(int, const void*, std::size_t); +int APS5_VABI sceHttp2Term(int); +} + +static void Require(bool value) { if (!value) std::abort(); } + +int main() { + const int context = sceHttp2Init(1, 1, 0x10000, 4); + Require(context > 0); + const int box = sceHttp2CreateCookieBox(context); + const int other = sceHttp2CreateCookieBox(context); + Require(box > 0 && other > 0 && box != other && box != context); + const int tmpl = sceHttp2CreateTemplate(context, "agent", 2, 0); + Require(tmpl > 0 && tmpl != box && tmpl != other); + Require(sceHttp2SetCookieBox(tmpl, box) == 0); + Require(sceHttp2SetCookieBox(tmpl, 0) == 0); + const int request = sceHttp2CreateRequestWithURL(tmpl, "POST", "https://example.com/", 16); + Require(request > 0); + Require(sceHttp2SetCookieBox(request, other) == 0); + Require(sceHttp2SetRequestNoContentLength(request) == 0); + Require(sceHttp2CookieFlush(context) == 0); + Require(sceHttp2SendRequest(request, nullptr, 0) == static_cast(0x80436063)); + Require(sceHttp2Term(context) == 0); +} diff --git a/core/libs/tests/GuestImeParameters.cpp b/core/libs/tests/GuestImeParameters.cpp index 3612fb077..f7711197d 100644 --- a/core/libs/tests/GuestImeParameters.cpp +++ b/core/libs/tests/GuestImeParameters.cpp @@ -11,6 +11,9 @@ int APS5_VABI sceImeClose_nid_postfix(void); int APS5_VABI sceImeSetCaret(const Caret* caret); int APS5_VABI sceImeSetText(const char16_t* text, uint32_t length); int APS5_VABI sceImeSetTextGeometry(TextAreaMode mode, const TextGeometry* geometry); +int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param); +int APS5_VABI sceImeKeyboardClose(int32_t user_id); +int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArray* resource_ids); } static_assert(sizeof(Param) == 96); @@ -118,9 +121,36 @@ static void CheckClosedPanel() { Require(sceImeClose_nid_postfix() == notOpened, "closing needs an open panel"); } +static void CheckKeyboardResourceIds() { + constexpr int NotOpened = static_cast(0x80bc0002u); + constexpr int ConnectionFailed = static_cast(0x80bc0004u); + constexpr int InvalidUserId = static_cast(0x80bc0010u); + constexpr int InvalidAddress = static_cast(0x80bc0031u); + KeyboardResourceIdArray ids; + std::memset(&ids, 0xa5, sizeof(ids)); + Require(sceImeKeyboardGetResourceId(1, nullptr) == InvalidAddress, "null resource id array"); + Require(sceImeKeyboardGetResourceId(-1, &ids) == InvalidUserId, "invalid user"); + const auto* bytes = reinterpret_cast(&ids); + for (size_t i = 0; i < sizeof(ids); ++i) Require(bytes[i] == 0xa5, "outputs changed on argument error"); + Require(sceImeKeyboardGetResourceId(1, &ids) == NotOpened, "keyboard not opened"); + Require(ids.user_id == 1, "user not reported for an unopened keyboard"); + for (uint32_t id : ids.resource_id) Require(id == 0, "resource id reported for an unopened keyboard"); + KeyboardParam param{}; + Require(sceImeKeyboardOpen(1, ¶m) == 0, "keyboard open failed"); + std::memset(&ids, 0xa5, sizeof(ids)); + Require(sceImeKeyboardGetResourceId(1, &ids) == ConnectionFailed, "a keyboard was reported as connected"); + Require(ids.user_id == 1, "user not reported"); + for (uint32_t id : ids.resource_id) Require(id == 0, "resource id reported without a keyboard"); + Require(sceImeKeyboardGetResourceId(2, &ids) == NotOpened, "keyboard of another user reported as opened"); + Require(ids.user_id == 2, "other user not reported"); + Require(sceImeKeyboardClose(1) == 0, "keyboard close failed"); + Require(sceImeKeyboardGetResourceId(1, &ids) == NotOpened, "closed keyboard reported as opened"); +} + int main() { CheckInitialization(); CheckPanelSizes(); CheckErrors(); CheckClosedPanel(); + CheckKeyboardResourceIds(); } diff --git a/core/libs/tests/GuestJpegEnc.cpp b/core/libs/tests/GuestJpegEnc.cpp index 7f5983747..52437df7a 100644 --- a/core/libs/tests/GuestJpegEnc.cpp +++ b/core/libs/tests/GuestJpegEnc.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -23,17 +24,58 @@ static bool IsJpeg(const unsigned char* data, std::uint32_t size) { return size > 4 && data[0] == 0xFF && data[1] == 0xD8 && data[size - 2] == 0xFF && data[size - 1] == 0xD9; } +static std::pair ReadFrame(const unsigned char* jpeg, std::uint32_t size) { + std::size_t offset = 2; + while (offset + 3 < size) { + Require(jpeg[offset++] == 0xFF); + while (offset < size && jpeg[offset] == 0xFF) ++offset; + Require(offset < size); + const std::uint8_t marker = jpeg[offset++]; + if (marker == 0xDA || marker == 0xD9) break; + Require(offset + 1 < size); + const std::size_t length = (static_cast(jpeg[offset]) << 8) | jpeg[offset + 1]; + Require(length >= 2 && offset + length <= size); + if (marker >= 0xC0 && marker <= 0xC3) return {jpeg[offset + 7], jpeg[offset + 9]}; + offset += length; + } + std::abort(); +} + +static std::pair ReadRestart(const unsigned char* jpeg, std::uint32_t size) { + int interval = 0; + std::size_t offset = 2; + while (true) { + Require(offset + 3 < size && jpeg[offset] == 0xFF); + const std::uint8_t marker = jpeg[offset + 1]; + const std::size_t length = (static_cast(jpeg[offset + 2]) << 8) | jpeg[offset + 3]; + Require(length >= 2 && offset + 2 + length <= size); + if (marker == 0xDD) { + Require(length == 4); + interval = (jpeg[offset + 4] << 8) | jpeg[offset + 5]; + } + offset += 2 + length; + if (marker == 0xDA) break; + } + int markers = 0; + for (; offset + 1 < size; ++offset) { + if (jpeg[offset] != 0xFF || jpeg[offset + 1] < 0xD0 || jpeg[offset + 1] > 0xD7) continue; + Require(jpeg[offset + 1] == 0xD0 + markers % 8); + ++markers; + } + return {interval, markers}; +} + static int AverageError(const std::vector& expected, const std::vector& actual) { long total = 0; for (std::size_t i = 0; i < expected.size(); ++i) total += expected[i] > actual[i] ? expected[i] - actual[i] : actual[i] - expected[i]; return static_cast(total / static_cast(expected.size())); } -static std::vector DecodeOutput(const JpegEncOutputInfo& info) { +static std::vector DecodeOutput(const JpegEncOutputInfo& info, std::uint32_t channels = 3) { Require(IsJpeg(jpeg, info.size)); const auto decoded = Decoder::Jpeg::Decode({jpeg, info.size}); Require(decoded.has_value()); - Require(decoded->width == 16 && decoded->height == 16 && decoded->channels == 3); + Require(decoded->width == 16 && decoded->height == 16 && decoded->channels == channels); return decoded->pixels; } @@ -169,6 +211,7 @@ int main() { info = {}; Require(sceJpegEncEncode(handle, &rgba, &info) == 0); Require(info.height == 16); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}}); Require(AverageError(expected, DecodeOutput(info)) < 6); for (std::uint32_t y = 0; y < 16; ++y) { @@ -183,6 +226,7 @@ int main() { bgra.pixel_format = 1; info = {}; Require(sceJpegEncEncode(handle, &bgra, &info) == 0); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x22}}); Require(AverageError(expected, DecodeOutput(info)) < 6); alignas(4) static unsigned char yuyv[16 * 16 * 2]; @@ -202,10 +246,8 @@ int main() { Require(AverageError(std::vector(16 * 16 * 3, 100), DecodeOutput(info)) < 3); static unsigned char gray[16 * 16]; - std::vector expectedGray(16 * 16 * 3); for (std::size_t i = 0; i < sizeof(gray); ++i) { gray[i] = static_cast(((i % 16) + (i / 16)) * 8); - for (int c = 0; c < 3; ++c) expectedGray[i * 3 + c] = gray[i]; } JpegEncEncodeParam y8 = ValidEncodeParam(); y8.image = gray; @@ -216,7 +258,15 @@ int main() { y8.sampling_type = 0; info = {}; Require(sceJpegEncEncode(handle, &y8, &info) == 0); - Require(AverageError(expectedGray, DecodeOutput(info)) < 6); + Require(ReadFrame(jpeg, info.size) == std::pair{1, std::uint8_t{0x11}}); + const auto decodedGray = DecodeOutput(info, 1); + Require(AverageError(std::vector(gray, gray + sizeof(gray)), decodedGray) < 6); + + rgba.sampling_type = 1; + rgba.compression_ratio = 0; + info = {}; + Require(sceJpegEncEncode(handle, &rgba, &info) == 0); + Require(ReadFrame(jpeg, info.size) == std::pair{3, std::uint8_t{0x21}}); std::memset(jpeg, 0, sizeof(jpeg)); Require(sceJpegEncEncode(handle, &rgba, nullptr) == 0); @@ -228,7 +278,59 @@ int main() { Require(encodeWith([](JpegEncEncodeParam& p) { p.image_width = 0; }) == invalidParam); Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.encode_mode = 1; }); })); - Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = 1; }); })); + Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = -2; }); })); + Require(ThrowsRuntimeError([&] { encodeWith([](JpegEncEncodeParam& p) { p.restart_interval = INT32_MIN; }); })); + + constexpr std::uint32_t wideWidth = 48; + constexpr std::uint32_t wideHeight = 32; + alignas(4) static unsigned char wide[wideWidth * wideHeight * 4]; + for (std::size_t i = 0; i < sizeof(wide); ++i) wide[i] = static_cast((i * 37) ^ (i / 192)); + static unsigned char wideGray[wideWidth * wideHeight]; + for (std::size_t i = 0; i < sizeof(wideGray); ++i) wideGray[i] = static_cast((i * 53) ^ (i / 48)); + static unsigned char restarted[8192]; + auto encodeWide = [&](std::uint8_t sampling, std::int32_t restart, bool grayscale) { + JpegEncEncodeParam p = ValidEncodeParam(); + p.image = grayscale ? wideGray : wide; + p.image_size = grayscale ? sizeof(wideGray) : sizeof(wide); + p.image_width = wideWidth; + p.image_height = wideHeight; + p.image_pitch = grayscale ? wideWidth : wideWidth * 4; + p.pixel_format = grayscale ? 11 : 0; + p.color_space = grayscale ? 2 : 1; + p.sampling_type = sampling; + p.jpeg = restarted; + p.jpeg_size = sizeof(restarted); + p.restart_interval = restart; + JpegEncOutputInfo out{}; + Require(sceJpegEncEncode(handle, &p, &out) == 0); + Require(IsJpeg(restarted, out.size) && out.height == wideHeight); + const auto decoded = Decoder::Jpeg::Decode({restarted, out.size}); + Require(decoded.has_value() && decoded->width == wideWidth && decoded->height == wideHeight); + return std::pair{ReadRestart(restarted, out.size), decoded->pixels}; + }; + + const auto plain420 = encodeWide(2, 0, false); + Require(plain420.first == std::pair{0, 0}); + const auto blocks420 = encodeWide(2, 2, false); + Require(blocks420.first == std::pair{2, 2}); + Require(blocks420.second == plain420.second); + const auto rows420 = encodeWide(2, -1, false); + Require(rows420.first == std::pair{3, 1}); + Require(rows420.second == plain420.second); + const auto plain422 = encodeWide(1, 0, false); + const auto rows422 = encodeWide(1, -1, false); + Require(rows422.first == std::pair{3, 3}); + Require(rows422.second == plain422.second); + const auto blocks422 = encodeWide(1, 5, false); + Require(blocks422.first == std::pair{5, 2}); + Require(blocks422.second == plain422.second); + const auto plainGray = encodeWide(0, 0, true); + const auto rowsGray = encodeWide(0, -1, true); + Require(rowsGray.first == std::pair{6, 3}); + Require(rowsGray.second == plainGray.second); + const auto blocksGray = encodeWide(0, 0xFFFF, true); + Require(blocksGray.first == std::pair{0xFFFF, 0}); + Require(blocksGray.second == plainGray.second); Require(sceJpegEncDelete(handle) == 0); diff --git a/core/libs/tests/GuestJson.cpp b/core/libs/tests/GuestJson.cpp index e30b0b123..7791c8404 100644 --- a/core/libs/tests/GuestJson.cpp +++ b/core/libs/tests/GuestJson.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -71,6 +72,16 @@ Value* APS5_VABI _ZN3sce4Json5Value10referValueERKNS0_6StringE(Value*, const Str const Value* APS5_VABI _ZNK3sce4Json5ValueixEm(const Value*, std::size_t); int APS5_VABI _ZN3sce4Json5Value9serializeERNS0_6StringE(Value*, String*); int APS5_VABI _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(Value*, const char*, std::size_t); +std::size_t APS5_VABI _ZNK3sce4Json6Object4sizeEv(const Object*); +const Object* APS5_VABI _ZNK3sce4Json5Value9getObjectEv(const Value*); +void APS5_VABI _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(Value*, std::int32_t); +const Value* APS5_VABI _ZNK3sce4Json5Value8getValueEm(const Value*, std::size_t); +const Value* APS5_VABI _ZNK3sce4Json5Value8getValueERKNS0_6StringE(const Value*, const String*); +Value* APS5_VABI _ZN3sce4Json5Value10referValueEm(Value*, std::size_t); +void APS5_VABI _ZN3sce4Json14InitParameter2C1Ev(void*); +void APS5_VABI _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(void*, void*, void*); +void APS5_VABI _ZN3sce4Json14InitParameter217setFileBufferSizeEm(void*, std::size_t); +int APS5_VABI _ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(void*, const void*); } static void Check(bool value, int line) { @@ -167,6 +178,16 @@ static void ParseAndRoundTrip() { Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, invalid, std::strlen(invalid)) < 0); Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); } + for (const char* overflow : {"1e309", "[-1e309]", "{\"a\":[1,1e400]}"}) { + bool unverified = false; + try { + _ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, overflow, std::strlen(overflow)); + } catch (const std::runtime_error& error) { + unverified = std::string(error.what()).find("unverified") != std::string::npos; + } + Require(unverified); + Require(_ZNK3sce4Json5Value7getTypeEv(&root) == TypeObject && _ZNK3sce4Json5Value5countEv(&root) == 8); + } _ZN3sce4Json5ValueD1Ev(&root); } @@ -275,9 +296,65 @@ static void NullAccess() { Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); } +static void ValueAccess() { + for (std::int32_t type = TypeNull; type <= TypeObject; ++type) { + Value typed{}; + _ZN3sce4Json5ValueC1ENS0_9ValueTypeE(&typed, type); + Require(_ZNK3sce4Json5Value7getTypeEv(&typed) == type); + Require(_ZNK3sce4Json5Value5countEv(&typed) == 0); + _ZN3sce4Json5ValueD1Ev(&typed); + } + + Value root{}; + _ZN3sce4Json5ValueC1Ev(&root); + const char text[] = "{\"list\":[10,20,30],\"name\":\"value\",\"flag\":true}"; + Require(_ZN3sce4Json6Parser5parseERNS0_5ValueEPKcm(&root, text, sizeof(text) - 1) == 0); + Require(_ZNK3sce4Json6Object4sizeEv(_ZNK3sce4Json5Value9getObjectEv(&root)) == 3); + + String key{}; + _ZN3sce4Json6StringC1EPKc(&key, "list"); + const Value* list = _ZNK3sce4Json5Value8getValueERKNS0_6StringE(&root, &key); + Require(_ZNK3sce4Json5Value7getTypeEv(list) == TypeArray); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Value8getValueEm(list, 1)) == 20); + Require(_ZNK3sce4Json5Value8getValueEm(list, 2) == _ZNK3sce4Json5ValueixEm(list, 2)); + Require(_ZNK3sce4Json5Value7getTypeEv(_ZNK3sce4Json5Value8getValueEm(list, 3)) == TypeNull); + + Value* mutableList = _ZN3sce4Json5Value10referValueERKNS0_6StringE(&root, &key); + Value* element = _ZN3sce4Json5Value10referValueEm(mutableList, 0); + Require(element != nullptr); + Require(_ZN3sce4Json5Value3setEl(element, 15) == 0); + Require(*_ZNK3sce4Json5Value10getIntegerEv(_ZNK3sce4Json5Value8getValueEm(list, 0)) == 15); + Require(_ZN3sce4Json5Value10referValueEm(mutableList, 3) == nullptr); + Require(_ZN3sce4Json5Value10referValueEm(&root, 0) == nullptr); + _ZN3sce4Json6StringD1Ev(&key); + + String missing{}; + _ZN3sce4Json6StringC1EPKc(&missing, "missing"); + Require(_ZNK3sce4Json5Value7getTypeEv(_ZNK3sce4Json5Value8getValueERKNS0_6StringE(&root, &missing)) == TypeNull); + _ZN3sce4Json6StringD1Ev(&missing); + _ZN3sce4Json5ValueD1Ev(&root); + + alignas(16) std::uint8_t parameter[40]; + std::memset(parameter, 0xff, sizeof(parameter)); + _ZN3sce4Json14InitParameter2C1Ev(parameter); + int allocator = 0; + int userData = 0; + _ZN3sce4Json14InitParameter212setAllocatorEPNS0_12MemAllocatorEPv(parameter, &allocator, &userData); + _ZN3sce4Json14InitParameter217setFileBufferSizeEm(parameter, 4096); + void* stored[3]{}; + std::memcpy(stored, parameter, sizeof(stored)); + Require(stored[0] == &allocator && stored[1] == &userData && reinterpret_cast(stored[2]) == 4096); + alignas(16) std::uint8_t initializer[16]{}; + Require(_ZN3sce4Json11InitializerC1Ev(initializer) == 0); + Require(_ZN3sce4Json11Initializer10initializeEPKNS0_14InitParameter2E(initializer, parameter) == 0); + Require(_ZN3sce4Json11Initializer9terminateEv(initializer) == 0); + Require(_ZN3sce4Json11InitializerD1Ev(initializer) == 0); +} + int main() { ParseAndRoundTrip(); NestingDepth(); ObjectsAndArrays(); NullAccess(); + ValueAccess(); } diff --git a/core/libs/tests/GuestKernelErrors.cpp b/core/libs/tests/GuestKernelErrors.cpp index d68975fdd..9a16733f3 100644 --- a/core/libs/tests/GuestKernelErrors.cpp +++ b/core/libs/tests/GuestKernelErrors.cpp @@ -28,6 +28,7 @@ int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* a int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexTrylock(PthreadMutex* mutex); } static constexpr int SCE_OK = 0; @@ -36,6 +37,7 @@ static constexpr int SCE_KERNEL_ERROR_EBADF = static_cast(0x80020009); static constexpr int SCE_KERNEL_ERROR_EDEADLK = static_cast(0x8002000B); static constexpr int SCE_KERNEL_ERROR_EFAULT = static_cast(0x8002000E); static constexpr int SCE_KERNEL_ERROR_EINVAL = static_cast(0x80020016); +static constexpr int SCE_KERNEL_ERROR_EBUSY = static_cast(0x80020010); static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = static_cast(0x8002003C); static constexpr int MUTEX_TYPE_ERRORCHECK = 1; static constexpr int PRIO_NONE = 0; @@ -81,6 +83,8 @@ int main() { PthreadMutexattr attr = nullptr; Require(scePthreadMutexattrInit(&attr) == SCE_OK); Require(scePthreadMutexattrSettype(&attr, MUTEX_TYPE_ERRORCHECK) == SCE_OK); + Require(scePthreadMutexattrSettype(&attr, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(scePthreadMutexattrSettype(&attr, 5) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_NONE) == SCE_OK); Require(scePthreadMutexattrSetprotocol(&attr, PRIO_INHERIT) == SCE_OK); bool protectionRejected = false; @@ -108,6 +112,21 @@ int main() { Require(scePthreadMutexUnlock(&adaptive) == SCE_OK); Require(scePthreadMutexDestroy(&adaptive) == SCE_OK); + PthreadMutex adaptiveStatic = reinterpret_cast(std::uintptr_t{1}); + Require(scePthreadMutexLock(&adaptiveStatic) == SCE_OK); + Require(adaptiveStatic != reinterpret_cast(std::uintptr_t{1})); + Require(scePthreadMutexLock(&adaptiveStatic) == SCE_KERNEL_ERROR_EDEADLK); + Require(scePthreadMutexTrylock(&adaptiveStatic) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadMutexUnlock(&adaptiveStatic) == SCE_OK); + Require(scePthreadMutexDestroy(&adaptiveStatic) == SCE_OK); + adaptiveStatic = reinterpret_cast(std::uintptr_t{1}); + Require(scePthreadMutexTrylock(&adaptiveStatic) == SCE_OK); + Require(scePthreadMutexTrylock(&adaptiveStatic) == SCE_KERNEL_ERROR_EBUSY); + Require(scePthreadMutexUnlock(&adaptiveStatic) == SCE_OK); + Require(scePthreadMutexDestroy(&adaptiveStatic) == SCE_OK); + adaptiveStatic = reinterpret_cast(std::uintptr_t{1}); + Require(scePthreadMutexDestroy(&adaptiveStatic) == SCE_OK); + Require(scePthreadMutexattrInit(&attr) == SCE_OK); PthreadMutex defaulted = nullptr; Require(scePthreadMutexInit(&defaulted, &attr, nullptr) == SCE_OK); diff --git a/core/libs/tests/GuestKernelIsStack.cpp b/core/libs/tests/GuestKernelIsStack.cpp new file mode 100644 index 000000000..b2c1097d2 --- /dev/null +++ b/core/libs/tests/GuestKernelIsStack.cpp @@ -0,0 +1,74 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI scePthreadCreate(Pthread* thread, const PthreadAttr* attr, PthreadEntry entry, void* arg, const char* name); +int APS5_VABI scePthreadJoin(Pthread thread, void** retval); +int APS5_VABI sceKernelIsStack(void* addr, void** start, void** end); +int APS5_VABI sceKernelMapFlexibleMemory(void** addr, std::size_t len, int prot, int flags); +int APS5_VABI sceKernelMunmap(std::uint64_t addr, std::size_t len); +} + +static constexpr int SCE_OK = 0; +static constexpr int SCE_KERNEL_ERROR_EACCES = static_cast(0x8002000d); +static int global = 0; + +static void Require(bool value) { if (!value) std::abort(); } + +struct WorkerContext { + std::atomic queried{false}; + std::atomic release{false}; + void* local = nullptr; + void* start = nullptr; + void* end = nullptr; + int result = -1; +}; + +static void* APS5_VABI Worker(void* arg) { + auto& context = *static_cast(arg); + int local = 0; + context.local = &local; + context.result = sceKernelIsStack(&local, &context.start, &context.end); + context.queried.store(true); + while (!context.release.load()) {} + return nullptr; +} + +int main() { + int local = 0; + void* start = nullptr; + void* end = nullptr; + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + Require(start <= static_cast(&local) && static_cast(&local) < end); + Require(sceKernelIsStack(&local, nullptr, nullptr) == SCE_OK); + + start = &local; + end = &local; + Require(sceKernelIsStack(&global, &start, &end) == SCE_OK); + Require(start == nullptr && end == nullptr); + + WorkerContext context; + Pthread thread = nullptr; + Require(scePthreadCreate(&thread, nullptr, Worker, &context, nullptr) == SCE_OK); + while (!context.queried.load()) {} + Require(context.result == SCE_OK); + Require(context.start <= context.local && context.local < context.end); + Require(sceKernelIsStack(&local, &start, &end) == SCE_OK); + Require(context.end <= start || context.start >= end); + void* workerStart = nullptr; + void* workerEnd = nullptr; + Require(sceKernelIsStack(context.local, &workerStart, &workerEnd) == SCE_OK); + Require(workerStart == context.start && workerEnd == context.end); + context.release.store(true); + Require(scePthreadJoin(thread, nullptr) == SCE_OK); + + constexpr std::size_t page = 0x4000; + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, page, 3, 0) == SCE_OK); + Require(sceKernelIsStack(mapped, &start, &end) == SCE_OK); + Require(start == nullptr && end == nullptr); + Require(sceKernelMunmap(reinterpret_cast(mapped), page) == SCE_OK); + Require(sceKernelIsStack(mapped, &start, &end) == SCE_KERNEL_ERROR_EACCES); +} diff --git a/core/libs/tests/GuestLocale.cpp b/core/libs/tests/GuestLocale.cpp index dc1635dd9..632670181 100644 --- a/core/libs/tests/GuestLocale.cpp +++ b/core/libs/tests/GuestLocale.cpp @@ -57,7 +57,7 @@ static void CheckGuestCalls() { Require(locale->base.references == initial + 1); Require(release(&locale->base) == nullptr); Require(locale->base.references == initial); - RequireException([&] { release(&locale->base); }); + Require(release(&locale->base) == nullptr && locale->base.references == initial); RequireException([&] { retain(nullptr); }); RequireException([&] { locale->base.vtable->deleteObject(&locale->base); }); Require(locale->base.references == initial); diff --git a/core/libs/tests/GuestLoginDialog.cpp b/core/libs/tests/GuestLoginDialog.cpp new file mode 100644 index 000000000..7eaeb5619 --- /dev/null +++ b/core/libs/tests/GuestLoginDialog.cpp @@ -0,0 +1,54 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceLoginDialogInitialize(void); +int APS5_VABI sceLoginDialogOpen(const void* param); +int APS5_VABI sceLoginDialogUpdateStatus(void); +int APS5_VABI sceLoginDialogGetStatus(void); +int APS5_VABI sceLoginDialogGetResult(void* result); +int APS5_VABI sceLoginDialogClose(void); +int APS5_VABI sceLoginDialogTerminate(void); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +constexpr int kStatusFinished = 3; +constexpr int kErrNotInitialized = static_cast(0x80B80003); +constexpr int kErrAlreadyInitialized = static_cast(0x80B80004); +constexpr int kErrNotFinished = static_cast(0x80B80005); +constexpr int kErrArgNull = static_cast(0x80B8000D); +constexpr int kResultUserCanceled = 1; + +} + +int main() { + std::int64_t param = 0; + std::int32_t result[2] = {-1, -1}; + + Require(sceLoginDialogTerminate() == kErrNotInitialized); + Require(sceLoginDialogOpen(¶m) == kErrNotInitialized); + Require(sceLoginDialogGetResult(result) == kErrNotInitialized); + + Require(sceLoginDialogInitialize() == 0); + Require(sceLoginDialogInitialize() == kErrAlreadyInitialized); + + Require(sceLoginDialogOpen(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(result) == kErrNotFinished); + + Require(sceLoginDialogOpen(¶m) == 0); + Require(sceLoginDialogGetStatus() == kStatusFinished); + Require(sceLoginDialogUpdateStatus() == kStatusFinished); + + Require(sceLoginDialogGetResult(nullptr) == kErrArgNull); + Require(sceLoginDialogGetResult(result) == 0); + Require(result[0] == kResultUserCanceled); + Require(result[1] == -1); + + Require(sceLoginDialogClose() == 0); + Require(sceLoginDialogTerminate() == 0); + Require(sceLoginDialogTerminate() == kErrNotInitialized); +} diff --git a/core/libs/tests/GuestMemory.cpp b/core/libs/tests/GuestMemory.cpp index 58358ae15..815e716d0 100644 --- a/core/libs/tests/GuestMemory.cpp +++ b/core/libs/tests/GuestMemory.cpp @@ -41,13 +41,17 @@ int APS5_VABI sceKernelMapNamedFlexibleMemoryInternal(void**, std::size_t, int, int APS5_VABI sceKernelAvailableFlexibleMemorySize(std::size_t*); int APS5_VABI sceKernelMapFlexibleMemory(void**, std::size_t, int, int); int APS5_VABI sceKernelMunmap(void*, std::size_t); +int APS5_VABI sceKernelReleaseFlexibleMemory(void*, std::size_t); int APS5_VABI sceKernelMprotect(const void*, std::size_t, int); +int APS5_VABI sceKernelMtypeprotect(const void*, std::size_t, int, int); +int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); int APS5_VABI sceKernelVirtualQuery(const void*, int, VirtualQueryInfo*, std::uint64_t); int APS5_VABI sceKernelSetVirtualRangeName(const void*, std::uint64_t, const char*); int APS5_VABI sceKernelClearVirtualRangeName(const void*, std::uint64_t); int APS5_VABI sceKernelAllocateDirectMemory(std::int64_t, std::int64_t, std::size_t, std::size_t, int, std::int64_t*); int APS5_VABI sceKernelMapDirectMemory(void**, std::size_t, int, int, std::int64_t, std::size_t); int APS5_VABI sceKernelReleaseDirectMemory(std::int64_t, std::size_t); +int APS5_VABI sceKernelCheckedReleaseDirectMemory(std::int64_t, std::size_t); int APS5_VABI sceKernelReserveVirtualRange(void**, std::size_t, int, std::size_t); int APS5_VABI sceKernelMemoryPoolReserve(void*, std::size_t, std::size_t, int, void**); int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); @@ -59,6 +63,8 @@ int APS5_VABI sceKernelAioSubmitReadCommands(KernelAioRwRequest*, std::int32_t, int APS5_VABI sceKernelAioWaitRequest(std::int32_t, std::int32_t*, std::uint32_t*); int APS5_VABI sceKernelAioDeleteRequest(std::int32_t, std::int32_t*); int APS5_VABI sceKernelMlock_nid_postfix(void*, std::uint64_t); +int APS5_VABI sceKernelGetDirectMemoryType(std::int64_t, int*, std::int64_t*, std::int64_t*); +int APS5_VABI sceKernelBatchMap2(KernelBatchMapEntry*, int, int*, int); } static void Require(bool condition, std::source_location location = std::source_location::current()) { @@ -74,6 +80,23 @@ static const char* NameAt(const void* address) { return info.name; } +static void CheckReleaseFlexibleMemory() { + constexpr std::size_t length = 0x10000; + std::size_t before = 0; + std::size_t available = 0; + Require(sceKernelAvailableFlexibleMemorySize(&before) == 0); + void* mapped = nullptr; + Require(sceKernelMapFlexibleMemory(&mapped, length, 3, 0) == 0 && mapped != nullptr); + static_cast(mapped)[length - 1] = 1; + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before - length); + Require(sceKernelReleaseFlexibleMemory(mapped, length) == 0); + Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); + void* again = mapped; + Require(sceKernelMapFlexibleMemory(&again, length, 3, 0x90) == 0 && again == mapped); + Require(static_cast(again)[length - 1] == 0); + Require(sceKernelMunmap(again, length) == 0); +} + static void CheckNamedAndHintedMappings() { constexpr std::size_t length = 0x10000; void* first = nullptr; @@ -90,10 +113,9 @@ static void CheckNamedAndHintedMappings() { Require(sceKernelMapFlexibleMemory(&hinted, length, 3, 0) == 0); Require(hinted > first && (reinterpret_cast(hinted) & 0x3fff) == 0); static_cast(hinted)[length - 1] = 1; - bool rejected = false; void* overwrite = first; - try { sceKernelMapFlexibleMemory(&overwrite, 0x4000, 3, 0x90); } catch (const std::exception&) { rejected = true; } - Require(rejected && overwrite == first); + Require(sceKernelMapFlexibleMemory(&overwrite, 0x4000, 3, 0x90) == static_cast(0x8002000cu)); + Require(overwrite == first); Require(sceKernelMunmap(hinted, length) == 0); void* exclusive = hinted; Require(sceKernelMapFlexibleMemory(&exclusive, length, 3, 0x90) == 0); @@ -103,6 +125,23 @@ static void CheckNamedAndHintedMappings() { Require(sceKernelMunmap(first, length) == 0); } +static void CheckAudioCoprocessorProtection() { + constexpr std::size_t length = 0x4000; + void* writable = nullptr; + Require(sceKernelMapFlexibleMemory(&writable, length, 0x200, 0) == 0); + static_cast(writable)[length - 1] = 7; + Require(static_cast(writable)[length - 1] == 7); + Require(sceKernelMprotect(writable, length, 0x100) == 0); + Require(static_cast(writable)[length - 1] == 7); + Require(sceKernelMprotect(writable, length, 0x3f2) == 0); + static_cast(writable)[0] = 9; + Require(sceKernelMunmap(writable, length) == 0); + bool rejected = false; + void* undefined = nullptr; + try { sceKernelMapFlexibleMemory(&undefined, length, 0x400, 0); } catch (const std::invalid_argument&) { rejected = true; } + Require(rejected && undefined == nullptr); +} + static void CheckInternalNamedFlexibleMapping() { constexpr std::size_t length = 0x10000; std::size_t before = 0; @@ -121,6 +160,55 @@ static void CheckInternalNamedFlexibleMapping() { Require(sceKernelAvailableFlexibleMemorySize(&available) == 0 && available == before); } +static void CheckBatchMapStopsAtInvalidEntry() { + constexpr std::size_t page = 0x4000; + constexpr int mapFlexible = 3; + constexpr int unmap = 1; + constexpr int protect = 2; + const auto flexible = [&] { return KernelBatchMapEntry{nullptr, 0, page, 3, 0, 0, mapFlexible}; }; + for (const std::int32_t operation : {5, 6, -1, std::numeric_limits::max()}) { + KernelBatchMapEntry entries[3] = {flexible(), flexible(), flexible()}; + entries[1].operation = operation; + int processed = -1; + Require(sceKernelBatchMap2(entries, 3, &processed, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(processed == 1); + Require(entries[0].start != nullptr && entries[1].start == nullptr && entries[2].start == nullptr); + Require(sceKernelMunmap(entries[0].start, page) == 0); + } + KernelBatchMapEntry entries[3] = {flexible(), flexible(), flexible()}; + entries[1].operation = protect; + entries[1].length = 0; + int processed = -1; + Require(sceKernelBatchMap2(entries, 3, &processed, 0) == SCE_KERNEL_ERROR_EINVAL); + Require(processed == 1 && entries[0].start != nullptr); + Require(entries[1].start == nullptr && entries[2].start == nullptr); + KernelBatchMapEntry unmaps[2] = {entries[0], entries[0]}; + unmaps[0].operation = unmap; + unmaps[1].operation = unmap; + unmaps[1].length = 0; + processed = -1; + Require(sceKernelBatchMap2(unmaps, 2, &processed, 0) == SCE_KERNEL_ERROR_EINVAL && processed == 1); + Require(sceKernelBatchMap2(entries, 1, &processed, 0) == 0 && processed == 1); + Require(sceKernelMunmap(entries[0].start, page) == 0); +} + +static void CheckCheckedReleaseDirectMemory() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys + 1, page) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCheckedReleaseDirectMemory(phys, page + 1) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelCheckedReleaseDirectMemory(phys, 0) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys, page * 3) == SCE_KERNEL_ERROR_ENOENT); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 2, 3, 0, phys, 0) == 0); + Require(sceKernelMunmap(mapped, page * 2) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys + page, page) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys, page * 2) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelCheckedReleaseDirectMemory(phys, page) == 0); + Require(sceKernelCheckedReleaseDirectMemory(phys, page) == SCE_KERNEL_ERROR_ENOENT); +} + static void CheckDirectMemoryFollowsPhysicalPages() { constexpr std::size_t page = 0x4000; std::int64_t phys = 0; @@ -191,6 +279,96 @@ static void CheckReleaseDirectMemoryClearsMappings() { Require(sceKernelMunmap(mapped, page * 2) == 0); } +static void CheckGetDirectMemoryType() { + constexpr std::size_t page = 0x4000; + std::int64_t first = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 3, &first) == 0); + std::int64_t second = 0; + Require(sceKernelAllocateDirectMemory(first + page * 2, first + page * 3, page, 0, 1, &second) == 0 && second == first + page * 2); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(first, &type, &start, &end) == 0); + Require(type == 3 && start == first && end == first + page * 2); + type = -1; + Require(sceKernelGetDirectMemoryType(first + page * 2 - 1, &type, &start, &end) == 0); + Require(type == 3 && start == first && end == first + page * 2); + Require(sceKernelGetDirectMemoryType(first + page * 2, &type, &start, &end) == 0); + Require(type == 1 && start == second && end == second + page); + Require(sceKernelGetDirectMemoryType(first, nullptr, &start, &end) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelGetDirectMemoryType(first, &type, nullptr, &end) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelGetDirectMemoryType(first, &type, &start, nullptr) == SCE_KERNEL_ERROR_EINVAL); + Require(sceKernelReleaseDirectMemory(first, page) == 0); + type = -1; + start = -1; + end = -1; + Require(sceKernelGetDirectMemoryType(first, &type, &start, &end) == SCE_KERNEL_ERROR_ENOENT); + Require(type == -1 && start == -1 && end == -1); + Require(sceKernelGetDirectMemoryType(-1, &type, &start, &end) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelGetDirectMemoryType(first + page, &type, &start, &end) == 0); + Require(type == 3 && start == first + page && end == first + page * 2); + Require(sceKernelReleaseDirectMemory(first + page, page * 2) == 0); + Require(sceKernelGetDirectMemoryType(second, &type, &start, &end) == SCE_KERNEL_ERROR_ENOENT); +} + +static void CheckMtypeprotect() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 3, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page * 3, 3, 0, phys, 0) == 0); + auto* bytes = static_cast(mapped); + Require(sceKernelMtypeprotect(bytes + page + 1, 1, 3, 1) == 0); + const auto check = [&](std::size_t index, int expectedType, int expectedProtection) { + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(bytes + page * index, 0, &info, sizeof(info)) == 0); + Require(info.is_direct && info.memory_type == expectedType && info.protection == expectedProtection); + Require(info.start == reinterpret_cast(bytes + page * index) && info.end == info.start + page); + Require(info.offset == static_cast(phys) + page * index); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(phys + static_cast(page * index), &type, &start, &end) == 0); + Require(type == expectedType && start == phys + static_cast(page * index) && end == start + static_cast(page)); + }; + check(0, 0, 3); + check(1, 3, 1); + check(2, 0, 3); + KernelBatchMapEntry entry{}; + entry.start = bytes + page * 2; + entry.length = page; + entry.protection = 3; + entry.type = 5; + entry.operation = 4; + int processed = -1; + Require(sceKernelBatchMap2(&entry, 1, &processed, 0) == 0 && processed == 1); + check(0, 0, 3); + check(1, 3, 1); + check(2, 5, 3); + Require(sceKernelMtypeprotect(bytes, page * 3, 2, 3) == 0); + VirtualQueryInfo whole{}; + Require(sceKernelVirtualQuery(bytes, 0, &whole, sizeof(whole)) == 0); + Require(whole.memory_type == 2 && whole.protection == 3); + int type = -1; + std::int64_t start = -1; + std::int64_t end = -1; + Require(sceKernelGetDirectMemoryType(phys + static_cast(page * 2), &type, &start, &end) == 0 && type == 2); + Require(sceKernelMunmap(mapped, page * 3) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 3) == 0); +} + +static void CheckDirectMemoryGpuProtBits() { + constexpr std::size_t page = 0x4000; + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* mapping = nullptr; + Require(sceKernelMapDirectMemory(&mapping, page, 0x3f2, 0, phys, 0) == 0); + static_cast(mapping)[0] = 11; + Require(static_cast(mapping)[0] == 11); + Require(sceKernelMunmap(mapping, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} + static void CheckFixedVirtualReservation() { constexpr std::size_t page = 0x4000; void* probe = nullptr; @@ -216,7 +394,8 @@ static void CheckFixedVirtualReservation() { Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0x10, 0) == 0); Require(reserved == requested); VirtualQueryInfo after{}; - Require(sceKernelVirtualQuery(reserved, 0, &after, sizeof(after)) != 0); + Require(sceKernelVirtualQuery(reserved, 0, &after, sizeof(after)) == 0); + Require(!after.is_committed && !after.is_direct && after.protection == 0); void* remapped = requested; Require(sceKernelMapDirectMemory(&remapped, page * 2, 3, 0x10, phys, 0) == 0); Require(remapped == requested); @@ -238,6 +417,52 @@ static void CheckFixedVirtualReservation() { Require(sceKernelMunmap(pooled, page * 2) == 0); } +static void CheckReservedRangeIsNotCommitted() { + constexpr std::size_t page = 0x4000; + void* reserved = nullptr; + Require(sceKernelReserveVirtualRange(&reserved, page * 2, 0, 0) == 0); + const auto start = reinterpret_cast(reserved); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(reserved, 0, &info, sizeof(info)) == 0); + Require(!info.is_committed && !info.is_direct && !info.is_flexible && info.protection == 0); + Require(info.start == start && info.end == start + page * 2); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page, 0, 0, &phys) == 0); + void* fixed = reserved; + Require(sceKernelMapDirectMemory(&fixed, page, 3, 0x10, phys, 0) == 0); + Require(fixed == reserved); + static_cast(fixed)[0] = 7; + Require(sceKernelVirtualQuery(reserved, 0, &info, sizeof(info)) == 0); + Require(info.is_committed && info.is_direct && info.protection == 3); + Require(info.start == start && info.end == start + page); + Require(sceKernelVirtualQuery(static_cast(reserved) + page, 0, &info, sizeof(info)) == 0); + Require(!info.is_committed && info.protection == 0); + Require(info.start == start + page && info.end == start + page * 2); + Require(sceKernelMunmap(reserved, page * 2) == 0); + Require(sceKernelReleaseDirectMemory(phys, page) == 0); +} + +static void CheckNoOverwriteRefusesLiveMapping() { + constexpr std::size_t page = 0x4000; + constexpr int outOfMemory = static_cast(0x8002000cu); + std::int64_t phys = 0; + Require(sceKernelAllocateDirectMemory(0, 0x7fffffffffll, page * 2, 0, 0, &phys) == 0); + void* mapped = nullptr; + Require(sceKernelMapDirectMemory(&mapped, page, 3, 0, phys, 0) == 0); + static_cast(mapped)[0] = 13; + void* again = mapped; + Require(sceKernelMapDirectMemory(&again, page, 3, 0x90, phys + page, 0) == outOfMemory); + Require(again == mapped); + void* flexible = mapped; + Require(sceKernelMapFlexibleMemory(&flexible, page, 3, 0x90) == outOfMemory); + Require(static_cast(mapped)[0] == 13); + VirtualQueryInfo info{}; + Require(sceKernelVirtualQuery(mapped, 0, &info, sizeof(info)) == 0); + Require(info.is_direct && info.offset == static_cast(phys)); + Require(sceKernelMunmap(mapped, page) == 0); + Require(sceKernelReleaseDirectMemory(phys, page * 2) == 0); +} + #ifdef _WIN32 static void CheckNoOverwriteRejectsHostOccupiedMapping() { constexpr std::size_t page = 0x4000; @@ -720,13 +945,22 @@ static void CheckDirectMemoryWriteWatch() { #endif int main() { + CheckReleaseFlexibleMemory(); CheckNamedAndHintedMappings(); CheckInternalNamedFlexibleMapping(); + CheckBatchMapStopsAtInvalidEntry(); + CheckCheckedReleaseDirectMemory(); + CheckAudioCoprocessorProtection(); CheckDirectMemoryFollowsPhysicalPages(); CheckReleaseDirectMemoryClearsMappings(); + CheckDirectMemoryGpuProtBits(); CheckFixedVirtualReservation(); + CheckReservedRangeIsNotCommitted(); + CheckNoOverwriteRefusesLiveMapping(); CheckMlock(); CheckSharedDirectMemoryLifecycle(); + CheckGetDirectMemoryType(); + CheckMtypeprotect(); CheckHeapAfterMappingReuse(); #ifdef _WIN32 CheckNoOverwriteRejectsHostOccupiedMapping(); diff --git a/core/libs/tests/GuestModuleInfo.cpp b/core/libs/tests/GuestModuleInfo.cpp new file mode 100644 index 000000000..5921931c9 --- /dev/null +++ b/core/libs/tests/GuestModuleInfo.cpp @@ -0,0 +1,64 @@ +#include "SceTypes.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include +#include +#include +#include +#include +extern "C" { +void* APS5_VABI dlopen_nid_postfix(const char*, int); +void* APS5_VABI dlsym_nid_postfix(void*, const char*); +int APS5_VABI dlclose_nid_postfix(void*); +int APS5_VABI sceKernelGetModuleInfoFromAddr(std::uint64_t, int, ModuleInfoEx*); +} +static void Require(bool value) { if (!value) std::abort(); } +template +static bool ThrowsInvalidArgument(TFunction function) { + try { + function(); + } catch (const std::invalid_argument&) { + return true; + } + return false; +} +static ModuleInfoEx Query(const void* address, int expected) { + ModuleInfoEx info{}; + info.st_size = sizeof(ModuleInfoEx); + Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(address), 2, &info) == expected); + return info; +} +int main(int argc, char** argv) { + Require(argc == 2); + void* module = dlopen_nid_postfix(argv[1], 2); + Require(module != nullptr); + const void* add = dlsym_nid_postfix(module, "GuestModuleAdd"); + Require(add != nullptr); + const auto info = Query(add, 0); + Require(info.st_size == sizeof(ModuleInfoEx)); + Require(info.id == static_cast(reinterpret_cast(module))); + Require(std::string(info.name) == std::string(argv[1]).substr(std::string(argv[1]).find_last_of("/\\") + 1)); + Require(info.segment_count > 0 && info.segment_count <= 4 && info.ref_count == 1); + bool executable = false; + for (std::uint32_t i = 0; i < info.segment_count; ++i) { + const auto& segment = info.segments[i]; + if (reinterpret_cast(add) >= segment.address && reinterpret_cast(add) - segment.address < segment.size) + executable = (segment.prot & 5) == 5; + } + Require(executable); + Require(info.eh_frame_hdr_addr != 0 && info.eh_frame_hdr_size != 0 && info.eh_frame_addr != 0 && info.eh_frame_size != 0); + Require(info.init_proc_addr == 0 || info.init_proc_addr >= info.segments[0].address); + const auto self = Query(reinterpret_cast(&Require), 0); + Require(self.id != info.id && Query(reinterpret_cast(&Query), 0).id == self.id); + void* second = dlopen_nid_postfix(argv[1], 2); + Require(second && Query(add, 0).id == info.id); + Require(dlclose_nid_postfix(second) == 0); + int local = 0; + Query(&local, SCE_KERNEL_ERROR_ESRCH); + Require(sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, nullptr) == SCE_KERNEL_ERROR_EFAULT); + ModuleInfoEx invalid{}; + invalid.st_size = sizeof(ModuleInfoEx); + Require(ThrowsInvalidArgument([&] { sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 1, &invalid); })); + invalid.st_size = sizeof(ModuleInfoEx) - 8; + Require(ThrowsInvalidArgument([&] { sceKernelGetModuleInfoFromAddr(reinterpret_cast(add), 2, &invalid); })); + Require(dlclose_nid_postfix(module) == 0); +} diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index 34f98d06b..dd43d10b4 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -6,6 +6,7 @@ extern "C" { int APS5_VABI sceMsgDialogInitialize(void); int APS5_VABI sceMsgDialogOpen(const void* param); +int APS5_VABI sceMsgDialogGetStatus(void); int APS5_VABI sceMsgDialogUpdateStatus(void); int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result); int APS5_VABI sceMsgDialogClose(void); @@ -31,19 +32,23 @@ int main() { Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == 0); + Require(sceMsgDialogGetStatus() == 0); Require(sceMsgDialogInitialize() == 0); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); + Require(sceMsgDialogGetStatus() == kStatusInitialized); Require(sceMsgDialogProgressBarSetValue(0, 50) == kErrNotRunning); Require(sceMsgDialogOpen(param) == 0); Require(sceMsgDialogUpdateStatus() == kStatusFinished); + Require(sceMsgDialogGetStatus() == kStatusFinished); Require(sceMsgDialogProgressBarInc(0, 10) == kErrNotRunning); Require(sceMsgDialogProgressBarSetMsg(0, "progress") == kErrNotRunning); Require(sceMsgDialogProgressBarSetValue(0, 100) == kErrNotRunning); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusFinished); + Require(sceMsgDialogGetStatus() == kStatusFinished); MsgDialogResult result{}; result.button_id = -1; @@ -53,4 +58,5 @@ int main() { Require(sceMsgDialogTerminate() == 0); Require(sceMsgDialogClose() == kErrNotRunning); + Require(sceMsgDialogGetStatus() == 0); } diff --git a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp b/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp deleted file mode 100644 index 8b5c7b585..000000000 --- a/core/libs/tests/GuestNpCppWebApiIntrusivePtr.cpp +++ /dev/null @@ -1,256 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" -#include -#include -#include -#include -#include - -using Deleter = void (APS5_VABI*)(void*); - -struct IntrusivePtr { - void* object; - Deleter deleter; - void* libContext; -}; - -using Construct = void (APS5_VABI*)(IntrusivePtr*); -using Copy = void (APS5_VABI*)(IntrusivePtr*, const IntrusivePtr*); -using Destroy = void (APS5_VABI*)(IntrusivePtr*); -using Access = void* (APS5_VABI*)(const IntrusivePtr*); - -extern "C" { -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev(IntrusivePtr*); - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(IntrusivePtr*, const IntrusivePtr*); - -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev(IntrusivePtr*); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev(IntrusivePtr*); - -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv(const IntrusivePtr*); -void* APS5_VABI _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv(const IntrusivePtr*); -} - -static void Require(bool value) { if (!value) std::abort(); } - -static constexpr std::size_t ObjectSize = 0x40; -static constexpr std::size_t VectorReferenceCountOffset = 0x30; - -struct DestroyCase { - Destroy destroy; - std::size_t referenceCountOffset; -}; - -static void* deletedObject = nullptr; -static int deleterCalls = 0; - -static void APS5_VABI RecordDelete(void* object) { - deletedObject = object; - ++deleterCalls; -} - -static std::int32_t ReadCount(const unsigned char* object, const std::size_t offset) { - std::int32_t value; - std::memcpy(&value, object + offset, sizeof(value)); - return value; -} - -static void WriteCount(unsigned char* object, const std::size_t offset, const std::int32_t value) { - std::memcpy(object + offset, &value, sizeof(value)); -} - -static bool OnlyCountChanged(const unsigned char* object, const std::size_t offset) { - for (std::size_t index = 0; index < ObjectSize; ++index) { - if (index >= offset && index < offset + sizeof(std::int32_t)) continue; - if (object[index] != 0x5a) return false; - } - return true; -} - -static void TestConstruct(const Construct construct) { - IntrusivePtr pointer; - std::memset(&pointer, 0xa5, sizeof(pointer)); - construct(&pointer); - Require(pointer.object == nullptr); - Require(pointer.deleter == nullptr); - Require(pointer.libContext == nullptr); -} - -static void TestAccess(const Access access) { - int object = 0; - int context = 0; - const IntrusivePtr empty{nullptr, RecordDelete, &context}; - Require(access(&empty) == nullptr); - const IntrusivePtr filled{&object, nullptr, nullptr}; - Require(access(&filled) == &object); -} - -static void TestCopy(const Copy copy) { - alignas(8) unsigned char object[ObjectSize]; - std::memset(object, 0x5a, sizeof(object)); - WriteCount(object, 0, 1); - int context = 0; - - const IntrusivePtr source{object, RecordDelete, &context}; - IntrusivePtr target; - std::memset(&target, 0xa5, sizeof(target)); - copy(&target, &source); - Require(target.object == object); - Require(target.deleter == RecordDelete); - Require(target.libContext == &context); - Require(source.object == object); - Require(ReadCount(object, 0) == 2); - Require(OnlyCountChanged(object, 0)); - - const IntrusivePtr empty{nullptr, RecordDelete, &context}; - std::memset(&target, 0xa5, sizeof(target)); - copy(&target, &empty); - Require(target.object == nullptr); - Require(target.deleter == RecordDelete); - Require(target.libContext == &context); -} - -static void TestDestroy(const DestroyCase& testCase) { - alignas(8) unsigned char object[ObjectSize]; - std::memset(object, 0x5a, sizeof(object)); - int context = 0; - const std::size_t offset = testCase.referenceCountOffset; - - deleterCalls = 0; - deletedObject = nullptr; - - IntrusivePtr empty{nullptr, RecordDelete, &context}; - testCase.destroy(&empty); - Require(empty.object == nullptr); - Require(empty.deleter == RecordDelete); - Require(empty.libContext == &context); - Require(deleterCalls == 0); - - WriteCount(object, offset, 2); - IntrusivePtr shared{object, RecordDelete, &context}; - testCase.destroy(&shared); - Require(ReadCount(object, offset) == 1); - Require(shared.object == object); - Require(shared.deleter == RecordDelete); - Require(shared.libContext == &context); - Require(deleterCalls == 0); - - IntrusivePtr last{object, RecordDelete, &context}; - testCase.destroy(&last); - Require(ReadCount(object, offset) == 0); - Require(OnlyCountChanged(object, offset)); - Require(deleterCalls == 1); - Require(deletedObject == object); - Require(last.object == nullptr); - Require(last.deleter == nullptr); - Require(last.libContext == &context); - - WriteCount(object, offset, 1); - IntrusivePtr pooled{object, nullptr, &context}; - bool thrown = false; - try { - testCase.destroy(&pooled); - } catch (const std::runtime_error&) { - thrown = true; - } - Require(thrown); - Require(deleterCalls == 1); -} - -static void TestSharedOwnership() { - alignas(8) unsigned char object[ObjectSize]; - std::memset(object, 0x5a, sizeof(object)); - WriteCount(object, 0, 1); - deleterCalls = 0; - deletedObject = nullptr; - - IntrusivePtr owner; - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev(&owner); - owner.object = object; - owner.deleter = RecordDelete; - - IntrusivePtr argument; - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_(&argument, &owner); - Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(&argument) == object); - Require(ReadCount(object, 0) == 2); - - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(&argument); - Require(ReadCount(object, 0) == 1); - Require(deleterCalls == 0); - - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev(&owner); - Require(ReadCount(object, 0) == 0); - Require(deleterCalls == 1); - Require(deletedObject == object); - Require(_ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv(&owner) == nullptr); -} - -int main() { - Require(sizeof(IntrusivePtr) == 0x18); - - const Construct constructors[] = { - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEC1Ev, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEC1Ev, - }; - for (const auto construct : constructors) TestConstruct(construct); - - const Access accessors[] = { - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V15EntryEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6BinaryEEptEv, - _ZNK3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEEdeEv, - }; - for (const auto access : accessors) TestAccess(access); - - const Copy copies[] = { - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEEC1ERS7_, - _ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEEC1ERS7_, - }; - for (const auto copy : copies) TestCopy(copy); - - const DestroyCase destructors[] = { - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V121GetRankingRequestBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122GetRankingResponseBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V122RecordScoreRequestBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V123RecordScoreResponseBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V130GetBoardDefinitionResponseBodyEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V14UserEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS1_12Leaderboards2V19RecordApi26RecordScoreResponseHeadersEED1Ev, 0}, - {_ZN3sce2Np9CppWebApi6Common12IntrusivePtrINS2_6VectorINS3_INS1_12Leaderboards2V15EntryEEEEEED1Ev, VectorReferenceCountOffset}, - }; - for (const auto& testCase : destructors) TestDestroy(testCase); - - TestSharedOwnership(); - return 0; -} diff --git a/core/libs/tests/GuestNpCppWebApiString.cpp b/core/libs/tests/GuestNpCppWebApiString.cpp deleted file mode 100644 index c54ee17c3..000000000 --- a/core/libs/tests/GuestNpCppWebApiString.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "prx/libc/include/general/VabiMacros.hpp" -#include -#include -#include -#include -#include - -extern "C" { -void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(void* self, void* libContext); -void APS5_VABI _ZN3sce2Np9CppWebApi6Common6StringD1Ev(void* self); -const char* APS5_VABI _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(const void* self); -} - -namespace { - -void Require(bool value) { if (!value) std::abort(); } - -constexpr std::size_t kStringSize = 0x20; -constexpr std::size_t kGuardSize = 0x10; -constexpr std::size_t kReferenceCountOffset = 0x00; -constexpr std::size_t kBufferOffset = 0x08; -constexpr std::size_t kBufferSizeOffset = 0x10; -constexpr std::size_t kLibContextOffset = 0x18; -constexpr unsigned char kFill = 0xA5; - -struct Storage { - alignas(8) unsigned char bytes[kStringSize + kGuardSize]; -}; - -template -TValue Load(const Storage& storage, std::size_t offset) { - TValue value; - std::memcpy(&value, storage.bytes + offset, sizeof(value)); - return value; -} - -template -void Store(Storage& storage, std::size_t offset, TValue value) { - std::memcpy(storage.bytes + offset, &value, sizeof(value)); -} - -bool GuardIntact(const Storage& storage) { - for (std::size_t i = kStringSize; i < sizeof(storage.bytes); i++) { - if (storage.bytes[i] != kFill) return false; - } - return true; -} - -bool DestructorThrows(Storage& storage) { - try { - _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); - } catch (const std::runtime_error&) { - return true; - } - return false; -} - -void TestConstructedStringIsEmpty() { - std::uint64_t libContext = 1; - Storage storage; - std::memset(storage.bytes, kFill, sizeof(storage.bytes)); - - _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); - - Require(Load(storage, kReferenceCountOffset) == 0); - Require(Load(storage, kBufferOffset) == nullptr); - Require(Load(storage, kBufferSizeOffset) == 0); - Require(Load(storage, kLibContextOffset) == &libContext); - Require(GuardIntact(storage)); - - const char* text = _ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes); - Require(text != nullptr && text[0] == '\0'); - - Storage before = storage; - _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); - Require(std::memcmp(before.bytes, storage.bytes, sizeof(storage.bytes)) == 0); -} - -void TestNullLibContextIsStored() { - Storage storage; - std::memset(storage.bytes, kFill, sizeof(storage.bytes)); - - _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, nullptr); - - Require(Load(storage, kLibContextOffset) == nullptr); - Require(Load(storage, kBufferOffset) == nullptr); - Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes)[0] == '\0'); - _ZN3sce2Np9CppWebApi6Common6StringD1Ev(storage.bytes); -} - -void TestBufferIsReturnedAndNotDroppedSilently() { - std::uint64_t libContext = 1; - char text[] = "ranking"; - Storage storage; - std::memset(storage.bytes, kFill, sizeof(storage.bytes)); - - _ZN3sce2Np9CppWebApi6Common6StringC1EPNS2_10LibContextE(storage.bytes, &libContext); - Store(storage, kBufferOffset, text); - Store(storage, kBufferSizeOffset, sizeof(text)); - - Require(_ZNK3sce2Np9CppWebApi6Common6String5c_strEv(storage.bytes) == text); - Require(DestructorThrows(storage)); - Require(std::strcmp(text, "ranking") == 0); -} - -} - -int main() { - TestConstructedStringIsEmpty(); - TestNullLibContextIsStored(); - TestBufferIsReturnedAndNotDroppedSilently(); -} diff --git a/core/libs/tests/GuestNpSessionSignaling.cpp b/core/libs/tests/GuestNpSessionSignaling.cpp index 10ec8b222..834f15fb4 100644 --- a/core/libs/tests/GuestNpSessionSignaling.cpp +++ b/core/libs/tests/GuestNpSessionSignaling.cpp @@ -14,7 +14,7 @@ int APS5_VABI sceNpSessionSignalingTerminate(void); namespace { -constexpr int InvalidArgument = static_cast(0x80552D02u); +constexpr int InvalidArgument = static_cast(0x80553303u); constexpr int Unavailable = static_cast(0x80552D06u); void Require(bool condition, const char* message) { diff --git a/core/libs/tests/GuestNpTrophy2.cpp b/core/libs/tests/GuestNpTrophy2.cpp index 5a286ab35..d52cc50ba 100644 --- a/core/libs/tests/GuestNpTrophy2.cpp +++ b/core/libs/tests/GuestNpTrophy2.cpp @@ -1,9 +1,12 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceNpTrophy2/include/NpTrophy2Types.hpp" #include +#include extern "C" { int APS5_VABI sceNpTrophy2RegisterUnlockCallback(void*, void*); int APS5_VABI sceNpTrophy2UnregisterUnlockCallback(); +int APS5_VABI sceNpTrophy2GetGameInfo(int, int, NpTrophy2GameDetails*, NpTrophy2GameData*); } static void Require(bool value) { if (!value) std::abort(); } @@ -17,4 +20,17 @@ int main() { Require(sceNpTrophy2UnregisterUnlockCallback() == 0); Require(sceNpTrophy2RegisterUnlockCallback(reinterpret_cast(&OnUnlock), &userdata) == 0); Require(sceNpTrophy2UnregisterUnlockCallback() == 0); + + NpTrophy2GameDetails details{}; + NpTrophy2GameData data{}; + Require(sceNpTrophy2GetGameInfo(1, 1, &details, nullptr) == 0); + Require(details.num_trophies != 0); + Require(sceNpTrophy2GetGameInfo(1, 1, nullptr, &data) == 0); + bool threw = false; + try { + sceNpTrophy2GetGameInfo(1, 1, nullptr, nullptr); + } catch (const std::invalid_argument&) { + threw = true; + } + Require(threw); } diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 522d99171..866c44ba7 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -12,6 +12,8 @@ int APS5_VABI scePadClose_nid_postfix(int); int APS5_VABI scePadGetHandle(int, int, int); int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool); int APS5_VABI scePadInit_nid_postfix(void); +int APS5_VABI scePadRead_nid_postfix(int, PadData*, int); +int APS5_VABI scePadReadState(int, PadData*); int APS5_VABI scePadSetTiltCorrectionState(int, bool); int APS5_VABI scePadResetOrientation(int); int APS5_VABI scePadSetAngularVelocityDeadbandState(int, bool); @@ -57,6 +59,14 @@ static void CheckTouchContact() { Pad::ReadState(); } +static void CheckReadStateHandle(int handle) { + PadData data{}; + Require(scePadReadState(0, &data) == PAD_ERROR_INVALID_HANDLE); + Require(scePadReadState(handle + 1, &data) == PAD_ERROR_INVALID_HANDLE); + Require(scePadRead_nid_postfix(0, &data, 1) == PAD_ERROR_INVALID_HANDLE); + Require(scePadReadState(handle, &data) == PAD_OK); +} + int main() { constexpr int noHandle = static_cast(0x80920008); constexpr int user = 0x10000000; @@ -71,6 +81,7 @@ int main() { Require(scePadGetHandle(user, 2, 0) == handle); CheckTiltCorrection(handle); CheckTouchContact(); + CheckReadStateHandle(handle); Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); diff --git a/core/libs/tests/GuestPthreadBarrier.cpp b/core/libs/tests/GuestPthreadBarrier.cpp index d7286b7ee..64d67e201 100644 --- a/core/libs/tests/GuestPthreadBarrier.cpp +++ b/core/libs/tests/GuestPthreadBarrier.cpp @@ -66,14 +66,16 @@ int main() { Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, 2) == GUEST_EINVAL); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, -1) == GUEST_EINVAL); - bool rejected = false; - try { pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED); } - catch (const std::runtime_error&) { rejected = true; } - Require(rejected); + Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_SHARED) == 0); + Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_SHARED); + Context shared; + Require(pthread_barrier_init_nid_postfix(&shared.barrier, &attr, WAITERS) == 0); + RunWaiters(shared); + Require(pthread_barrier_destroy_nid_postfix(&shared.barrier) == 0); Require(pthread_barrierattr_setpshared_nid_postfix(&attr, PROCESS_PRIVATE) == 0); pshared = -1; Require(pthread_barrierattr_getpshared_nid_postfix(&attr, &pshared) == 0 && pshared == PROCESS_PRIVATE); - rejected = false; + bool rejected = false; try { pthread_barrierattr_getpshared_nid_postfix(&attr, nullptr); } catch (const std::invalid_argument&) { rejected = true; } Require(rejected); diff --git a/core/libs/tests/GuestRazorCpu.cpp b/core/libs/tests/GuestRazorCpu.cpp index 0f1469a82..81dcc1780 100644 --- a/core/libs/tests/GuestRazorCpu.cpp +++ b/core/libs/tests/GuestRazorCpu.cpp @@ -9,6 +9,7 @@ int APS5_VABI sceRazorCpuJobManagerJob(const void* args); int APS5_VABI sceRazorCpuJobManagerSequence(const void* args); int APS5_VABI sceRazorCpuPushMarkerStatic(const char* name, std::uint32_t color, std::uint32_t flags); int APS5_VABI sceRazorCpuPopMarker(void); +int APS5_VABI sceRazorCpuFlushOccurred(std::uint64_t* timeSpentInFlush); } namespace { @@ -28,4 +29,8 @@ int main() { Require(sceRazorCpuPopMarker() == 0); Require(sceRazorCpuPushMarkerStatic("unbalanced", 0x80ffffffu, 2) == 0); Require(sceRazorCpuIsCapturing() == 0); + std::uint64_t timeSpentInFlush = 0x123456789abcdef0ull; + Require(sceRazorCpuFlushOccurred(&timeSpentInFlush) == 0); + Require(timeSpentInFlush == 0); + Require(sceRazorCpuFlushOccurred(nullptr) == 0); } diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp index 2d843ebfe..576dacbb8 100644 --- a/core/libs/tests/GuestRtc.cpp +++ b/core/libs/tests/GuestRtc.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -22,6 +23,8 @@ int APS5_VABI sceRtcGetTime_t(const RtcDateTime*, std::int64_t*); int APS5_VABI sceRtcSetTime_t(RtcDateTime*, std::int64_t); int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime*, std::uint64_t*); int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime*, std::uint64_t); +int APS5_VABI sceRtcGetDosTime(const RtcDateTime*, std::uint32_t*); +int APS5_VABI sceRtcSetDosTime(RtcDateTime*, std::uint32_t); int APS5_VABI sceRtcFormatRFC3339(char*, const RtcTick*, int); int APS5_VABI sceRtcParseRFC3339(RtcTick*, const char*); int APS5_VABI sceRtcParseDateTime(RtcTick*, const char*); @@ -30,10 +33,23 @@ int APS5_VABI sceRtcTickAddSeconds(RtcTick*, const RtcTick*, std::int64_t); int APS5_VABI sceRtcTickAddDays(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddMonths(RtcTick*, const RtcTick*, std::int32_t); int APS5_VABI sceRtcTickAddYears(RtcTick*, const RtcTick*, std::int16_t); +int APS5_VABI sceRtcFormatRFC3339LocalTime(char*, const RtcTick*); +int APS5_VABI sceRtcFormatRFC2822(char*, const RtcTick*, int); +int APS5_VABI sceRtcFormatRFC2822LocalTime(char*, const RtcTick*); } static void Require(bool value) { if (!value) std::abort(); } +static void SetTimeZone(const char* zone) { +#ifdef _WIN32 + _putenv_s("TZ", zone); + _tzset(); +#else + setenv("TZ", zone, 1); + tzset(); +#endif +} + static bool Equal(const RtcDateTime& left, const RtcDateTime& right) { return left.year == right.year && left.month == right.month && left.day == right.day && left.hour == right.hour && left.minute == right.minute && left.second == right.second && left.microsecond == right.microsecond; @@ -92,6 +108,28 @@ int main() { std::uint64_t fileTime = 0; Require(sceRtcGetWin32FileTime(&epoch, &fileTime) == 0 && fileTime == 116444736000000000ull); Require(sceRtcSetWin32FileTime(&converted, 116444736000000000ull) == 0 && Equal(converted, epoch)); + std::uint32_t dosTime = 0xffffffffu; + const RtcDateTime dosDate{2024, 2, 29, 12, 34, 57, 789000}; + Require(sceRtcGetDosTime(&dosDate, &dosTime) == 0 && dosTime == 0x585d645cu); + const RtcDateTime dosFirst{1980, 1, 1, 0, 0, 0, 0}; + Require(sceRtcGetDosTime(&dosFirst, &dosTime) == 0 && dosTime == 0x00210000u); + const RtcDateTime dosLast{2107, 12, 31, 23, 59, 59, 0}; + Require(sceRtcGetDosTime(&dosLast, &dosTime) == 0 && dosTime == 0xff9fbf7du); + Require(sceRtcGetDosTime(&dosDate, nullptr) == invalidPointer); + Require(sceRtcGetDosTime(nullptr, &dosTime) == invalidPointer); + const RtcDateTime dosBadMonth{2024, 13, 1, 0, 0, 0, 0}; + Require(sceRtcGetDosTime(&dosBadMonth, &dosTime) == invalidMonth); + const RtcDateTime dosEarly{1979, 12, 31, 0, 0, 0, 0}; + bool dosEarlyThrew = false; + try { sceRtcGetDosTime(&dosEarly, &dosTime); } catch (const std::exception&) { dosEarlyThrew = true; } + Require(dosEarlyThrew); + converted = RtcDateTime{1, 1, 1, 1, 1, 1, 1}; + Require(sceRtcSetDosTime(&converted, 0x585d645cu) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 12, 34, 56, 0})); + Require(sceRtcSetDosTime(&converted, 0x7f9fbf7du) == 0 && Equal(converted, RtcDateTime{2043, 12, 31, 23, 59, 58, 0})); + Require(sceRtcSetDosTime(&converted, 0) == 0 && Equal(converted, RtcDateTime{1980, 0, 0, 0, 0, 0, 0})); + Require(sceRtcSetDosTime(nullptr, 0) == invalidPointer); + Require(sceRtcSetDosTime(&converted, 0xff9fbf7du) == 0 && Equal(converted, RtcDateTime{2107, 12, 31, 23, 59, 58, 0})); + Require(sceRtcSetDosTime(&converted, 0x80210000u) == 0 && Equal(converted, RtcDateTime{2044, 1, 1, 0, 0, 0, 0})); char text[32]; tick.tick = leapDayTick; @@ -150,12 +188,29 @@ int main() { Require(sceRtcParseDateTime(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick); Require(sceRtcParseDateTime(&tick, "2023-02-29T00:00:00") == invalidDay); Require(sceRtcParseDateTime(nullptr, "1970-01-01T00:00:00Z") == invalidPointer); + Require(sceRtcParseDateTime(&tick, "Thu, 29 Feb 2024 12:34:56") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu, 29 Feb 2024 12:34:56 GMT") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu, 29 Feb 2024 14:04:56 +0130") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu, 29 Feb 2024 11:04:56 -0130") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu, 01 Jan 1970 00:00:00 +0000") == 0 && tick.tick == unixEpochTick); + Require(sceRtcParseDateTime(&tick, "Thu, 30 Feb 2024 00:00:00") == invalidDay); + Require(sceRtcParseDateTime(&tick, "Thu Feb 29 12:34:56 2024") == 0 && tick.tick == leapDayTick - 789000ull); + Require(sceRtcParseDateTime(&tick, "Thu Jan 1 00:00:00 1970") == 0 && tick.tick == unixEpochTick); + Require(sceRtcParseDateTime(&tick, "Thu Feb 30 00:00:00 2024") == invalidDay); const char* unparseable[] = { "2024-02-29", "2024/02/29T12:34:56", "2024-02-29T12:34:56Zjunk", "2024-02-29T12:34:56+99:99", "2024-02-29T12:34:56.", + "Thu, 29 Feb 2024 12:34:56 +01:30", + "Thu, 29 Feb 2024 12:34:56 PST", + "Thu, 29 Xyz 2024 12:34:56", + "Xyz, 29 Feb 2024 12:34:56", + "Thu, 29 Feb 2024 12:34:56 +2400", + "Thu Feb 29 12:34:56 2024\n", + "Thu Feb 29 12:34:56 24", + "Thu Feb 29 12:34:56 2024", }; for (const char* text : unparseable) { bool thrown = false; @@ -224,4 +279,50 @@ int main() { RtcTick back{}; Require(sceRtcConvertUtcToLocalTime(&now, &local) == 0); Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == now.tick); + + char rfc2822[32]; + tick.tick = leapDayTick; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:34:56 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 90) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 14:04:56 +0130") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -300) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 07:34:56 -0500") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -30) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:04:56 -0030") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 65) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 13:39:56 +0105") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1439) == 0 && std::strcmp(rfc2822, "Fri, 01 Mar 2024 12:33:56 +2359") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1439) == 0 && std::strcmp(rfc2822, "Wed, 28 Feb 2024 12:35:56 -2359") == 0); + const char* week[] = {"Sun, 03 Mar", "Mon, 04 Mar", "Tue, 05 Mar", "Wed, 06 Mar", "Thu, 07 Mar", "Fri, 08 Mar", "Sat, 09 Mar"}; + for (int day = 0; day < 7; ++day) { + const RtcDateTime date{2024, 3, static_cast(3 + day), 0, 0, 0, 0}; + Require(sceRtcGetTick(&date, &tick) == 0 && sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0); + Require(std::strncmp(rfc2822, week[day], std::strlen(week[day])) == 0); + } + tick.tick = 0; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Mon, 01 Jan 0001 00:00:00 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, -1) == invalidValue); + tick.tick = maxTick; + Require(sceRtcFormatRFC2822(rfc2822, &tick, 0) == 0 && std::strcmp(rfc2822, "Fri, 31 Dec 9999 23:59:59 +0000") == 0); + Require(sceRtcFormatRFC2822(rfc2822, &tick, 1) == invalidValue); + tick.tick = leapDayTick; + for (int offset : {1440, -1440, std::numeric_limits::min(), std::numeric_limits::max()}) { + std::memset(rfc2822, 'x', sizeof(rfc2822)); + char original[sizeof(rfc2822)]; + std::memcpy(original, rfc2822, sizeof(rfc2822)); + Require(sceRtcFormatRFC2822(rfc2822, &tick, offset) == invalidValue); + Require(std::memcmp(rfc2822, original, sizeof(rfc2822)) == 0); + } + Require(sceRtcFormatRFC2822(nullptr, &tick, 0) == invalidPointer); + Require(sceRtcFormatRFC2822(rfc2822, nullptr, 0) == invalidPointer); + + SetTimeZone("XXX-5:30"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 18:04:56 +0530") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T18:04:56.78+05:30") == 0); + SetTimeZone("XXX+3"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 09:34:56 -0300") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T09:34:56.78-03:00") == 0); + SetTimeZone("UTC0"); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, &tick) == 0 && std::strcmp(rfc2822, "Thu, 29 Feb 2024 12:34:56 +0000") == 0); + Require(sceRtcFormatRFC3339LocalTime(text, &tick) == 0 && std::strcmp(text, "2024-02-29T12:34:56.78Z") == 0); + Require(sceRtcFormatRFC2822LocalTime(nullptr, &tick) == invalidPointer); + Require(sceRtcFormatRFC2822LocalTime(rfc2822, nullptr) == invalidPointer); + Require(sceRtcFormatRFC3339LocalTime(nullptr, &tick) == invalidPointer); + Require(sceRtcFormatRFC3339LocalTime(text, nullptr) == invalidPointer); } diff --git a/core/libs/tests/GuestScanCount.cpp b/core/libs/tests/GuestScanCount.cpp new file mode 100644 index 000000000..f77e73b62 --- /dev/null +++ b/core/libs/tests/GuestScanCount.cpp @@ -0,0 +1,70 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sscanf_s_nid_postfix(const char*, const char*, ...); + +static int failures = 0; + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + ++failures; + } +} + +template static void CheckCount(const char* modifier, int count) { + struct alignas(8) Destination { + TValue value; + std::array guard; + } destination{static_cast(-1), {}}; + destination.guard.fill(0xa5); + const auto guard = destination.guard; + const std::string input(static_cast(count), 'x'); + const std::string format = input + "%" + modifier + "n"; + const int assigned = sscanf_s_nid_postfix(input.c_str(), format.c_str(), &destination.value); + if (destination.value != count || destination.guard != guard || assigned != 0) { + std::fprintf(stderr, "%%%sn after %d bytes: count=%lld, guard=%s, assignments=%d\n", + modifier, count, static_cast(destination.value), + destination.guard == guard ? "unchanged" : "overwritten", assigned); + ++failures; + } +} + +int main() { + for (const int count : {0, 3, 127}) { + CheckCount("hh", count); + CheckCount("h", count); + CheckCount("", count); + CheckCount("l", count); + CheckCount("ll", count); + CheckCount("j", count); + CheckCount("z", count); + CheckCount("t", count); + } + CheckCount("h", 257); + CheckCount("l", 257); + + int number = -1; + std::int64_t before = -1; + std::int64_t after = -1; + char word[4] = {}; + Require(sscanf_s_nid_postfix(" 12 abc!", "%ln%d %3[a-z]%jn!", &before, &number, + word, 4u, &after) == 2, "Count conversions must not increase assignments"); + Require(before == 0 && number == 12 && std::strcmp(word, "abc") == 0 && after == 7, + "Counts must include leading whitespace and earlier conversions"); + + after = -1; + Require(sscanf_s_nid_postfix("123x", "%*d%tn", &after) == 0 && after == 3, + "Count must include suppressed input without consuming a capacity argument"); + after = -1; + Require(sscanf_s_nid_postfix("x", "%d%lln", &number, &after) == 0 && after == -1, + "Matching failure must leave a later count untouched"); + Require(sscanf_s_nid_postfix("", "%d%lln", &number, &after) == EOF && after == -1, + "Input failure must leave a later count untouched"); + return failures == 0 ? 0 : 1; +} diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index fe213c179..4bbb122ce 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include extern "C" { @@ -36,10 +37,91 @@ const char* APS5_VABI sceNetInetNtop(int, const void*, char*, std::uint32_t); int* APS5_VABI sceNetErrnoLoc(void); } +struct NetIovec { + void* base; + std::uint64_t length; +}; + +struct NetMsghdr { + void* name; + std::uint32_t name_length; + NetIovec* iov; + int iov_length; + void* control; + std::uint32_t control_length; + int flags; +}; + +extern "C" { +std::int64_t APS5_VABI sceNetSendmsg(int, const NetMsghdr*, int); +std::int64_t APS5_VABI sceNetRecvmsg(int, NetMsghdr*, int); +} + static void Require(bool condition) { if (!condition) std::abort(); } +static bool Failed(std::int64_t result, int error) { + return result == static_cast(0x80410100u | static_cast(error)) && *sceNetErrnoLoc() == error; +} + +static void CheckMessages(int receiver, int sender, const std::array& address) { + char head[] = "scatter "; + char tail[] = "gather"; + NetIovec out[] = {{head, 8}, {nullptr, 0}, {tail, 6}}; + std::array destination = address; + NetMsghdr send{destination.data(), 16, out, 3, nullptr, 0, 0}; + Require(sceNetSendmsg(sender, &send, 0) == 14); + + char first[5]{}; + char second[20]{}; + NetIovec in[] = {{first, sizeof(first)}, {second, sizeof(second)}}; + std::array source{}; + char control[16]{}; + NetMsghdr receive{source.data(), static_cast(source.size()), in, 2, control, sizeof(control), -1}; + Require(sceNetRecvmsg(receiver, &receive, 2) == 14); + Require(std::memcmp(first, "scatt", 5) == 0 && std::memcmp(second, "er gather", 9) == 0); + Require(receive.flags == 0 && receive.control_length == 0 && receive.name_length == 16); + Require(source[0] == 16 && source[1] == 2 && source[4] == 127 && source[7] == 1); + std::memset(second, 0, sizeof(second)); + receive.iov_length = 1; + receive.flags = -1; + Require(sceNetRecvmsg(receiver, &receive, 0) == 5); + Require(receive.flags == 0x10 && std::memcmp(first, "scatt", 5) == 0 && second[0] == 0); + + NetMsghdr bad = send; + Require(Failed(sceNetSendmsg(sender, nullptr, 0), 14)); + Require(Failed(sceNetRecvmsg(receiver, nullptr, 0), 14)); + bad.iov_length = -1; + Require(Failed(sceNetSendmsg(sender, &bad, 0), 40)); + bad.iov_length = 1025; + Require(Failed(sceNetRecvmsg(receiver, &bad, 0), 40)); + bad.iov_length = 1; + bad.iov = nullptr; + Require(Failed(sceNetSendmsg(sender, &bad, 0), 14)); + NetIovec hole[] = {{nullptr, 1}}; + bad.iov = hole; + Require(Failed(sceNetRecvmsg(receiver, &bad, 0), 14)); + NetIovec huge[] = {{head, 0x7fffffff}, {tail, 1}}; + bad.iov = huge; + bad.iov_length = 2; + Require(Failed(sceNetSendmsg(sender, &bad, 0), 22)); + Require(Failed(sceNetRecvmsg(receiver, &receive, 1), 45)); + Require(Failed(sceNetSendmsg(sender, &send, 1), 45)); + Require(Failed(sceNetRecvmsg(12345, &receive, 0), 9)); + Require(Failed(sceNetSendmsg(12345, &send, 0), 9)); + NetMsghdr with_control = send; + with_control.control = control; + with_control.control_length = sizeof(control); + bool threw = false; + try { + sceNetSendmsg(sender, &with_control, 0); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); +} + static void CheckAddressText(int family, const char* text) { std::array address{}; Require(sceNetInetPton(family, text, address.data()) == 1); @@ -103,6 +185,16 @@ int main() { Require(ready_count == 1 && (ready.events & 1) && ready.ident == static_cast(accepted)); Require(sceNetRecv(accepted, response, sizeof(response), 0) == sizeof(response)); Require(std::strcmp(request, response) == 0); + char reply[] = "stream reply"; + NetIovec reply_out[] = {{reply, 7}, {reply + 7, sizeof(reply) - 7}}; + NetMsghdr reply_send{nullptr, 0, reply_out, 2, nullptr, 0, 0}; + Require(sceNetSendmsg(accepted, &reply_send, 0) == sizeof(reply)); + char reply_result[sizeof(reply)]{}; + NetIovec reply_in[] = {{reply_result, sizeof(reply_result)}}; + std::array reply_name{}; + NetMsghdr reply_receive{reply_name.data(), 16, reply_in, 1, nullptr, 0, -1}; + Require(sceNetRecvmsg(client, &reply_receive, 0) == sizeof(reply)); + Require(std::strcmp(reply, reply_result) == 0 && reply_receive.flags == 0 && reply_receive.name_length == 0); Require(sceNetEpollDestroy(epoll) == 0); Require(sceNetSocketClose(accepted) == 0); bool send_failed = false; @@ -130,6 +222,7 @@ int main() { Require(sceNetRecvfrom(udp_receiver, datagram_result, sizeof(datagram_result), 0, source.data(), &address_size) == sizeof(datagram_result)); Require(std::strcmp(datagram, datagram_result) == 0 && source[1] == 2); + CheckMessages(udp_receiver, udp_sender, address); Require(sceNetSocketClose(udp_sender) == 0); Require(sceNetSocketClose(udp_receiver) == 0); diff --git a/core/libs/tests/GuestStandardStreams.cpp b/core/libs/tests/GuestStandardStreams.cpp index ee322b0d4..714912017 100644 --- a/core/libs/tests/GuestStandardStreams.cpp +++ b/core/libs/tests/GuestStandardStreams.cpp @@ -6,11 +6,16 @@ #include #include #include +#include +#include #include #ifndef _WIN32 #include #endif extern "C" { +FileStream* APS5_VABI fopen_nid_postfix(const char*, const char*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, FileStream*); +std::size_t APS5_VABI fwrite_nid_postfix(const void*, std::size_t, std::size_t, FileStream*); FileStream* APS5_VABI freopen_nid_postfix(const char*, const char*, FileStream*); int APS5_VABI fseeko_nid_postfix(FileStream*, std::int64_t, int); std::int64_t APS5_VABI ftello_nid_postfix(FileStream*); @@ -76,6 +81,54 @@ static int APS5_VABI FormatString(char* buffer, const char* format, ...) { #endif return result; } +static bool CheckFileBytes(const std::string& filename, const std::string& expected, const char* mode, bool reopen) { + std::ifstream file(filename, std::ios::binary); + const std::string actual((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (actual == expected) return true; + std::fprintf(stderr, "%s %s: expected %zu raw bytes, received %zu\n", reopen ? "freopen" : "fopen", mode, expected.size(), actual.size()); + return false; +} + +static bool CheckBinaryModes() { + const auto directory = "anyps5-byte-stream-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + Require(std::filesystem::create_directory(directory)); + const auto filename = directory + "/bytes"; + const std::string original("A\r\n\x1a" "B\0C", 7); + const std::string written("D\n\x1a" "E\0F", 6); + const char* modes[] = {"r", "r+", "w", "w+", "a", "a+", "rb", "rb+", "r+b", "wb", "wb+", "w+b", "ab", "ab+", "a+b"}; + bool correct = true; + for (const auto* mode : modes) { + for (const bool reopen : {false, true}) { + { std::ofstream seed(filename, std::ios::binary | std::ios::trunc); seed.write(original.data(), original.size()); Require(seed.good()); } + FileStream redirected(std::tmpfile()); + FileStream* stream = reopen ? freopen_nid_postfix(filename.c_str(), mode, &redirected) : fopen_nid_postfix(filename.c_str(), mode); + Require(stream != nullptr); + const bool update = std::strchr(mode, '+') != nullptr; + if (*mode == 'r' || (*mode == 'a' && update)) { + Require(fseeko_nid_postfix(stream, 0, SEEK_SET) == 0); + char bytes[16]{}; + const auto count = fread_nid_postfix(bytes, 1, sizeof(bytes), stream); + const bool matches = count == original.size() && std::memcmp(bytes, original.data(), original.size()) == 0; + if (!matches) std::fprintf(stderr, "%s %s: expected 7 input bytes, received %zu\n", reopen ? "freopen" : "fopen", mode, count); + correct &= matches; + Require(fseeko_nid_postfix(stream, 3, SEEK_SET) == 0); + correct &= fgetc_nid_postfix(stream) == 0x1a; + } + std::string expected = original; + if (*mode == 'w' || *mode == 'a' || update) { + Require(fseeko_nid_postfix(stream, 0, SEEK_SET) == 0); + Require(fwrite_nid_postfix(written.data(), 1, written.size(), stream) == written.size()); + expected = *mode == 'w' ? written : *mode == 'a' ? original + written : written + original.substr(written.size()); + } + Require(fclose_nid_postfix(stream) == 0); + correct &= CheckFileBytes(filename, expected, mode, reopen); + } + } + Require(std::filesystem::remove(filename)); + Require(std::filesystem::remove(directory)); + return correct; +} + int main() { Require(_Getmbcurmax_nid_postfix() == 1 && _Getmbcurmax_nid_postfix() == ___mb_cur_max_nid_postfix()); Require(fdopen_nid_postfix(-1, "rb") == nullptr && *__error_nid_postfix() == 9); @@ -168,15 +221,41 @@ int main() { FileStream scanned(std::tmpfile()); Require(fprintf_nid_postfix(&scanned, "%d %s", 42, "answer") == 9); std::rewind(scanned.GetHandle()); -#ifndef _WIN32 + Require(fscanf_nid_postfix(&scanned, "%*d") == 0); + Require(ftello_nid_postfix(&scanned) == 2); + std::rewind(scanned.GetHandle()); int scannedNumber = 0; char scannedWord[16]{}; + std::fputs("fscanf: suppressed conversion passed; assigning register arguments\n", stderr); + std::fflush(stderr); Require(fscanf_nid_postfix(&scanned, "%d %15s", &scannedNumber, scannedWord) == 2); Require(scannedNumber == 42 && std::strcmp(scannedWord, "answer") == 0); Require(fscanf_nid_postfix(&scanned, "%d", &scannedNumber) == EOF); -#endif + Require(feof_nid_postfix(&scanned) && (scanned.GuestState().flags & 0x20)); scanned.Close(); + FileStream scanMany(std::tmpfile()); + Require(std::fputs("7 1 2 3 4 5 6 7 8 4294967297 -4294967298 4294967299 abc %!", scanMany.GetHandle()) >= 0); + std::rewind(scanMany.GetHandle()); + int numbers[8]{}; + std::int64_t large = 0, negative = 0; + std::uint64_t sized = 0; + char letters[4]{}; + std::int64_t consumed = -1; + Require(fscanf_nid_postfix(&scanMany, "%*d %d %d %d %d %d %d %d %d %ld %jd %zu %3[a-z] %%%ln", + &numbers[0], &numbers[1], &numbers[2], &numbers[3], &numbers[4], &numbers[5], &numbers[6], &numbers[7], + &large, &negative, &sized, letters, &consumed) == 12); + for (int i = 0; i < 8; ++i) Require(numbers[i] == i + 1); + Require(large == INT64_C(4294967297) && negative == -INT64_C(4294967298) && sized == UINT64_C(4294967299)); + Require(std::strcmp(letters, "abc") == 0 && consumed == ftello_nid_postfix(&scanMany)); + Require(fgetc_nid_postfix(&scanMany) == '!'); + int unmatched = 123; + Require(fseeko_nid_postfix(&scanMany, -1, SEEK_CUR) == 0); + Require(fscanf_nid_postfix(&scanMany, "%d", &unmatched) == 0 && unmatched == 123); + Require(fgetc_nid_postfix(&scanMany) == '!'); + Require(fscanf_nid_postfix(&scanMany, "%d", &unmatched) == EOF && unmatched == 123); + scanMany.Close(); + FileStream positioned(std::tmpfile()); constexpr std::int64_t largeOffset = INT64_C(4294967313); Require(fseeko_nid_postfix(&positioned, largeOffset, SEEK_SET) == 0); @@ -214,4 +293,5 @@ int main() { Require(freopen_nid_postfix(filename.c_str(), "rb", &failed) == nullptr); Require(*__error_nid_postfix() == 2); Require(failed.GuestState().flags == 0 && failed.GuestState().descriptor == -1); + return CheckBinaryModes() ? 0 : 1; } diff --git a/core/libs/tests/GuestStdExecuteOnce.cpp b/core/libs/tests/GuestStdExecuteOnce.cpp new file mode 100644 index 000000000..d189ddcd7 --- /dev/null +++ b/core/libs/tests/GuestStdExecuteOnce.cpp @@ -0,0 +1,64 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +using Callback = int (APS5_VABI *)(void*, void*, void**); +extern "C" int APS5_VABI std_execute_once_nid_postfix(int*, Callback, void*); +static void Require(bool value) { if (!value) std::abort(); } + +static std::atomic calls{0}, arrived{0}; +static int control = 0, nested = 0, failing = 0, throwing = 0; +static int published = 0, nestedValue = 0, failures = 0, throws = 0; +static int APS5_VABI Nested(void*, void* arg, void**) { + nestedValue = *static_cast(arg); + return 1; +} +static int APS5_VABI Initialize(void* first, void* arg, void** third) { + Require(first == nullptr && third == nullptr && arg != nullptr); + ++calls; + while (arrived.load() != 16) std::this_thread::yield(); + int value = 17; + Require(std_execute_once_nid_postfix(&nested, Nested, &value) == 1); + published = 42; + return 5; +} +static int APS5_VABI Fail(void*, void*, void**) { + ++failures; + return 0; +} +static int APS5_VABI Throw(void*, void*, void**) { + if (++throws == 1) throw 7; + return 1; +} +static int APS5_VABI Unreachable(void*, void*, void**) { + std::abort(); +} + +int main() { + int token = 0; + std::array workers; + for (auto& worker : workers) worker = std::thread([&token] { + ++arrived; + Require(std_execute_once_nid_postfix(&control, Initialize, &token) == 1); + Require(published == 42 && nestedValue == 17); + }); + for (auto& worker : workers) worker.join(); + Require(calls == 1 && control == 1 && nested == 1); + Require(std_execute_once_nid_postfix(&control, Unreachable, nullptr) == 1 && calls == 1); + + Require(std_execute_once_nid_postfix(&failing, Fail, nullptr) == 0 && failing == 0 && failures == 1); + Require(std_execute_once_nid_postfix(&failing, Fail, nullptr) == 0 && failing == 0 && failures == 2); + + try { + std_execute_once_nid_postfix(&throwing, Throw, nullptr); + std::abort(); + } catch (int value) { + Require(value == 7 && throwing == 0); + } + Require(std_execute_once_nid_postfix(&throwing, Throw, nullptr) == 1 && throwing == 1 && throws == 2); + + int preset = -3; + Require(std_execute_once_nid_postfix(&preset, Unreachable, nullptr) == 1 && preset == -3); +} diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index 808f69407..145980fed 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -34,6 +34,33 @@ static void Require(bool condition) { } } +static bool CheckMemcpyOverlap() { + bool correct = true; + for (const auto offset : {0, 1, -1, 3, -3}) { + unsigned char bytes[16]; + std::memset(bytes, 0x5a, sizeof(bytes)); + auto* destination = bytes + 4; + const auto* source = destination + offset; + const int error = memcpy_s_nid_postfix(destination, 8, source, 4); + bool cleared = true; + for (unsigned i = 4; i < 12; ++i) cleared &= bytes[i] == 0; + const bool matches = error == 22 && cleared && bytes[3] == 0x5a && bytes[12] == 0x5a; + if (!matches) std::fprintf(stderr, "memcpy_s overlap %+d: expected EINVAL 22 and eight zero bytes, received %d\n", offset, error); + correct &= matches; + } + unsigned char adjacent[] = {1, 2, 3, 4, 5, 6, 7, 8}; + Require(memcpy_s_nid_postfix(adjacent, 4, adjacent + 4, 4) == 0); + Require(std::memcmp(adjacent, adjacent + 4, 4) == 0); + const unsigned char original[] = {1, 2, 3, 4, 5, 6, 7, 8}; + std::memcpy(adjacent, original, sizeof(adjacent)); + Require(memcpy_s_nid_postfix(adjacent + 4, 4, adjacent, 4) == 0); + const unsigned char unchanged[] = {1, 2, 3, 4, 1, 2, 3, 4}; + Require(std::memcmp(adjacent, unchanged, sizeof(adjacent)) == 0); + Require(memcpy_s_nid_postfix(adjacent, sizeof(adjacent), adjacent, 0) == 0); + Require(std::memcmp(adjacent, unchanged, sizeof(adjacent)) == 0); + return correct; +} + static void CheckBoundsCheckedFunctions() { char small[4] = "zz"; Require(strcpy_s_nid_postfix(small, sizeof(small), "abc") == 0 && std::strcmp(small, "abc") == 0); @@ -131,4 +158,5 @@ int main() { Require(strcasestr_nid_postfix("", "a") == nullptr); const char highBytes[] = {static_cast(0xff), 'A', 0}; Require(strcasestr_nid_postfix(highBytes, "a") == highBytes + 1); + return CheckMemcpyOverlap() ? 0 : 1; } diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index cd875318c..6341c8d40 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -25,6 +25,8 @@ bool ParamGetStringThrows(int paramId, char* buf, std::size_t bufSize) { extern "C" int APS5_VABI sceSystemServicePowerTick(void); extern "C" int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info); +extern "C" int APS5_VABI sceSystemServiceDisableMusicPlayer(void); +extern "C" int APS5_VABI sceSystemServiceReenableMusicPlayer(void); int main() { Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); @@ -32,6 +34,10 @@ int main() { Require(sceSystemServiceReportAbnormalTermination(nullptr) == SYSTEM_SERVICE_OK); int info = 0; Require(sceSystemServiceReportAbnormalTermination(&info) == SYSTEM_SERVICE_OK); + Require(sceSystemServiceDisableMusicPlayer() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceDisableMusicPlayer() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceReenableMusicPlayer() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceReenableMusicPlayer() == SYSTEM_SERVICE_OK); Require(sceSystemServiceGetHdrToneMapLuminance(nullptr) == SYSTEM_SERVICE_ERROR_PARAMETER); SystemServiceHdrToneMapLuminance luminance{-1.0f, -1.0f, -1.0f}; Require(sceSystemServiceGetHdrToneMapLuminance(&luminance) == SYSTEM_SERVICE_OK); diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 4290ec703..1358c5045 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -8,6 +8,10 @@ int APS5_VABI scePthreadJoin(Pthread thread, void** retval); int APS5_VABI scePthreadDetach(Pthread thread); void APS5_VABI scePthreadExit(void* retval); Pthread APS5_VABI scePthreadSelf(); +void APS5_VABI scePthreadTestcancel(); +void APS5_VABI pthread_testcancel_nid_postfix(void); +int APS5_VABI scePthreadSetcancelstate(int state, int* old_state); +int APS5_VABI scePthreadSetcanceltype(int type, int* old_type); int APS5_VABI scePthreadMutexattrInit(PthreadMutexattr* attr); int APS5_VABI scePthreadMutexattrDestroy(PthreadMutexattr* attr); int APS5_VABI scePthreadMutexattrSettype(PthreadMutexattr* attr, int type); @@ -38,6 +42,8 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; static constexpr int SCE_KERNEL_ERROR_EPERM = 0x80020001; static constexpr int MUTEX_TYPE_RECURSIVE = 2; +static constexpr int PTHREAD_CANCEL_ENABLE = 0; +static constexpr int PTHREAD_CANCEL_ASYNCHRONOUS = 2; static constexpr std::intptr_t WorkerRetval = 0x1234; static void Require(bool value) { if (!value) std::abort(); } @@ -48,6 +54,7 @@ struct WorkerContext { bool workerStackReported = false; PthreadMutex* mutex = nullptr; int unlockResult = 0; + bool testcancelReturned = false; }; static void* APS5_VABI Worker(void* arg) { @@ -56,6 +63,13 @@ static void* APS5_VABI Worker(void* arg) { int local = 0; context.workerStackReported = StackContains(context.selfFromWorker, &local); context.unlockResult = scePthreadMutexUnlock(context.mutex); + int oldState = -1; + int oldType = -1; + context.testcancelReturned = scePthreadSetcancelstate(PTHREAD_CANCEL_ENABLE, &oldState) == SCE_OK && + scePthreadSetcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, &oldType) == SCE_OK; + scePthreadTestcancel(); + pthread_testcancel_nid_postfix(); + context.testcancelReturned = context.testcancelReturned && oldState == PTHREAD_CANCEL_ENABLE; scePthreadExit(reinterpret_cast(WorkerRetval)); return nullptr; } @@ -92,6 +106,10 @@ int main() { Require(context.selfFromWorker != mainSelf); Require(context.unlockResult == SCE_KERNEL_ERROR_EPERM); Require(context.workerStackReported); + Require(context.testcancelReturned); + + scePthreadTestcancel(); + pthread_testcancel_nid_postfix(); Require(scePthreadMutexUnlock(&mutex) == SCE_OK); Require(scePthreadMutexDestroy(&mutex) == SCE_OK); diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp new file mode 100644 index 000000000..823c0536a --- /dev/null +++ b/core/libs/tests/GuestUserService.cpp @@ -0,0 +1,44 @@ +#include "SceTypes.hpp" +#include "prx/libSceUserService/UserService.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceUserServiceGetForegroundUser(int* user_id); +int APS5_VABI sceUserServiceGetRegisteredUserIdList(UserServiceRegisteredUserIdList* user_id_list); +int APS5_VABI sceUserServiceGetUserColor(int user_id, int* color); +int APS5_VABI sceUserServiceGetNpAccountId(int user_id, std::uint64_t* account_id); +} + +namespace { + +void Require(bool value) { if (!value) std::abort(); } + +} + +int main() { + Require(sceUserServiceGetForegroundUser(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + int foreground = -1; + Require(sceUserServiceGetForegroundUser(&foreground) == USER_SERVICE_OK); + Require(foreground == USER_SERVICE_INITIAL_USER_ID); + + Require(sceUserServiceGetRegisteredUserIdList(nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + UserServiceRegisteredUserIdList registered{}; + Require(sceUserServiceGetRegisteredUserIdList(®istered) == USER_SERVICE_OK); + Require(registered.user_id[0] == USER_SERVICE_INITIAL_USER_ID); + for (int i = 1; i < 16; ++i) { + Require(registered.user_id[i] == USER_SERVICE_USER_ID_INVALID); + } + + int color = -1; + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(123, &color) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetUserColor(USER_SERVICE_INITIAL_USER_ID, &color) == USER_SERVICE_OK); + Require(color == 0); + + std::uint64_t account_id = 0xdeadbeefu; + Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, nullptr) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetNpAccountId(123, &account_id) == USER_SERVICE_ERROR_INVALID_ARGUMENT); + Require(sceUserServiceGetNpAccountId(USER_SERVICE_INITIAL_USER_ID, &account_id) == USER_SERVICE_OK); + Require(account_id == 0); +} diff --git a/core/libs/tests/GuestVideodec2.cpp b/core/libs/tests/GuestVideodec2.cpp index 5a7e8227a..89e1d2328 100644 --- a/core/libs/tests/GuestVideodec2.cpp +++ b/core/libs/tests/GuestVideodec2.cpp @@ -83,6 +83,7 @@ int APS5_VABI sceVideodec2Decode_nid_postfix(std::uint64_t, const InputData*, Fr int APS5_VABI sceVideodec2Flush_nid_postfix(std::uint64_t, FrameBuffer*, OutputInfo*); int APS5_VABI sceVideodec2Reset_nid_postfix(std::uint64_t); int APS5_VABI sceVideodec2GetPictureInfo_nid_postfix(const OutputInfo*, AvcPictureInfo*, void*); +int APS5_VABI sceVideodec2GetAvcPictureInfo_nid_postfix(const OutputInfo*, AvcPictureInfo*, AvcPictureInfo*); } namespace { @@ -272,6 +273,11 @@ void testFailures() { input.auSize = 2; checkThrows([&] { sceVideodec2Decode_nid_postfix(handle, &input, &frame, &output); }); check(sceVideodec2Reset_nid_postfix(handle) == 0, "reset failed"); + AvcPictureInfo avc{sizeof(AvcPictureInfo)}; + checkThrows([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(nullptr, &avc, nullptr); }); + checkThrows([&] { sceVideodec2GetAvcPictureInfo_nid_postfix(&output, nullptr, nullptr); }); + output.frameBuffer = buffer.data(); + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && !avc.isValid, "unknown frame buffer reported a picture"); frame.frameBufferSize = 1; checkThrows([&] { for (const auto& unit : accessUnits(false)) { @@ -302,6 +308,9 @@ void testDecode(bool lengthPrefixed) { const auto unit = DisplayOrderUnits[pictures]; check(info.ptsData == 1000 + unit && info.dtsData == unit && info.attachedData == 0xa0 + unit, "picture timestamps out of display order"); check(info.idrPictureFlag == (pictures == 0 ? 1 : 0) && info.profileIdc == 100, "picture flags mismatch"); + AvcPictureInfo avc{sizeof(AvcPictureInfo)}; + check(sceVideodec2GetAvcPictureInfo_nid_postfix(&output, &avc, nullptr) == 0 && avc.isValid, "avc picture info missing"); + check(avc.ptsData == 1000 + unit && avc.dtsData == unit && avc.attachedData == 0xa0 + unit && avc.idrPictureFlag == (pictures == 0 ? 1 : 0) && avc.profileIdc == 100 && avc.picWidthInLumaSamples == Width && avc.picHeightInLumaSamples == Height, "avc picture info mismatch"); check(hashNv12(buffer.data(), output.framePitch) == PictureHashes[pictures], "picture " + std::to_string(pictures) + " differs from the reference"); ++pictures; return true; diff --git a/core/libs/tests/GuestWebBrowserDialog.cpp b/core/libs/tests/GuestWebBrowserDialog.cpp new file mode 100644 index 000000000..9e9f02fe0 --- /dev/null +++ b/core/libs/tests/GuestWebBrowserDialog.cpp @@ -0,0 +1,34 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include + +extern "C" { +int APS5_VABI sceWebBrowserDialogInitialize(void); +int APS5_VABI sceWebBrowserDialogTerminate(void); +int APS5_VABI sceWebBrowserDialogOpen(const void* param); +int APS5_VABI sceWebBrowserDialogGetStatus(void); +int APS5_VABI sceWebBrowserDialogUpdateStatus(void); +} + +namespace { + +constexpr int COMMON_DIALOG_STATUS_NONE = 0; +constexpr int COMMON_DIALOG_STATUS_INITIALIZED = 1; +constexpr int COMMON_DIALOG_STATUS_FINISHED = 3; + +void Require(bool value) { if (!value) std::abort(); } + +} + +int main() { + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogInitialize() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_INITIALIZED); + Require(sceWebBrowserDialogTerminate() == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_NONE); + Require(sceWebBrowserDialogInitialize() == 0); + std::uint8_t param[64] = {}; + Require(sceWebBrowserDialogOpen(param) == 0); + Require(sceWebBrowserDialogGetStatus() == COMMON_DIALOG_STATUS_FINISHED); + Require(sceWebBrowserDialogUpdateStatus() == COMMON_DIALOG_STATUS_FINISHED); +} diff --git a/core/libs/tests/GuestWideFormatting.cpp b/core/libs/tests/GuestWideFormatting.cpp index 343ef0e51..c758ba10b 100644 --- a/core/libs/tests/GuestWideFormatting.cpp +++ b/core/libs/tests/GuestWideFormatting.cpp @@ -88,8 +88,32 @@ static void CheckBounded() { Require(Format(buffer, 16, u"%s", nullNarrow) == 6 && buffer == std::u16string(u"(null)"), "vswprintf null %s argument"); } +static void CheckCount() { + char16_t buffer[16]; + int count = -7; + Require(Format(buffer, 16, u"ab%ncd", &count) == 4 && buffer == std::u16string(u"abcd") && count == 2, "vswprintf %n"); + count = -7; + Require(Format(buffer, 16, u"%s%n!", "\xf0\x9f\x98\x80", &count) == 3 && count == 2, "vswprintf %n counts UTF-16 units"); + count = -7; + Require(Format(buffer, 16, u"%n", &count) == 0 && buffer[0] == 0 && count == 0, "vswprintf %n at start"); + unsigned char bytes[2] = {0xaa, 0xaa}; + Require(Format(buffer, 16, u"%3d%hhn", 5, bytes) == 3 && bytes[0] == 3 && bytes[1] == 0xaa, "vswprintf %hhn"); + unsigned short halves[2] = {0xaaaa, 0xaaaa}; + Require(Format(buffer, 16, u"%5d%hn", 5, halves) == 5 && halves[0] == 5 && halves[1] == 0xaaaa, "vswprintf %hn"); + long long wide = -1; + Require(Format(buffer, 16, u"%4d%lln", 5, &wide) == 4 && wide == 4, "vswprintf %lln"); + wide = -1; + Require(Format(buffer, 16, u"%2d%ln", 5, &wide) == 2 && wide == 2, "vswprintf %ln"); + count = -7; + Require(Format(buffer, 16, u"a%5n", &count) < 0 && count == -7, "vswprintf %n with a width"); + Require(Format(buffer, 16, u"a%Ln", &count) < 0 && count == -7, "vswprintf %Ln"); + const int* const nullCount = nullptr; + Require(Format(buffer, 16, u"a%n", nullCount) < 0, "vswprintf null %n argument"); +} + int main() { CheckBounded(); + CheckCount(); Check(u"%.2s", "\xc3\xa9\xc3\xa8", u"\u00e9\u00e8"); Check(u"%.1s", "\xc3\xa9\xc3\xa8", u"\u00e9"); Check(u"%.0s", "\xc3\xa9", u""); diff --git a/core/libs/tests/PixelAncillary.cpp b/core/libs/tests/PixelAncillary.cpp new file mode 100644 index 000000000..2d6f10dad --- /dev/null +++ b/core/libs/tests/PixelAncillary.cpp @@ -0,0 +1,64 @@ +#include "IntermediateRepresentation/IrBuilder.hpp" +#include "Optimization/ConstantFolder.hpp" +#include "Optimization/DeadCodeEliminator.hpp" +#include "Optimization/ShaderInfoCollector.hpp" +#include +#include +#include + +using namespace ShaderRecompiler; +static void Require(bool value) { if (!value) throw std::runtime_error("packed pixel ancillary regression"); } +static IrValue& Build(IrProgram& program, IrOpcode opcode, std::uint32_t offset, std::uint32_t count) { + program.Resources().stage = IrShaderStage::Pixel; + program.Resources().resourceTrackingComplete = true; + auto& block = program.CreateBlock(); + program.SetEntryBlock(block); + program.BlockOrder().push_back(&block); + IrBuilder builder(program); + builder.SetInsertionPoint(block); + auto& ancillary = builder.Emit(IrOpcode::GetBuiltin, IrType::U32, {&builder.Constant(static_cast(StageInputKind::PackedAncillary)), &builder.Constant(0u)}); + auto& user = opcode == IrOpcode::BitwiseOr32 ? builder.Emit(opcode, IrType::U32, {&ancillary, &builder.Constant(offset)}) + : builder.Emit(opcode, IrType::U32, {&ancillary, &builder.Constant(offset), &builder.Constant(count)}); + static_cast(builder.Emit(IrOpcode::ReferenceU32, IrType::Void, {&user})); + static_cast(builder.Emit(IrOpcode::Return, IrType::Void, {})); + return user; +} +static void Lower(IrProgram& program) { + ConstantFolder().Fold(program); + DeadCodeEliminator().RemoveIdentities(program); + DeadCodeEliminator().Eliminate(program); + const ShaderPixelInputInfo pixel {}; + ShaderInfoCollector().Collect(program, ShaderStageInputInfo {nullptr, &pixel, nullptr}); +} +static void Extract(IrOpcode opcode, std::uint32_t offset, std::uint32_t count, StageInputKind kind, std::uint32_t fieldOffset) { + IrProgram program; + auto& user = Build(program, opcode, offset, count); + Lower(program); + const IrValue* field = user.Argument(0)->Resolve(); + Require(field->Opcode() == IrOpcode::GetBuiltin); + Require(static_cast(field->Argument(0)->Resolve()->ImmediateU32()) == kind); + Require(user.Argument(1)->Resolve()->ImmediateU32() == fieldOffset); + Require(user.Argument(2)->Resolve()->ImmediateU32() == count); +} +static void Refused(IrOpcode opcode, std::uint32_t offset, std::uint32_t count) { + IrProgram program; + static_cast(Build(program, opcode, offset, count)); + try { + Lower(program); + } catch (const std::runtime_error& error) { + Require(std::string(error.what()).find("unsupported live use") != std::string::npos); + return; + } + Require(false); +} +int main() { + Extract(IrOpcode::BitFieldUExtract, 8u, 4u, StageInputKind::SampleId, 0u); + Extract(IrOpcode::BitFieldUExtract, 9u, 2u, StageInputKind::SampleId, 1u); + Extract(IrOpcode::BitFieldUExtract, 16u, 13u, StageInputKind::Layer, 0u); + Extract(IrOpcode::BitFieldSExtract, 20u, 9u, StageInputKind::Layer, 4u); + Refused(IrOpcode::BitwiseOr32, 1u, 0u); + Refused(IrOpcode::BitFieldUExtract, 2u, 4u); + Refused(IrOpcode::BitFieldUExtract, 10u, 4u); + Refused(IrOpcode::BitFieldUExtract, 13u, 2u); + Refused(IrOpcode::BitFieldUExtract, 16u, 14u); +} diff --git a/core/libs/tests/PlayerInvitationDialog.cpp b/core/libs/tests/PlayerInvitationDialog.cpp index 34fa83ede..4cbd37064 100644 --- a/core/libs/tests/PlayerInvitationDialog.cpp +++ b/core/libs/tests/PlayerInvitationDialog.cpp @@ -1,6 +1,7 @@ #include "prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h" #include +#include namespace { @@ -14,6 +15,12 @@ void expect(std::int32_t actual, std::int32_t expected) { int main() { using enum PlayerInvitationDialogStatus; + constexpr auto notInitialized = static_cast(0x80B80003u); + constexpr auto notFinished = static_cast(0x80B80005u); + constexpr auto argNull = static_cast(0x80B8000Du); + constexpr std::int32_t userCanceled = 1; + ScePlayerInvitationDialogResult result{}; + expect(scePlayerInvitationDialogGetResult(&result), notInitialized); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(None)); expect(scePlayerInvitationDialogClose(), 0); expect(scePlayerInvitationDialogTerminate(), 0); @@ -22,17 +29,26 @@ int main() { expect(scePlayerInvitationDialogInitialize(), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(Initialized)); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Initialized)); + expect(scePlayerInvitationDialogGetResult(nullptr), argNull); + expect(scePlayerInvitationDialogGetResult(&result), notFinished); ScePlayerInvitationDialogParam param{}; expect(scePlayerInvitationDialogOpen(¶m), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(Running)); + expect(scePlayerInvitationDialogGetResult(&result), notFinished); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); + std::memset(&result, 0xFF, sizeof(result)); + expect(scePlayerInvitationDialogGetResult(&result), 0); + expect(result.errorCode, 0); + expect(result.result, userCanceled); + for (const std::uint8_t byte : result.reserved) expect(byte, 0); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); expect(scePlayerInvitationDialogOpen(nullptr), 0); expect(scePlayerInvitationDialogClose(), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(Finished)); expect(scePlayerInvitationDialogTerminate(), 0); expect(scePlayerInvitationDialogGetStatus(), static_cast(None)); + expect(scePlayerInvitationDialogGetResult(&result), notInitialized); expect(scePlayerInvitationDialogOpen(nullptr), 0); expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); diff --git a/core/libs/tests/SaveDataMemoryMetadata.cpp b/core/libs/tests/SaveDataMemoryMetadata.cpp index e3b960ee9..5c75de021 100644 --- a/core/libs/tests/SaveDataMemoryMetadata.cpp +++ b/core/libs/tests/SaveDataMemoryMetadata.cpp @@ -34,6 +34,8 @@ int main() { std::filesystem::create_directories(path.parent_path()); { std::ofstream file("_sd_mem/u7531/slot0.bin"); file << "save"; Require(static_cast(file)); } Require(sceSaveDataInitialize3(nullptr) == 0); + Require(sceSaveDataInitialize3(nullptr) == 0); + Require(sceSaveDataTerminate() == 0); SaveDataParam param{}; SaveDataMemorySet2 set{}; set.user_id = 7531; diff --git a/core/relinker/CMakeLists.txt b/core/relinker/CMakeLists.txt index 006738d94..1b3e069fe 100644 --- a/core/relinker/CMakeLists.txt +++ b/core/relinker/CMakeLists.txt @@ -192,6 +192,8 @@ if(BUILD_TESTING) if(Python3_Interpreter_FOUND) add_test(NAME elf_offsets COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_offsets.py $) + add_test(NAME dynamic_segment COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_dynamic_segment.py $) add_test(NAME optional_plt COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_optional_plt.py $) add_test(NAME empty_tls COMMAND ${Python3_EXECUTABLE} @@ -216,6 +218,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_intel_trampolines.py $) add_test(NAME guest_tls COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_tls.py $) + add_test(NAME guest_needed_modules COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_needed_modules.py $) add_test(NAME guest_module_directories COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_module_directories.py $) add_test(NAME guest_symbol_names COMMAND ${Python3_EXECUTABLE} @@ -224,6 +228,8 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_host_libc.py $) add_test(NAME guest_absolute_symbols COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_guest_absolute_symbols.py $) + add_test(NAME windows_import_modules COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_windows_import_modules.py $) add_test(NAME elf_header COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/test_elf_header.py $) add_test(NAME input_magic COMMAND ${Python3_EXECUTABLE} @@ -265,8 +271,16 @@ if(BUILD_TESTING) ) target_include_directories(strict_nid_filter_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/domain/include + ${CMAKE_CURRENT_SOURCE_DIR}/elfpatcher/include ${CMAKE_CURRENT_SOURCE_DIR}/codegen/include ${CMAKE_CURRENT_SOURCE_DIR}/relinker/include ) add_test(NAME strict_nid_filter COMMAND strict_nid_filter_tests) + + if(UNIX AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64") + add_test_executable(to_intel_linux_stubs_tests + ${CMAKE_CURRENT_SOURCE_DIR}/relinker/tests/ToIntelLinuxStubsTests.cpp + ) + add_test(NAME to_intel_linux_stubs COMMAND to_intel_linux_stubs_tests $) + endif() endif() diff --git a/core/relinker/cli/src/Autorun.cpp b/core/relinker/cli/src/Autorun.cpp index 3bb7016be..0660289e4 100644 --- a/core/relinker/cli/src/Autorun.cpp +++ b/core/relinker/cli/src/Autorun.cpp @@ -16,7 +16,16 @@ int Autorun(const std::string& absPath, bool toWindows) { std::filesystem::perm_options::add); } +#ifdef _WIN32 const std::string cmd = "\"" + absPath + "\""; +#else + std::string cmd = "'"; + for (const char character : absPath) { + if (character == '\'') cmd += "'\\''"; + else cmd += character; + } + cmd += '\''; +#endif const int rawCode = std::system(cmd.c_str()); int exitCode = rawCode; diff --git a/core/relinker/cli/tests/AutorunExitTests.cpp b/core/relinker/cli/tests/AutorunExitTests.cpp index f6fc70829..4890de71d 100644 --- a/core/relinker/cli/tests/AutorunExitTests.cpp +++ b/core/relinker/cli/tests/AutorunExitTests.cpp @@ -6,6 +6,7 @@ #include #include #include +#include int main(int, char* argv[]) { const auto executable = std::filesystem::absolute(argv[0]); @@ -14,7 +15,7 @@ int main(int, char* argv[]) { if (name.find("signal") != std::string::npos) std::raise(SIGILL); return 42; } - const auto child = std::filesystem::temp_directory_path() / ("anyps5-autorun-child-" + + auto child = std::filesystem::temp_directory_path() / ("anyps5-autorun-child-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + " with spaces" + executable.extension().string()); #ifndef _WIN32 const auto signaled = child.parent_path() / ("anyps5-autorun-child-signal-" + child.filename().string()); @@ -24,9 +25,21 @@ int main(int, char* argv[]) { int result = 0; try { std::filesystem::copy_file(executable, child); - for (const bool windows : {false, true}) { - const int code = Cli::Autorun(child.string(), windows); - if (code != 42) throw std::runtime_error("Autorun changed exit code 42 to " + std::to_string(code)); + std::vector suffixes{""}; +#ifndef _WIN32 + suffixes.insert(suffixes.end(), {" $HOME", " $(printf substituted)", " `printf substituted`", + " 'single'", " \"double\"", " \\backslash", " line\nbreak"}); +#endif + const auto originalChild = child; + for (const auto& suffix : suffixes) { + const auto renamed = std::filesystem::path(originalChild.string() + suffix); + if (renamed != child) std::filesystem::rename(child, renamed); + child = renamed; + for (const bool windows : {false, true}) { + const int code = Cli::Autorun(child.string(), windows); + if (code != 42) throw std::runtime_error("Autorun changed exit code 42 to " + + std::to_string(code) + " for " + child.string()); + } } #ifndef _WIN32 std::filesystem::rename(child, signaled); diff --git a/core/relinker/domain/include/domain/GuestRuntime.hpp b/core/relinker/domain/include/domain/GuestRuntime.hpp index a90225b54..2c45abb92 100644 --- a/core/relinker/domain/include/domain/GuestRuntime.hpp +++ b/core/relinker/domain/include/domain/GuestRuntime.hpp @@ -12,6 +12,7 @@ struct GuestImport { std::uint32_t TargetRva; std::uint64_t Addend; std::uint32_t RelocationType = 1; + std::string Library; }; struct GuestRuntime { @@ -22,6 +23,7 @@ struct GuestRuntime { std::uint32_t InitRva = 0; std::uint32_t FiniRva = 0; bool UsePlatformTlsResolver = true; + std::vector Names; }; } diff --git a/core/relinker/domain/include/domain/ImportModule.hpp b/core/relinker/domain/include/domain/ImportModule.hpp new file mode 100644 index 000000000..81b0c3b2b --- /dev/null +++ b/core/relinker/domain/include/domain/ImportModule.hpp @@ -0,0 +1,52 @@ +#ifndef DOMAIN_IMPORTMODULE_HPP +#define DOMAIN_IMPORTMODULE_HPP + +#include +#include +#include +#include + +namespace Domain { + +inline std::string ImportModuleName(std::string name) { + if (name.ends_with(".prx")) name.resize(name.size() - 4); + if (name.ends_with("-module")) name.resize(name.size() - 7); + std::replace(name.begin(), name.end(), '.', '_'); + return name; +} + +inline std::string ImportModule(const std::string& symbol, const std::map& modules, + std::span dependencies = {}) { + if (modules.empty()) return {}; + const auto first = symbol.find('#'); + if (first == std::string::npos) return {}; + const auto second = symbol.find('#', first + 1); + if (second == std::string::npos || second + 1 == symbol.size()) + throw RelinkerException("Invalid qualified import: " + symbol); + constexpr std::string_view alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-"; + std::uint64_t id = 0; + for (const char character : std::string_view(symbol).substr(second + 1)) { + const auto digit = alphabet.find(character); + if (digit == std::string_view::npos || id > (65535u - digit) / 64u) + throw RelinkerException("Invalid import module ID: " + symbol); + id = id * 64u + digit; + } + const auto module = modules.find(id); + if (module == modules.end()) throw RelinkerException("Unknown import module ID: " + symbol); + auto name = module->second; + if (name.empty() || name.find_first_of("/\\:$\r\n") != std::string::npos) + throw RelinkerException("Invalid import module name: " + name); + if (!name.ends_with(".prx")) name += ".prx"; + if (std::find(dependencies.begin(), dependencies.end(), name) != dependencies.end()) return name; + std::string matched; + for (const auto& dependency : dependencies) { + if (ImportModuleName(dependency) != ImportModuleName(name)) continue; + if (!matched.empty() && matched != dependency) throw RelinkerException("Ambiguous import module dependency: " + name); + matched = dependency; + } + return matched; +} + +} + +#endif diff --git a/core/relinker/domain/include/domain/Types.hpp b/core/relinker/domain/include/domain/Types.hpp index d9caa6a41..e49cc0347 100644 --- a/core/relinker/domain/include/domain/Types.hpp +++ b/core/relinker/domain/include/domain/Types.hpp @@ -95,6 +95,7 @@ struct SysVDynamicSection { std::vector RelaData; std::vector RelaPltData; std::vector GuestModules; + std::map ImportModules; }; struct CallRegistryEntry { diff --git a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp index c6f63f41d..c67ededd1 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/general/ElfConstants.hpp @@ -87,12 +87,59 @@ inline constexpr std::int64_t DT_STRSZ = 10; inline constexpr std::int64_t DT_SYMENT = 11; inline constexpr std::int64_t DT_PLTREL = 20; inline constexpr std::int64_t DT_JMPREL = 23; +inline constexpr std::int64_t DT_INIT = 12; +inline constexpr std::int64_t DT_FINI = 13; +inline constexpr std::int64_t DT_INIT_ARRAY = 25; +inline constexpr std::int64_t DT_FINI_ARRAY = 26; +inline constexpr std::int64_t DT_INIT_ARRAYSZ = 27; +inline constexpr std::int64_t DT_FINI_ARRAYSZ = 28; inline constexpr std::int64_t DT_RUNPATH = 29; inline constexpr std::int64_t DT_FLAGS = 30; inline constexpr std::uint64_t DF_BIND_NOW = 0x8; +inline constexpr std::int64_t DT_OS_INIT = 0x6000000c; +inline constexpr std::int64_t DT_OS_FINI = 0x6000000d; +inline constexpr std::int64_t DT_OS_INIT_ARRAY = 0x60000019; +inline constexpr std::int64_t DT_OS_FINI_ARRAY = 0x6000001a; +inline constexpr std::int64_t DT_OS_INIT_ARRAYSZ = 0x6000001b; +inline constexpr std::int64_t DT_OS_FINI_ARRAYSZ = 0x6000001c; +inline constexpr std::int64_t DT_OS_PLTGOT = 0x61000027; +inline constexpr std::int64_t DT_OS_JMPREL = 0x61000029; +inline constexpr std::int64_t DT_OS_PLTREL = 0x6100002b; +inline constexpr std::int64_t DT_OS_PLTRELSZ = 0x6100002d; +inline constexpr std::int64_t DT_OS_RELA = 0x6100002f; +inline constexpr std::int64_t DT_OS_RELASZ = 0x61000031; +inline constexpr std::int64_t DT_OS_RELAENT = 0x61000033; +inline constexpr std::int64_t DT_OS_STRTAB = 0x61000035; +inline constexpr std::int64_t DT_OS_STRSZ = 0x61000037; +inline constexpr std::int64_t DT_OS_SYMTAB = 0x61000039; +inline constexpr std::int64_t DT_OS_SYMENT = 0x6100003b; +inline constexpr std::int64_t DT_OS_SYMTABSZ = 0x6100003f; + +inline constexpr std::uint32_t R_X86_64_NONE = 0; +inline constexpr std::uint32_t R_X86_64_64 = 1; +inline constexpr std::uint32_t R_X86_64_PC32 = 2; +inline constexpr std::uint32_t R_X86_64_GOT32 = 3; +inline constexpr std::uint32_t R_X86_64_PLT32 = 4; +inline constexpr std::uint32_t R_X86_64_COPY = 5; +inline constexpr std::uint32_t R_X86_64_GLOB_DAT = 6; +inline constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7; +inline constexpr std::uint32_t R_X86_64_RELATIVE = 8; +inline constexpr std::uint32_t R_X86_64_GOTPCREL = 9; +inline constexpr std::uint32_t R_X86_64_32 = 10; +inline constexpr std::uint32_t R_X86_64_32S = 11; +inline constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; +inline constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; + +inline constexpr std::uint8_t STB_GLOBAL = 1; +inline constexpr std::uint8_t STB_WEAK = 2; +inline constexpr std::uint8_t STT_FUNC = 2; +inline constexpr std::uint8_t STV_DEFAULT = 0; +inline constexpr std::uint16_t SHN_UNDEF = 0; + inline constexpr std::uint32_t SHT_NULL = 0; inline constexpr std::uint32_t SHT_PROGBITS = 1; +inline constexpr std::uint32_t SHT_SYMTAB = 2; inline constexpr std::uint32_t SHT_STRTAB = 3; inline constexpr std::uint32_t SHT_DYNAMIC = 6; inline constexpr std::uint32_t SHT_DYNSYM = 11; diff --git a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp index bc1c0984d..bef348f4a 100644 --- a/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp +++ b/core/relinker/elfpatcher/include/elfpatcher/windows/WindowsPeFormat.hpp @@ -34,6 +34,7 @@ struct PeImport { std::uint64_t Addend; std::int32_t TargetModule = -1; std::uint32_t RelocationType = 1; + std::string Library; }; struct PeRelocations { diff --git a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp index 13b4caeb3..230d805a8 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsEntryStubBuilder.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace Elfpatcher::Windows { @@ -85,6 +86,35 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con for (const auto& import : imports) symbolNames.push_back(addString(import.Name)); + std::vector searchTables; + std::vector searchCounts; + std::vector platformTlsResolvers; + for (const auto& import : imports) { + std::vector order; + for (std::size_t index = 0; index < libraries.size(); ++index) { + auto name = std::filesystem::path(libraries[index]).filename().string(); + if (name.ends_with(".guest.prx")) name.resize(name.size() - 10); + const bool guestName = index < guestModules.size() && + std::find(guestModules[index].Names.begin(), guestModules[index].Names.end(), import.Library) != guestModules[index].Names.end(); + if (import.Library.empty() || name == import.Library || guestName) order.push_back(CheckedRva(index)); + } + if (order.empty()) throw Domain::RelinkerException("Import module is not a dependency: " + import.Library); + const bool hostModule = !import.Library.empty() && + std::any_of(order.begin(), order.end(), [&](auto index) { return index >= guestModules.size(); }); + if (hostModule) { + for (std::size_t index = guestModules.size(); index < libraries.size(); ++index) { + if (libraries[index] == "libc.prx" && import.Library != "libc.prx") order.push_back(CheckedRva(index)); + } + } + Io::AlignBuffer(data, 4); + platformTlsResolvers.push_back(std::none_of(order.begin(), order.end(), [&](auto index) { + return index < guestModules.size() && !guestModules[index].UsePlatformTlsResolver; + })); + searchTables.push_back(CheckedRva(dataRva + data.size())); + searchCounts.push_back(CheckedRva(order.size())); + for (const auto index : order) Io::AppendU32(data, index); + } + const auto lastError = reserve(4); const auto errorDigits = reserve(11); const auto errorMessage = reserve(ErrorMessageCapacity); @@ -105,7 +135,8 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con const auto diagnosticsOffset = data.size(); std::vector errors = {"FAIL: cannot obtain executable path\n", "FAIL: executable or library path is too long\n", "FAIL: executable path has no directory\n"}; for (const auto& import : imports) - errors.push_back("FAIL: unresolved ELF import " + import.Name + "\n"); + errors.push_back("FAIL: unresolved ELF import " + import.Name + + (import.Library.empty() ? std::string{} : " from " + import.Library) + "\n"); const auto argumentError = errors.size(); errors.push_back("FAIL: cannot prepare command-line arguments\n"); std::vector errorRvas; @@ -291,7 +322,7 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con std::vector lazyUnresolvedImports; std::vector tlsResolverAddresses; for (std::size_t index = 0; index < imports.size(); ++index) { - if (!guestModules.empty() && guestModules.front().UsePlatformTlsResolver && imports[index].Name == "vNe1w4diLCs") { + if (!guestModules.empty() && platformTlsResolvers[index] && imports[index].Name == "vNe1w4diLCs") { if (imports[index].Addend != 0) throw Domain::RelinkerException("TLS resolver import has an addend"); tlsResolverAddresses.push_back(code.Branch({0x48, 0x8d, 0x05})); guestStartup.WriteImport(code, imports[index], handles); @@ -299,14 +330,15 @@ WindowsEntryStub WindowsEntryStubBuilder::Build(const std::uint32_t dataRva, con } code.Rip({0x48, 0x8d, 0x1d}, handles); code.Rip({0x48, 0x8d, 0x35}, symbolNames[index]); + code.Rip({0x48, 0x8d, 0x3d}, searchTables[index]); code.Emit({0xbd}); - code.U32(CheckedRva(libraries.size())); + code.U32(searchCounts[index]); const auto search = code.GetRva(); - code.Emit({0x48, 0x8b, 0x0b, 0x48, 0x89, 0xf2}); + code.Emit({0x8b, 0x0f, 0x48, 0x8b, 0x0c, 0xcb, 0x48, 0x89, 0xf2}); call("GetProcAddress"); code.Emit({0x48, 0x85, 0xc0}); const auto resolved = code.Branch({0x0f, 0x85}); - code.Emit({0x48, 0x83, 0xc3, 8, 0xff, 0xcd}); + code.Emit({0x48, 0x83, 0xc7, 4, 0xff, 0xcd}); code.Rip({0x0f, 0x85}, search); if (lazyBinding) { lazyUnresolvedImports.push_back(index); diff --git a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp index 5cdb3ed73..2dc27774d 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsGuestModuleWriter.cpp @@ -47,13 +47,13 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI if (addend != 0 || (symbolIndex != 0 && (symbol.Info & 15) != 6)) throw Domain::RelinkerException("Invalid guest TLS module relocation", target); if (symbolIndex != 0 && symbol.Section == 0) { image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, 0, 16}); + runtime.Imports.push_back({symbol.Name, rva, 0, 16, symbol.Library}); } else tlsModules.emplace_back(target, rva); } else if (type == 17) { if (symbolIndex != 0 && (symbol.Info & 15) != 6) throw Domain::RelinkerException("Invalid guest TLS offset relocation", target); if (symbolIndex != 0 && symbol.Section == 0) { image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, addend, 17}); + runtime.Imports.push_back({symbol.Name, rva, addend, 17, symbol.Library}); } else image.WritePointer(target, symbol.Value + addend); } else if (type == 18) { const auto tls = std::find_if(guest.Headers.begin(), guest.Headers.end(), [](const auto& header) { return header.Type == 7; }); @@ -70,7 +70,7 @@ std::vector GuestModuleWriter::WriteWindows(const Relinker::GuestI } else { if (symbol.Name.empty()) throw Domain::RelinkerException("Empty guest import", target); image.WritePointer(target, 0); - runtime.Imports.push_back({symbol.Name, rva, addend}); + runtime.Imports.push_back({symbol.Name, rva, addend, 1, symbol.Library}); } } else throw Domain::RelinkerException("Unsupported Windows guest relocation " + std::to_string(type), target); } diff --git a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp index 1b9370ceb..322fbd46c 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsPePatcher.cpp @@ -83,7 +83,7 @@ std::vector WindowsPePatcher::Patch(const std::vector(index), import.RelocationType}); + for (const auto& import : module.Imports) relocations.Imports.push_back({import.Name, import.TargetRva, import.Addend, static_cast(index), import.RelocationType, import.Library}); } libraries.insert(libraries.begin(), guestPaths.begin(), guestPaths.end()); if (dependencyDiagnostics) diff --git a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp index a2d33a623..b1f71e3f3 100644 --- a/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp +++ b/core/relinker/elfpatcher/src/windows/WindowsRelocationBuilder.cpp @@ -45,6 +45,8 @@ PeRelocations WindowsRelocationBuilder::Apply(WindowsLoadImage& image, const Dom image.RequireWritable(target, 8); image.WritePointer(target, 0); result.Imports.push_back({name, rva, addend}); + if (const auto module = dynamicSection.ImportModules.find(target); module != dynamicSection.ImportModules.end()) + result.Imports.back().Library = module->second; } }; applyTable(dynamicSection.RelaData, false); diff --git a/core/relinker/main.cpp b/core/relinker/main.cpp index 2e3b6553e..1b4db2e0d 100644 --- a/core/relinker/main.cpp +++ b/core/relinker/main.cpp @@ -135,7 +135,7 @@ int main(const int argc, char* argv[]) { << std::filesystem::path(absPath).filename().string() << "\n" << "libs/\n *.prx\napp0/\n \n"; for (const auto& artifact : guestArtifacts) - std::cout << " " << artifact.Path.parent_path().filename().string() << "/" << artifact.Path.filename().string() << '\n'; + std::cout << " " << artifact.Path.lexically_relative(std::filesystem::path(absPath).parent_path() / "app0").generic_string() << '\n'; std::cout << "Game resources and system libraries must be placed in this layout separately.\n"; if (args.runPath != "$ORIGIN/libs") std::cout << "Custom library search path (--rpath): " << args.runPath << '\n'; diff --git a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp index 572234514..fe34092b5 100644 --- a/core/relinker/relinker/include/relinker/guest/GuestImage.hpp +++ b/core/relinker/relinker/include/relinker/guest/GuestImage.hpp @@ -21,6 +21,7 @@ struct GuestSymbol { std::uint16_t Section; std::uint64_t Value; std::uint64_t Size; + std::string Library; }; struct GuestImage { diff --git a/core/relinker/relinker/include/relinker/output/SysVDynamicSectionBuilder.hpp b/core/relinker/relinker/include/relinker/output/SysVDynamicSectionBuilder.hpp index 7816d126c..c75274640 100644 --- a/core/relinker/relinker/include/relinker/output/SysVDynamicSectionBuilder.hpp +++ b/core/relinker/relinker/include/relinker/output/SysVDynamicSectionBuilder.hpp @@ -15,25 +15,6 @@ public: ) override; private: - static constexpr std::uint32_t STB_GLOBAL = 1; - static constexpr std::uint32_t STT_FUNC = 2; - static constexpr std::uint32_t STV_DEFAULT = 0; - static constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7; - static constexpr std::uint32_t R_X86_64_GLOB_DAT = 6; - - static constexpr std::int64_t DT_NEEDED = 1; - static constexpr std::int64_t DT_SYMTAB = 6; - static constexpr std::int64_t DT_STRTAB = 5; - static constexpr std::int64_t DT_STRSZ = 10; - static constexpr std::int64_t DT_RELA = 7; - static constexpr std::int64_t DT_RELASZ = 8; - static constexpr std::int64_t DT_RELAENT = 9; - static constexpr std::int64_t DT_JMPREL = 23; - static constexpr std::int64_t DT_PLTRELSZ = 2; - static constexpr std::int64_t DT_PLTREL = 20; - static constexpr std::int64_t DT_SYMENT = 11; - static constexpr std::int64_t DT_NULL = 0; - void _appendU64(std::vector& buf, std::uint64_t v) const; void _appendI64(std::vector& buf, std::int64_t v) const; void _appendDynEntry(std::vector& buf, std::int64_t tag, std::uint64_t val) const; diff --git a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp index 67be9ac4b..78554168b 100644 --- a/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp +++ b/core/relinker/relinker/include/relinker/pipeline/RelinkerPipeline.hpp @@ -27,36 +27,6 @@ private: std::shared_ptr _unusedNidFilter; std::uint32_t unusedFilterLevel; - static constexpr std::uint32_t PT_LOAD = 1; - static constexpr std::uint32_t PT_DYNAMIC = 2; - static constexpr std::uint32_t PF_X = 0x1; - - static constexpr std::int64_t DT_NEEDED = 0x00000001; - static constexpr std::int64_t DT_PLTRELSZ = 0x00000002; - static constexpr std::int64_t DT_PLTGOT = 0x00000003; - static constexpr std::int64_t DT_STRTAB = 0x00000005; - static constexpr std::int64_t DT_SYMTAB = 0x00000006; - static constexpr std::int64_t DT_RELA = 0x00000007; - static constexpr std::int64_t DT_RELASZ = 0x00000008; - static constexpr std::int64_t DT_RELAENT = 0x00000009; - static constexpr std::int64_t DT_STRSZ = 0x0000000a; - static constexpr std::int64_t DT_SYMENT = 0x0000000b; - static constexpr std::int64_t DT_PLTREL = 0x00000014; - static constexpr std::int64_t DT_JMPREL = 0x00000017; - - static constexpr std::int64_t DT_OS_PLTGOT = 0x61000027; - static constexpr std::int64_t DT_OS_PLTREL = 0x6100002b; - static constexpr std::int64_t DT_OS_PLTRELSZ = 0x6100002d; - static constexpr std::int64_t DT_OS_STRTAB = 0x61000035; - static constexpr std::int64_t DT_OS_STRSZ = 0x61000037; - static constexpr std::int64_t DT_OS_SYMTAB = 0x61000039; - static constexpr std::int64_t DT_OS_SYMENT = 0x6100003b; - static constexpr std::int64_t DT_OS_SYMTABSZ = 0x6100003f; - static constexpr std::int64_t DT_OS_RELA = 0x6100002f; - static constexpr std::int64_t DT_OS_RELASZ = 0x61000031; - static constexpr std::int64_t DT_OS_RELAENT = 0x61000033; - static constexpr std::int64_t DT_OS_JMPREL = 0x61000029; - static std::string _relocationTypeName(std::uint32_t type); }; diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp index c2225509f..d24bcaf3d 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/EntryPointCollector.cpp @@ -1,8 +1,11 @@ #include +#include #include namespace Relinker::UnusedNidFilter { +using namespace Elfpatcher; + namespace { std::uint16_t read16(const std::vector& b, std::size_t off) { @@ -65,7 +68,6 @@ public: std::uint32_t type = read32(elfBytes, phPos); - constexpr std::uint32_t PT_DYNAMIC = 2; if (type != PT_DYNAMIC) continue; std::uint64_t segOff = read64(elfBytes, phPos + 8); @@ -73,20 +75,6 @@ public: if (segOff + segSz > elfBytes.size()) throw RelinkerException("PT_DYNAMIC segment out of bounds"); - constexpr std::int64_t DT_NULL = 0; - constexpr std::int64_t DT_INIT = 12; - constexpr std::int64_t DT_FINI = 13; - constexpr std::int64_t DT_INIT_ARRAY = 25; - constexpr std::int64_t DT_INIT_ARRAYSZ = 27; - constexpr std::int64_t DT_FINI_ARRAY = 26; - constexpr std::int64_t DT_FINI_ARRAYSZ = 28; - constexpr std::int64_t DT_OS_INIT_ARRAY = 0x60000019; - constexpr std::int64_t DT_OS_INIT_ARRAYSZ = 0x6000001b; - constexpr std::int64_t DT_OS_FINI_ARRAY = 0x6000001a; - constexpr std::int64_t DT_OS_FINI_ARRAYSZ = 0x6000001c; - constexpr std::int64_t DT_OS_INIT = 0x6000000c; - constexpr std::int64_t DT_OS_FINI = 0x6000000d; - VirtualAddress initArrayVa = 0; std::uint64_t initArraySz = 0; VirtualAddress finiArrayVa = 0; @@ -142,8 +130,6 @@ public: if (shPos + 64 > elfBytes.size()) break; std::uint32_t shType = read32(elfBytes, shPos + 4); - constexpr std::uint32_t SHT_DYNSYM = 11; - constexpr std::uint32_t SHT_SYMTAB = 2; if (shType != SHT_DYNSYM && shType != SHT_SYMTAB) continue; std::uint64_t symOff = read64(elfBytes, shPos + 24); @@ -172,10 +158,6 @@ public: std::uint8_t stType = info & 0xF; std::uint16_t shndx = read16(elfBytes, sPos + 6); std::uint64_t symVal = read64(elfBytes, sPos + 8); - constexpr std::uint8_t STB_GLOBAL = 1; - constexpr std::uint8_t STB_WEAK = 2; - constexpr std::uint8_t STT_FUNC = 2; - constexpr std::uint16_t SHN_UNDEF = 0; if ((stBind == STB_GLOBAL || stBind == STB_WEAK) && stType == STT_FUNC && shndx != SHN_UNDEF) { addIfInText(symVal); diff --git a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp index 9af486615..fd508b987 100644 --- a/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp +++ b/core/relinker/relinker/src/analysis/UnusedNidFilter/RelativeRelocationIndex.cpp @@ -1,9 +1,12 @@ #include +#include #include #include namespace Relinker::UnusedNidFilter { +using namespace Elfpatcher; + namespace { std::uint16_t read16(const std::vector& b, std::size_t off) { @@ -38,7 +41,6 @@ std::uint64_t vaddrToFileOffset(const std::vector& loads, VirtualAd throw RelinkerException("Cannot translate virtual address to file offset", va); } -constexpr std::uint32_t R_X86_64_RELATIVE = 8; constexpr std::size_t RelaEntSize = 24; void collectRelativeEntries( @@ -105,7 +107,6 @@ std::unique_ptr BuildRelativeRelocationIndex( std::size_t phPos = static_cast(phOff) + i * phEntSize; if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds"); std::uint32_t type = read32(elfBytes, phPos); - constexpr std::uint32_t PT_LOAD = 1; if (type != PT_LOAD) continue; LoadSegment seg; seg.fileOffset = read64(elfBytes, phPos + 8); @@ -121,23 +122,12 @@ std::unique_ptr BuildRelativeRelocationIndex( if (phPos + 56 > elfBytes.size()) throw RelinkerException("Program header out of bounds"); std::uint32_t type = read32(elfBytes, phPos); - constexpr std::uint32_t PT_DYNAMIC = 2; if (type != PT_DYNAMIC) continue; std::uint64_t segOff = read64(elfBytes, phPos + 8); std::uint64_t segSz = read64(elfBytes, phPos + 32); if (segOff + segSz > elfBytes.size()) throw RelinkerException("PT_DYNAMIC segment out of bounds"); - constexpr std::int64_t DT_NULL = 0; - constexpr std::int64_t DT_RELA = 7; - constexpr std::int64_t DT_RELASZ = 8; - constexpr std::int64_t DT_JMPREL = 0x17; - constexpr std::int64_t DT_PLTRELSZ = 2; - constexpr std::int64_t DT_OS_RELA = 0x6100002f; - constexpr std::int64_t DT_OS_RELASZ = 0x61000031; - constexpr std::int64_t DT_OS_JMPREL = 0x61000029; - constexpr std::int64_t DT_OS_PLTRELSZ = 0x6100002d; - VirtualAddress relaVa = 0; std::uint64_t relaSz = 0; VirtualAddress jmprelVa = 0; diff --git a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp index 4a0a6a7cf..2f2f4f91b 100644 --- a/core/relinker/relinker/src/analysis/ValidationPolicy.cpp +++ b/core/relinker/relinker/src/analysis/ValidationPolicy.cpp @@ -1,8 +1,11 @@ #include "relinker/analysis//ValidationPolicy.hpp" +#include #include namespace Relinker { +using namespace Elfpatcher; + static constexpr std::uint8_t SYSCALL_BYTE0 = 0x0F; static constexpr std::uint8_t SYSCALL_BYTE1 = 0x05; static constexpr std::uint8_t INT80_BYTE0 = 0xCD; @@ -12,21 +15,6 @@ static constexpr std::uint8_t SYSENTER_BYTE1 = 0x34; static constexpr std::uint8_t SYSRET_BYTE0 = 0x0F; static constexpr std::uint8_t SYSRET_BYTE1 = 0x07; -static constexpr std::uint32_t R_X86_64_NONE = 0; -static constexpr std::uint32_t R_X86_64_64 = 1; -static constexpr std::uint32_t R_X86_64_PC32 = 2; -static constexpr std::uint32_t R_X86_64_GOT32 = 3; -static constexpr std::uint32_t R_X86_64_PLT32 = 4; -static constexpr std::uint32_t R_X86_64_COPY = 5; -static constexpr std::uint32_t R_X86_64_GLOB_DAT = 6; -static constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7; -static constexpr std::uint32_t R_X86_64_RELATIVE = 8; -static constexpr std::uint32_t R_X86_64_GOTPCREL = 9; -static constexpr std::uint32_t R_X86_64_32 = 10; -static constexpr std::uint32_t R_X86_64_32S = 11; -static constexpr std::uint32_t R_X86_64_GOTPCRELX = 41; -static constexpr std::uint32_t R_X86_64_REX_GOTPCRELX = 42; - void ValidationPolicy::_initializeSupportedRelocationTypes() { _supportedRelocationTypes = { R_X86_64_NONE, diff --git a/core/relinker/relinker/src/guest/GuestImageReader.cpp b/core/relinker/relinker/src/guest/GuestImageReader.cpp index 837273006..56551a48d 100644 --- a/core/relinker/relinker/src/guest/GuestImageReader.cpp +++ b/core/relinker/relinker/src/guest/GuestImageReader.cpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace Relinker { @@ -61,12 +62,14 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (loads.empty() || dynamic == nullptr || dynamic->FileSize % 16 != 0) fail("Missing or invalid ELF load/dynamic segments"); std::map tags; std::vector needed; + std::vector moduleImports; bool terminated = false; for (std::uint64_t offset = dynamic->Offset; offset < dynamic->Offset + dynamic->FileSize; offset += 16) { const auto tag = Io::ReadU64(bytes, offset); const auto value = Io::ReadU64(bytes, offset + 8); if (tag == 0) { terminated = true; break; } if (tag == 1) needed.push_back(value); + else if (tag == 0x61000045) moduleImports.push_back(value); else if (tag < 0x60000000 || tag == 0x6100003f || (tag >= 0x61000027 && tag <= 0x6100003b)) { if (!tags.emplace(tag, value).second) fail("Duplicate dynamic tag " + std::to_string(tag)); } @@ -118,9 +121,15 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector const auto symOffset = table(6, 0x61000039, symSize); image.Dynamic.DynSymData.assign(bytes.begin() + symOffset, bytes.begin() + symOffset + symSize); image.Dynamic.DynStrData.push_back(0); + std::map importModules; + for (const auto value : moduleImports) + if (!importModules.emplace(value >> 48, string(value & 0xffffffffu)).second) fail("Duplicate import module ID"); + std::vector neededLibraries; + for (const auto offset : needed) neededLibraries.push_back(string(offset)); std::set exports; for (std::uint64_t offset = 0; offset < symSize; offset += 24) { auto name = string(Io::ReadU32(bytes, symOffset + offset)); + const auto library = Io::ReadU16(bytes, symOffset + offset + 6) == 0 ? Domain::ImportModule(name, importModules, neededLibraries) : std::string{}; name = name.substr(0, name.find('#')); const auto info = bytes[symOffset + offset + 4]; const auto visibility = bytes[symOffset + offset + 5]; @@ -144,7 +153,7 @@ GuestImage GuestImageReader::Read(const std::filesystem::path& path, std::vector if (image.Dynamic.DynStrData.size() > std::numeric_limits::max()) fail("String table too large"); Io::WriteU32(image.Dynamic.DynSymData, offset, static_cast(image.Dynamic.DynStrData.size())); Io::AppendString(image.Dynamic.DynStrData, name); - image.Symbols.push_back({name, info, visibility, section, value, size}); + image.Symbols.push_back({name, info, visibility, section, value, size, library}); } if (tags.contains(14)) { image.Soname = string(tags.at(14)); diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index fe15abdd9..99368fb49 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -27,6 +27,14 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (hasPrx) directories.push_back(prx); std::vector paths; std::set unmatchedExclusions = excludedModules; + const auto isElf = [](const std::filesystem::path& path) { + std::ifstream stream(path, std::ios::binary); + if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + path.string()); + char magic[4]{}; + stream.read(magic, 4); + if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + path.string()); + return stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F'; + }; for (const auto& directory : directories) { if (!std::filesystem::is_directory(directory)) throw Domain::RelinkerException("Guest module path is not a directory: " + directory.string()); for (const auto& entry : std::filesystem::directory_iterator(directory)) { @@ -36,20 +44,41 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path continue; } if (!entry.is_regular_file()) continue; - std::ifstream stream(entry.path(), std::ios::binary); - if (!stream) throw Domain::RelinkerException("Cannot read guest candidate: " + entry.path().string()); - char magic[4]{}; - stream.read(magic, 4); - if (stream.bad()) throw Domain::RelinkerException("Cannot read guest candidate magic: " + entry.path().string()); - if (stream.gcount() == 4 && static_cast(magic[0]) == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') paths.push_back(entry.path()); + if (isElf(entry.path())) paths.push_back(entry.path()); } } + if (dynamic.DynamicSegmentData.size() % 16 != 0) throw Domain::RelinkerException("Invalid executable dependency table"); + std::set missingNeeded; + for (std::size_t offset = 0; offset < dynamic.DynamicSegmentData.size(); offset += 16) { + if (Io::ReadU64(dynamic.DynamicSegmentData, offset) != 1) continue; + const auto nameOffset = Io::ReadU64(dynamic.DynamicSegmentData, offset + 8); + if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid dependency string offset"); + const auto start = dynamic.DynStrData.begin() + static_cast(nameOffset); + const auto end = std::find(start, dynamic.DynStrData.end(), 0); + if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated dependency string"); + const std::string name(start, end); + if (excludedModules.contains(name)) continue; + if (std::none_of(paths.begin(), paths.end(), [&](const auto& path) { return path.filename().string() == name; })) missingNeeded.insert(name); + } + if (!missingNeeded.empty()) { + std::map found; + for (auto it = std::filesystem::recursive_directory_iterator(root); it != std::filesystem::recursive_directory_iterator(); ++it) { + if (it->is_directory() && std::find(directories.begin(), directories.end(), it->path()) != directories.end()) { + it.disable_recursion_pending(); + continue; + } + const auto name = it->path().filename().string(); + if (!it->is_regular_file() || !missingNeeded.contains(name) || !isElf(it->path())) continue; + if (!found.emplace(name, it->path()).second) throw Domain::RelinkerException("Ambiguous needed module: " + found.at(name).string() + " and " + it->path().string()); + } + for (const auto& [name, path] : found) paths.push_back(path); + } if (!unmatchedExclusions.empty()) throw Domain::RelinkerException("Excluded guest module file not found: " + *unmatchedExclusions.begin()); std::sort(paths.begin(), paths.end()); if (paths.empty()) return {}; if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); std::vector images; - std::map exports; + std::map> exports; std::map> sharedExports; std::set outputNames; Io::FileReader reader; @@ -66,12 +95,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!outputNames.insert(folded).second) throw Domain::RelinkerException("Conflicting guest output filename: " + image.OutputName); for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || symbol.Section == AbsoluteSection || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; - const auto [existing, inserted] = exports.emplace(symbol.Name, images.size()); - if (inserted) continue; - if (windows || existing->second == images.size()) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + images.at(existing->second).SourcePath.string() + " and " + path.string()); - auto& providers = sharedExports[symbol.Name]; - providers.insert(existing->second); - providers.insert(images.size()); + auto& providers = exports[symbol.Name]; + const bool repeated = std::find(providers.begin(), providers.end(), images.size()) != providers.end(); + if (!windows && repeated) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + path.string() + " and " + path.string()); + if (!repeated) providers.push_back(images.size()); + if (!windows && providers.size() > 1) sharedExports[symbol.Name].insert(providers.begin(), providers.end()); } std::vector codeHeaders; for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); @@ -119,7 +147,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!windows && exports.contains(name)) rename(dynamic.DynSymData, dynamic.DynStrData, offset / 24, name); } std::vector> dependencies(images.size()); - for (auto& image : images) image.UsePlatformTlsResolver = !exports.contains("vNe1w4diLCs"); + for (auto& image : images) { + image.UsePlatformTlsResolver = !windows ? !exports.contains("vNe1w4diLCs") : std::none_of(image.Symbols.begin(), image.Symbols.end(), [](const auto& symbol) { + return symbol.Name == "vNe1w4diLCs" && symbol.Section != 0 && symbol.Section != AbsoluteSection && (symbol.Info >> 4) != 0 && symbol.Visibility != 1 && symbol.Visibility != 2; + }); + } for (std::size_t index = 0; index < images.size(); ++index) { for (const auto& name : images[index].Dependencies) { const auto found = guestNames.find(name); @@ -129,11 +161,19 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (symbol.Section != 0 || symbol.Name.empty()) continue; rejectSharedImport(symbol.Name, images[index].SourcePath.string()); const auto found = exports.find(symbol.Name); + std::vector providers; if (found != exports.end()) { - const auto& provider = images[found->second]; + for (const auto provider : found->second) { + const auto& candidate = images[provider]; + if (!windows || symbol.Library.empty() || symbol.Library == candidate.SourcePath.filename().string() || symbol.Library == candidate.Soname) providers.push_back(provider); + } + } + if (providers.size() > 1) throw Domain::RelinkerException("Ambiguous guest import after stripping #: " + symbol.Name); + if (!providers.empty()) { + const auto& provider = images[providers.front()]; const auto exported = std::find_if(provider.Symbols.begin(), provider.Symbols.end(), [&](const auto& candidate) { return candidate.Section != 0 && candidate.Section != AbsoluteSection && candidate.Name == symbol.Name && (candidate.Info >> 4) != 0 && candidate.Visibility != 1 && candidate.Visibility != 2; }); if (exported == provider.Symbols.end() || ((symbol.Info & 15) != 0 && (symbol.Info & 15) != (exported->Info & 15))) throw Domain::RelinkerException("Guest import/export type mismatch: " + symbol.Name); - if (found->second != index) dependencies[index].insert(found->second); + if (providers.front() != index) dependencies[index].insert(providers.front()); } else if (windows && (symbol.Info & 15) == 6) throw Domain::RelinkerException("Windows guest TLS import requires a guest TLS export: " + symbol.Name); } } @@ -182,7 +222,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Io::AppendU64(dynamic.DynamicSegmentData, dynamic.DynStrData.size()); Io::AppendString(dynamic.DynStrData, name); }; - for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().filename().generic_string() + "/" + images[index].OutputName); + for (const auto index : order) if (!windows) addNeeded("$ORIGIN/app0/" + images[index].SourcePath.parent_path().lexically_relative(root).generic_string() + "/" + images[index].OutputName); for (const auto& name : hostLibraries) addNeeded(name); std::string guestRunPath = runPath; if (!windows) { @@ -193,7 +233,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path std::vector artifacts; for (const auto index : order) { const auto& image = images[index]; - const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().filename().generic_string(); + const auto relativeDirectory = "app0/" + image.SourcePath.parent_path().lexically_relative(root).generic_string(); const auto destination = std::filesystem::absolute(outputPath).parent_path() / relativeDirectory; const auto target = destination / image.OutputName; if (target.lexically_normal() == std::filesystem::absolute(outputPath).lexically_normal()) throw Domain::RelinkerException("Guest output collides with the executable output"); @@ -202,6 +242,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path Domain::GuestRuntime runtime; runtime.UsePlatformTlsResolver = image.UsePlatformTlsResolver; runtime.Path = relativeDirectory + "/" + image.OutputName; + runtime.Names = {image.SourcePath.filename().string(), image.Soname}; std::vector output; if (windows) output = Elfpatcher::GuestModuleWriter().WriteWindows(image, runtime); else { diff --git a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp index 3e29923b9..d3ad5fee4 100644 --- a/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp +++ b/core/relinker/relinker/src/output/SysVDynamicSectionBuilder.cpp @@ -1,8 +1,11 @@ #include +#include #include namespace Relinker { +using namespace Elfpatcher; + void SysVDynamicSectionBuilder::_appendU64(std::vector& buf, std::uint64_t v) const { std::size_t pos = buf.size(); buf.resize(pos + 8); @@ -85,6 +88,7 @@ SysVDynamicSection SysVDynamicSectionBuilder::BuildDynamicSection( std::vector nonPltRefs; for (const auto& ref : nidReferences) { + if (!ref.Library.empty()) result.ImportModules.emplace(ref.RelocationAddress, ref.Library); std::uint32_t relType = ref.RelocationTypeValue; if (relType == 0) relType = R_X86_64_JUMP_SLOT; diff --git a/core/relinker/relinker/src/parsing/ElfReader.cpp b/core/relinker/relinker/src/parsing/ElfReader.cpp index ba8992b17..8401513b1 100644 --- a/core/relinker/relinker/src/parsing/ElfReader.cpp +++ b/core/relinker/relinker/src/parsing/ElfReader.cpp @@ -1,9 +1,12 @@ #include +#include #include #include namespace Relinker { +using namespace Elfpatcher; + namespace { bool _rangeFits(const std::uint64_t offset, const std::uint64_t length, const std::size_t size) { @@ -170,24 +173,31 @@ std::string ElfReader::_resolveShdrName(std::uint32_t nameOffset, const ElfHeade } std::vector ElfReader::ReadDynamicTags(const ProgramHeader& dynamicHeader) const { + if (!_rangeFits(dynamicHeader.Offset, dynamicHeader.FileSize, _fileBuffer.size())) { + throw RelinkerException("Dynamic segment out of bounds", dynamicHeader.Offset); + } + if (dynamicHeader.FileSize % 16 != 0) { + throw RelinkerException("Invalid dynamic segment size", dynamicHeader.Offset); + } + std::vector tags; FileByteOffset offset = dynamicHeader.Offset; const FileByteOffset end = dynamicHeader.Offset + dynamicHeader.FileSize; - while (offset + 16 <= end && offset + 16 <= _fileBuffer.size()) { + while (offset < end) { DynamicTag tag; tag.Tag = static_cast(_readU64At(offset)); tag.Value = _readU64At(offset + 0x08); if (tag.Tag == 0) { - break; + return tags; } tags.push_back(tag); offset += 16; } - return tags; + throw RelinkerException("Unterminated dynamic segment", dynamicHeader.Offset); } FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const { @@ -201,8 +211,6 @@ FileByteOffset ElfReader::TranslateVirtualAddress(VirtualAddress address) const const VirtualAddress segVAddr = _readU64At(offset + 0x10); const ByteCount segFileSize = _readU64At(offset + 0x20); - static constexpr std::uint32_t PT_LOAD = 1; - if (type == PT_LOAD && address >= segVAddr && address < segVAddr + segFileSize) { return segOffset + (address - segVAddr); } diff --git a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp index 2438ce6b3..1d1407d05 100644 --- a/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp +++ b/core/relinker/relinker/src/pipeline/RelinkerPipeline.cpp @@ -1,12 +1,16 @@ #include +#include #include #include #include #include #include +#include namespace Relinker { +using namespace Elfpatcher; + RelinkerPipeline::RelinkerPipeline(std::shared_ptr elfReader, std::shared_ptr syscallScanner, std::shared_ptr callSiteResolver, std::shared_ptr validationPolicy, std::shared_ptr dynamicSectionBuilder, std::shared_ptr unusedNidFilter, std::uint32_t unusedFilterLevel) : _elfReader(std::move(elfReader)) , _syscallScanner(std::move(syscallScanner)) @@ -175,6 +179,12 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf if (policy) policy->RegisterLibraryImport(snd); } + std::map importModules; + for (const auto& tag : dynTags) { + if (tag.Tag == 0x61000045 && !importModules.emplace(tag.Value >> 48, readCStr(tag.Value & 0xffffffffu)).second) + throw RelinkerException("Duplicate import module ID"); + } + auto extractRela = [&](const FileByteOffset relaOff, const ByteCount relaSize) { for (ByteCount off = 0; off + relaEntSize <= relaSize; off += relaEntSize) { const FileByteOffset pos = relaOff + off; @@ -203,7 +213,8 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf std::uint32_t nameOff = 0; std::memcpy(&nameOff, raw.data() + symOff, 4); - nidRefs.push_back({readCStr(nameOff), {}, relType, pos, rOffset, rAddend}); + const auto name = readCStr(nameOff); + nidRefs.push_back({name, Domain::ImportModule(name, importModules, neededLibraries), relType, pos, rOffset, rAddend}); } }; @@ -218,8 +229,6 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf _validationPolicy->ValidateSyscallAbsence(); - static constexpr std::uint32_t R_X86_64_JUMP_SLOT = 7; - const std::size_t originalNidCount = nidRefs.size(); const auto originalNidRefs = nidRefs; std::cout << "NID input: " << originalNidCount << " references\n"; @@ -273,8 +282,6 @@ RelinkResult RelinkerPipeline::Relink(const std::vector& sourceElf } auto dynSection = _dynamicSectionBuilder->BuildDynamicSection(dynamicRefs, neededLibraries, dynJmpRelOffset, pltCount); - static constexpr std::uint32_t R_X86_64_RELATIVE = 8; - auto appendRela = [&](std::vector& buf, std::uint64_t offset, std::uint64_t info, std::int64_t addend) { std::size_t pos = buf.size(); buf.resize(pos + 24); diff --git a/core/relinker/relinker/tests/ElfFixture.hpp b/core/relinker/relinker/tests/ElfFixture.hpp new file mode 100644 index 000000000..9962b9d85 --- /dev/null +++ b/core/relinker/relinker/tests/ElfFixture.hpp @@ -0,0 +1,129 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace RelinkerTests { + +using Bytes = std::vector; + +inline constexpr std::uint64_t kCodeOffset = 0x4000; +inline constexpr std::uint64_t kCodeSize = 0x1000; +inline constexpr std::uint64_t kDynamicVaddr = 0x600; +inline constexpr std::uint16_t kSpareProgramHeaders = 3; + +template +void Write(Bytes& bytes, const std::size_t offset, const TValue value) { + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("ELF fixture write is out of bounds"); + std::memcpy(bytes.data() + offset, &value, sizeof(value)); +} + +template +TValue Read(const Bytes& bytes, const std::size_t offset) { + TValue value; + if (offset > bytes.size() || sizeof(value) > bytes.size() - offset) throw std::runtime_error("ELF read is out of bounds"); + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +inline void WriteProgramHeader(Bytes& image, const std::uint16_t index, const std::uint32_t type, const std::uint32_t flags, const std::uint64_t offset, const std::uint64_t vaddr, const std::uint64_t size, const std::uint64_t align) { + const std::size_t header = 64 + index * 56; + Write(image, header, type); + Write(image, header + 4, flags); + Write(image, header + 8, offset); + Write(image, header + 16, vaddr); + Write(image, header + 24, vaddr); + Write(image, header + 32, size); + Write(image, header + 40, size); + Write(image, header + 48, align); +} + +inline Bytes MakeExecutable(const std::span code) { + if (code.size() > kDynamicVaddr) throw std::runtime_error("ELF fixture code overlaps the dynamic section"); + Bytes image(0x8000); + const std::uint8_t ident[16] = {0x7F, 'E', 'L', 'F', 2, 1, 1}; + std::memcpy(image.data(), ident, sizeof(ident)); + Write(image, 16, 3); + Write(image, 18, 62); + Write(image, 20, 1); + Write(image, 24, 0); + Write(image, 32, 64); + Write(image, 52, 64); + Write(image, 54, 56); + Write(image, 56, 2 + kSpareProgramHeaders); + Write(image, 58, 64); + const std::pair tags[] = {{5, 0x600}, {10, 1}, {6, 0x620}, {11, 24}, {7, 0x700}, {8, 0}, {9, 24}, {0, 0}}; + WriteProgramHeader(image, 0, 1, 5, kCodeOffset, 0, kCodeSize, 0x4000); + WriteProgramHeader(image, 1, 2, 6, kCodeOffset + kDynamicVaddr, kDynamicVaddr, sizeof(tags) / sizeof(tags[0]) * 16, 8); + for (std::uint16_t index = 2; index < 2 + kSpareProgramHeaders; ++index) WriteProgramHeader(image, index, 0x6FFFFF01, 0, 0, 0, 0, 1); + std::fill(image.begin() + kCodeOffset, image.begin() + kCodeOffset + kDynamicVaddr, 0xCC); + std::copy(code.begin(), code.end(), image.begin() + kCodeOffset); + for (std::size_t index = 0; index < sizeof(tags) / sizeof(tags[0]); ++index) { + Write(image, kCodeOffset + kDynamicVaddr + index * 16, tags[index].first); + Write(image, kCodeOffset + kDynamicVaddr + index * 16 + 8, tags[index].second); + } + return image; +} + +class MappedImage { +public: + explicit MappedImage(const Bytes& elf) { + const auto phOffset = Read(elf, 32); + const auto phSize = Read(elf, 54); + const auto phCount = Read(elf, 56); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + size = std::max(size, Read(elf, header + 16) + Read(elf, header + 40)); + } + if (size == 0) throw std::runtime_error("ELF has no PT_LOAD segment"); + base = mmap(nullptr, size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (base == MAP_FAILED) throw std::runtime_error("Cannot map executable memory for the ELF image"); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + const auto offset = Read(elf, header + 8); + const auto fileSize = Read(elf, header + 32); + if (offset > elf.size() || fileSize > elf.size() - offset) throw std::runtime_error("PT_LOAD exceeds the ELF file"); + std::memcpy(static_cast(base) + Read(elf, header + 16), elf.data() + offset, fileSize); + } + if (mprotect(base, size, PROT_NONE) != 0) throw std::runtime_error("Cannot protect the ELF image"); + const auto page = static_cast(sysconf(_SC_PAGESIZE)); + for (std::uint16_t index = 0; index < phCount; ++index) { + const auto header = phOffset + index * phSize; + if (Read(elf, header) != 1) continue; + const auto flags = Read(elf, header + 4); + const auto vaddr = Read(elf, header + 16); + const auto start = vaddr / page * page; + const auto end = (vaddr + Read(elf, header + 40) + page - 1) / page * page; + const int protection = ((flags & 4) != 0 ? PROT_READ : 0) | ((flags & 2) != 0 ? PROT_WRITE : 0) | ((flags & 1) != 0 ? PROT_EXEC : 0); + if (mprotect(static_cast(base) + start, std::min(end, size) - start, protection) != 0) throw std::runtime_error("Cannot apply PT_LOAD permissions"); + } + } + + MappedImage(const MappedImage&) = delete; + MappedImage& operator=(const MappedImage&) = delete; + + ~MappedImage() { + munmap(base, size); + } + + template + TFunction* At(const std::uint64_t vaddr) const { + if (vaddr >= size) throw std::runtime_error("Address is outside the mapped ELF image"); + return reinterpret_cast(static_cast(base) + vaddr); + } + +private: + void* base = nullptr; + std::size_t size = 0; +}; + +} // namespace RelinkerTests diff --git a/core/relinker/relinker/tests/RelinkerProcess.hpp b/core/relinker/relinker/tests/RelinkerProcess.hpp new file mode 100644 index 000000000..b642783fe --- /dev/null +++ b/core/relinker/relinker/tests/RelinkerProcess.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char** environ; + +namespace RelinkerTests { + +class TempDirectory { +public: + TempDirectory() { + auto pattern = (std::filesystem::temp_directory_path() / "anyps5-relinker-XXXXXX").string(); + if (mkdtemp(pattern.data()) == nullptr) throw std::runtime_error("Cannot create a temporary directory"); + path = pattern; + } + + TempDirectory(const TempDirectory&) = delete; + TempDirectory& operator=(const TempDirectory&) = delete; + + ~TempDirectory() { + std::error_code error; + std::filesystem::remove_all(path, error); + } + + const std::filesystem::path& Path() const { + return path; + } + +private: + std::filesystem::path path; +}; + +struct RelinkerRun { + int ExitCode = -1; + std::string Output; +}; + +inline RelinkerRun RunRelinker(const std::filesystem::path& relinker, const std::vector& arguments, const std::filesystem::path& logPath) { + std::vector owned{relinker.string()}; + owned.insert(owned.end(), arguments.begin(), arguments.end()); + std::vector argv; + for (auto& argument : owned) argv.push_back(argument.data()); + argv.push_back(nullptr); + posix_spawn_file_actions_t actions; + posix_spawn_file_actions_init(&actions); + posix_spawn_file_actions_addopen(&actions, STDOUT_FILENO, logPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + posix_spawn_file_actions_adddup2(&actions, STDOUT_FILENO, STDERR_FILENO); + pid_t pid = 0; + const auto spawned = posix_spawn(&pid, argv[0], &actions, nullptr, argv.data(), environ); + posix_spawn_file_actions_destroy(&actions); + if (spawned != 0) throw std::runtime_error("Cannot start the relinker"); + int status = 0; + if (waitpid(pid, &status, 0) != pid) throw std::runtime_error("Cannot wait for the relinker"); + RelinkerRun run; + if (WIFEXITED(status)) run.ExitCode = WEXITSTATUS(status); + std::ifstream log(logPath, std::ios::binary); + run.Output.assign(std::istreambuf_iterator(log), std::istreambuf_iterator()); + return run; +} + +inline std::vector ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) throw std::runtime_error("Cannot read " + path.string()); + return {std::istreambuf_iterator(file), std::istreambuf_iterator()}; +} + +inline void WriteFile(const std::filesystem::path& path, const std::vector& bytes) { + std::ofstream file(path, std::ios::binary); + file.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + if (!file) throw std::runtime_error("Cannot write " + path.string()); +} + +} // namespace RelinkerTests diff --git a/core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp b/core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp new file mode 100644 index 000000000..04a57131f --- /dev/null +++ b/core/relinker/relinker/tests/ToIntelLinuxStubsTests.cpp @@ -0,0 +1,64 @@ +#include "ElfFixture.hpp" +#include "RelinkerProcess.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace RelinkerTests; + +void require(const bool condition, const std::string& message) { + if (!condition) throw std::runtime_error(message); +} + +const Bytes kCode = { + 0x66, 0x48, 0x0F, 0x6E, 0xCF, + 0x66, 0x48, 0x0F, 0x6E, 0xD6, + 0x66, 0x0F, 0x78, 0xC1, 0x08, 0x08, + 0xF2, 0x0F, 0x78, 0xD1, 0x08, 0x10, + 0x66, 0x48, 0x0F, 0x7E, 0xD0, + 0xC3, +}; +constexpr std::uint64_t kSites[] = {10, 16}; +constexpr std::size_t kSiteLength = 6; + +std::uint64_t expected(const std::uint64_t value, const std::uint64_t destination) { + return (destination & ~0xFF0000ull) | (((value >> 8) & 0xFF) << 16); +} + +} + +int main(const int argc, char** argv) { + try { + require(argc == 2, "usage: to_intel_linux_stubs_tests "); + const TempDirectory directory; + const auto input = directory.Path() / "input.elf"; + const auto output = directory.Path() / "output.elf"; + WriteFile(input, MakeExecutable(kCode)); + const auto run = RunRelinker(argv[1], {"--skip-sce-module", "--to-intel", input.string(), output.string()}, directory.Path() / "relinker.log"); + require(run.ExitCode == 0, "Relinker failed:\n" + run.Output); + require(run.Output.find("Intel conversion: 0 in place, 2 stubs") != std::string::npos, "Relinker did not report two stubs:\n" + run.Output); + + const MappedImage image(ReadFile(output)); + for (const auto site : kSites) { + const auto* bytes = image.At(site); + require(bytes[0] == 0xE9, "SSE4a site was not replaced by a jump"); + for (std::size_t index = 5; index < kSiteLength; ++index) require(bytes[index] == 0x90, "SSE4a site tail is not padded with NOPs"); + std::int32_t displacement; + std::memcpy(&displacement, bytes + 1, sizeof(displacement)); + require(site + 5 + displacement >= kCodeSize, "SSE4a stub is inside the original code segment"); + } + + const auto function = image.At(0); + for (const auto [value, destination] : {std::pair{0x1122334455667788ull, 0x0123456789ABCDEFull}, {0ull, ~0ull}, {~0ull, 0ull}, {0x9E3779B97F4A7C15ull, 0x0F1E2D3C4B5A6978ull}}) + require(function(value, destination) == expected(value, destination), "Relinked SSE4a stubs computed the wrong value"); + std::cout << "--to-intel Linux stub tests passed\n"; + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} diff --git a/core/relinker/relinker/tests/test_dynamic_segment.py b/core/relinker/relinker/tests/test_dynamic_segment.py new file mode 100644 index 000000000..830b96707 --- /dev/null +++ b/core/relinker/relinker/tests/test_dynamic_segment.py @@ -0,0 +1,81 @@ +from pathlib import Path +import struct +import subprocess +import sys +import tempfile + +from test_optional_plt import fixture as optional_plt_fixture + + +def fixture(): + image = optional_plt_fixture() + struct.pack_into("= 8u) { + for (std::uint8_t& value : info.targetExportMapping) { + value = reader.ReadU8(); + } + } else { + info.targetExportMapping.fill(0u); + } if (version < 5u) { const auto input = [](PixelInput value, bool present) { return present ? PixelInputBit(value) : 0u; }; info.inputAddr = input(PixelInput::PerspectiveSample, info.hasPerspectiveCenterVgpr && inputAddrOrCenterVgpr == 2u) | input(PixelInput::PerspectiveCenter, info.hasPerspectiveCenterVgpr) | @@ -687,7 +697,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(7u); + writer.WriteU32(9u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -703,6 +713,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const } } writer.WriteBool(request.target.nonConstantImageOffsets); + writer.WriteU32(request.target.srgbDecodeFormats); return base64Encode(buffer); } @@ -711,7 +722,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const Reader reader(decoded); if (reader.ReadU32() != 0x41505335u) throw std::runtime_error("invalid recompile request signature"); const auto version = reader.ReadU32(); - if (version < 1u || version > 7u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 9u) throw std::runtime_error("unsupported recompile request serialization version"); DeserializedRequest result{}; result.request.shader = readShaderBinary(reader, result.shaderCode, result.shaderHeader); result.request.context = readGuestContext(reader, result, version); @@ -728,6 +739,7 @@ DeserializedRequest RequestSerializer::Deserialize(std::string_view text) const } } if (version >= 6u) result.request.target.nonConstantImageOffsets = reader.ReadBool(); + if (version >= 9u) result.request.target.srgbDecodeFormats = reader.ReadU32(); return result; } diff --git a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp index 6f6eb09be..f23bd9258 100644 --- a/core/shader/recompiler/ControlFlow/src/Structurizer.cpp +++ b/core/shader/recompiler/ControlFlow/src/Structurizer.cpp @@ -464,6 +464,19 @@ std::uint32_t findSelectionMerge(const ControlFlowGraph& graph, const BasicBlock if (isLoopControlGateway(graph, *loop, falseTarget) && graph.Dominates(block.id, falseTarget) && isInsideLoopConstruct(graph, *loop, trueTarget)) { return falseTarget; } + const auto joinsAt = [&](std::uint32_t join, std::uint32_t other) { + const auto& arm = graph.FindBlock(other); + const bool armLeavesOrJoins = arm.terminator.kind == TerminatorKind::ConditionalBranch && arm.predecessors.size() == 1u && std::all_of(arm.successors.begin(), arm.successors.end(), [&](std::uint32_t successor) { + return successor == join || successor == loop->continueBlock || successor == loop->mergeBlock; + }); + return armLeavesOrJoins && isInsideLoopConstruct(graph, *loop, other) && isInsideLoopConstruct(graph, *loop, join) && graph.Dominates(block.id, join); + }; + if (joinsAt(trueTarget, falseTarget)) { + return trueTarget; + } + if (joinsAt(falseTarget, trueTarget)) { + return falseTarget; + } if (globalMerge != InvalidControlFlowId) { const bool trueJoins = reachesWithinIteration(graph, *loop, trueTarget, globalMerge); const bool falseJoins = reachesWithinIteration(graph, *loop, falseTarget, globalMerge); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp index 7ea1c1823..bd2c34d3b 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/DescriptorBinding.hpp @@ -11,7 +11,7 @@ namespace ShaderRecompiler { inline constexpr std::uint32_t FirstImageBinding = 1u; inline constexpr std::uint32_t FirstComparisonImageBinding = 22u; inline constexpr std::uint32_t FirstStorageImageBinding = 29u; -inline constexpr std::uint32_t ImageBindingCount = 43u; +inline constexpr std::uint32_t ImageBindingCount = 48u; enum class DescriptorBindingKind : std::uint32_t { Buffers = 0u, diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp index 4c23a8014..a59dbab67 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp @@ -55,6 +55,7 @@ struct IrResourcePlan { std::uint64_t shaderHash = 0; std::uint32_t userDataBase = 0; std::uint32_t userDataCount = 64; + std::uint32_t srgbDecodeFormats = 0; std::vector> valueStorage; std::vector> blockStorage; std::vector memoryInfo; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp index 13c13fcf2..a25930f11 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILIER_INTERMEDIATEREPRESENTATION_INCLUDE_INTERMEDIATEREPRESENTATION_IRMETADATA_RESOURCES_HPP #include "IntermediateRepresentation/IrMetadata/BufferFormat.hpp" +#include "IntermediateRepresentation/IrMetadata/ShaderStage.hpp" #include "IntermediateRepresentation/IrOpcode.hpp" #include "RdnaDecoder/RdnaInstruction.hpp" #include @@ -68,9 +69,11 @@ struct ImageResource { bool read = false; bool written = false; bool atomic = false; + bool atomic64 = false; bool depthCompare = false; bool cube = false; bool r128 = false; + bool srgbDecode = false; bool depthBits = false; bool depthUnorm16 = false; bool packed = false; @@ -84,11 +87,27 @@ struct ImageResource { bool operator==(const ImageResource& other) const = default; }; +enum SamplerUse : std::uint8_t { + SamplerUseExplicitLod = 1u << 0u, + SamplerUseImplicitLod = 1u << 1u, + SamplerUseGradient = 1u << 2u, + SamplerUseOffset = 1u << 3u, + SamplerUseCompare = 1u << 4u, + SamplerUseGather = 1u << 5u, + SamplerUseQueryLod = 1u << 6u, + SamplerUseAdjust = 1u << 7u, +}; + +inline bool ImageSampleExplicitLod(std::uint32_t flags, IrShaderStage stage) { + return (flags & (RdnaImageSampleFlagDerivative | RdnaImageSampleFlagLod | RdnaImageSampleFlagLevelZero)) != 0u || stage != IrShaderStage::Pixel; +} + struct SamplerResource { std::uint32_t source = 0; std::uint32_t firstUsePc = 0; bool forcePointFiltering = false; bool depthCompare = false; + std::uint8_t uses = 0; bool operator==(const SamplerResource& other) const = default; }; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp index 23fe56c39..aa6ee6b96 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ShaderInfo.hpp @@ -12,7 +12,6 @@ namespace ShaderRecompiler { struct ShaderInfo { std::uint32_t scratchDwords = 0; std::uint32_t sharedMemoryBytes = 0; - static constexpr std::uint32_t MaxBuffers = 32; static constexpr std::uint32_t MaxImages = 64; static constexpr std::uint32_t MaxSamplers = 32; static constexpr std::uint32_t MaxSampledPairs = 64; diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index b0ced9a19..484c34519 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -444,6 +444,17 @@ enum class IrOpcode : std::uint16_t { ImageAtomicFCmpSwap32, ImageAtomicFMin32, ImageAtomicFMax32, + ImageAtomicSwap64, + ImageAtomicIAdd64, + ImageAtomicISub64, + ImageAtomicUMin64, + ImageAtomicUMax64, + ImageAtomicSMin64, + ImageAtomicSMax64, + ImageAtomicAnd64, + ImageAtomicOr64, + ImageAtomicXor64, + ImageAtomicCmpSwap64, GetAttribute, GetInterpolationParameter, SetAttribute, @@ -512,6 +523,7 @@ struct ExportFlags { [[nodiscard]] std::uint32_t SharedComponentCount(IrOpcode opcode); [[nodiscard]] AddressOpcodeInfo AddressOpcodeInfoOf(IrOpcode opcode); [[nodiscard]] ImageOpcodeInfo ImageOpcodeInfoOf(IrOpcode opcode); +bool IsImageAtomic64Opcode(IrOpcode opcode); [[nodiscard]] std::string_view IrOpcodeName(IrOpcode opcode); } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index bede0e6e5..5919c08e6 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -485,6 +485,17 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("ImageAtomicFCmpSwap32", U32, ImageResource, ImageAddress, U32, U32, U1), makeMeta("ImageAtomicFMin32", U32, ImageResource, ImageAddress, U32, U1), makeMeta("ImageAtomicFMax32", U32, ImageResource, ImageAddress, U32, U1), + makeMeta("ImageAtomicSwap64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicIAdd64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicISub64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicUMin64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicUMax64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicSMin64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicSMax64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicAnd64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicOr64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicXor64", U64, ImageResource, ImageAddress, U64, U1), + makeMeta("ImageAtomicCmpSwap64", U64, ImageResource, ImageAddress, U64, U64, U1), makeMeta("GetAttribute", U32, U32, U32), makeMeta("GetInterpolationParameter", U32, U32, U32, U32), makeMeta("SetAttribute", Void, U32x4, U1), @@ -804,12 +815,27 @@ ImageOpcodeInfo ImageOpcodeInfoOf(IrOpcode opcode) { case IrOpcode::ImageAtomicFCmpSwap32: case IrOpcode::ImageAtomicFMin32: case IrOpcode::ImageAtomicFMax32: + case IrOpcode::ImageAtomicSwap64: + case IrOpcode::ImageAtomicIAdd64: + case IrOpcode::ImageAtomicISub64: + case IrOpcode::ImageAtomicUMin64: + case IrOpcode::ImageAtomicUMax64: + case IrOpcode::ImageAtomicSMin64: + case IrOpcode::ImageAtomicSMax64: + case IrOpcode::ImageAtomicAnd64: + case IrOpcode::ImageAtomicOr64: + case IrOpcode::ImageAtomicXor64: + case IrOpcode::ImageAtomicCmpSwap64: return {ImageAccess::Atomic, ImageResourceClass::Storage, false}; default: return {}; } } +bool IsImageAtomic64Opcode(IrOpcode opcode) { + return opcode >= IrOpcode::ImageAtomicSwap64 && opcode <= IrOpcode::ImageAtomicCmpSwap64; +} + bool IsFloat64Opcode(IrOpcode opcode) { return opcode >= IrOpcode::FPAdd64 && opcode <= IrOpcode::ConvertU32F64; } diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp index 7a0d829a6..15aed9410 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrProgram.cpp @@ -852,6 +852,7 @@ DescriptorBindingKind DescriptorBindingForImage(const ImageResource& image) { constexpr std::uint32_t storageFloatBinding = FirstStorageImageBinding; constexpr std::uint32_t storageUintBinding = storageFloatBinding + 5u; constexpr std::uint32_t atomicUintBinding = storageUintBinding + 5u; + constexpr std::uint32_t atomic64UintBinding = atomicUintBinding + 5u; std::uint32_t base = 0u; bool sampled = false; @@ -882,7 +883,7 @@ DescriptorBindingKind DescriptorBindingForImage(const ImageResource& image) { if (image.numericClass != IrTextureNumericClass::Uint) { fail("DescriptorBindingForImage atomic image must be uint"); } - base = atomicUintBinding; + base = image.atomic64 ? atomic64UintBinding : atomicUintBinding; } else { switch (image.numericClass) { case IrTextureNumericClass::Float: diff --git a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp index bac6a047a..cae8055d8 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/DescriptorBindingBuilder.hpp @@ -4,9 +4,12 @@ #include "IntermediateRepresentation/IrProgram.hpp" #include "Optimization/BindingAllocator.hpp" #include "Recompiler.hpp" +#include namespace ShaderRecompiler { +std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter); + class DescriptorBindingBuilder { public: void Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const; diff --git a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp index 92ba14de9..aed4b1d83 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp @@ -33,6 +33,7 @@ struct ResourceSpecialization { bool depthUnorm16 = false; IrBufferFormat packedFormat = IrBufferFormat::Invalid; std::uint32_t emulatedCompare = 0; + bool srgbDecode = false; bool operator==(const Image& other) const; }; diff --git a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp index 8f87d21de..a05a6c7a8 100644 --- a/core/shader/recompiler/Optimization/src/ConstantFolder.cpp +++ b/core/shader/recompiler/Optimization/src/ConstantFolder.cpp @@ -2,8 +2,11 @@ #include "IntermediateRepresentation/IrBuilder.hpp" #include +#include #include #include +#include +#include namespace ShaderRecompiler { namespace { @@ -181,12 +184,68 @@ bool foldCompositeExtract(IrBuilder& builder, IrValue& inst, IrOpcode construct, return false; } +void collectUses(IrValue& value, std::vector& uses) { + for (const IrUse& use : value.OperandUses()) { + if (use.user->Opcode() == IrOpcode::Identity) { + collectUses(*use.user, uses); + } else { + uses.push_back(use); + } + } +} + +bool lowerPackedAncillary(IrProgram& program, IrBuilder& builder, IrValue& ancillary) { + auto& kind = resolveArg(ancillary, 0); + if (!isImmediate(kind, IrType::U32) || static_cast(kind.ImmediateU32()) != StageInputKind::PackedAncillary) { + return false; + } + std::vector uses; + collectUses(ancillary, uses); + std::array fields {}; + const auto field = [&](std::size_t index) -> IrValue& { + if (fields[index] == nullptr) { + const auto builtin = index == 0u ? StageInputKind::SampleId : StageInputKind::Layer; + IrValue& created = program.CreateValue(IrOpcode::GetBuiltin, IrType::U32); + created.AddArgument(&builder.Constant(static_cast(builtin))); + created.AddArgument(&builder.Constant(0u)); + ancillary.Parent()->InsertInstructionBefore(&ancillary, &created); + fields[index] = &created; + } + return *fields[index]; + }; + constexpr std::array, 2> ranges {{{8u, 4u}, {16u, 13u}}}; + bool lowered = false; + for (const IrUse& use : uses) { + IrValue& user = *use.user; + if ((user.Opcode() != IrOpcode::BitFieldUExtract && user.Opcode() != IrOpcode::BitFieldSExtract) || use.operand != 0u) { + continue; + } + auto& offset = resolveArg(user, 1); + auto& count = resolveArg(user, 2); + if (!isImmediate(offset, IrType::U32) || !isImmediate(count, IrType::U32) || count.ImmediateU32() == 0u) { + continue; + } + const auto range = std::ranges::find_if(ranges, [&](const auto& candidate) { + return offset.ImmediateU32() >= candidate.first && count.ImmediateU32() <= candidate.second && + offset.ImmediateU32() - candidate.first <= candidate.second - count.ImmediateU32(); + }); + if (range == ranges.end()) { + continue; + } + user.ReplaceArgument(0, &field(static_cast(range - ranges.begin()))); + user.ReplaceArgument(1, &builder.Constant(offset.ImmediateU32() - range->first)); + lowered = true; + } + return lowered; +} + } // namespace bool ConstantFolder::tryFoldValue(IrProgram& program, IrValue& value) const { IrBuilder builder(program); switch (value.Opcode()) { case IrOpcode::Phi: return foldPhi(value); + case IrOpcode::GetBuiltin: return lowerPackedAncillary(program, builder, value); case IrOpcode::SelectU1: case IrOpcode::SelectF32: case IrOpcode::SelectU32: return foldSelect(value); diff --git a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp index 2b06d89d7..e7182cfd8 100644 --- a/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp +++ b/core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp @@ -179,6 +179,95 @@ std::vector GuestSamplersDescriptor(const std::vector samplers; + std::vector images; +}; + +UnnormalizedProof ProveUnnormalized(const ShaderInfo& info, const ResourceSnapshot& snapshot) { + UnnormalizedProof proof{std::vector(info.samplers.size()), std::vector(info.images.size())}; + for (std::uint32_t r = 0; r < info.samplers.size(); r++) { + if ((GuestSamplersDescriptor({r}, snapshot)[0] & ForceUnnormalizedBit) == 0u) { + continue; + } + const auto& sampler = info.samplers[r]; + const std::uint32_t unsupported = sampler.uses & ~static_cast(SamplerUseExplicitLod); + if (unsupported != 0u) { + failUnnormalized(UnnormalizedUseReason(unsupported)); + } + if (sampler.depthCompare) { + failUnnormalized("is used with depth comparison"); + } + for (const auto& pair : info.sampledPairs) { + if (pair.sampler != r) { + continue; + } + const auto& image = info.images.at(pair.image); + if (image.indirectRoot != ImageResource::NoIndirectImage) { + failUnnormalized("samples an image selected at run time"); + } + if ((image.dimension != RdnaImageDimension::Dim1D && image.dimension != RdnaImageDimension::Dim2D) || image.cube) { + failUnnormalized("samples a 1D-array, 2D-array, 3D, cube or multisampled image"); + } + if (image.depthCompare) { + failUnnormalized("is used with depth comparison"); + } + if (image.conversionFormat != IrBufferFormat::Invalid || image.packed) { + failUnnormalized("samples an image that needs a format conversion or packed access"); + } + proof.images[pair.image] = true; + } + proof.samplers[r] = true; + } + return proof; +} + +std::vector SamplerElements(const IrBindingLayout& layout, const ShaderInfo& info) { + std::vector elements(info.samplers.size(), ShaderInfo::MaxSamplers); + for (const IrDescriptorBinding& logical : layout.descriptors) { + if (logical.kind != DescriptorBindingKind::Samplers) { + continue; + } + for (std::uint32_t element = 0; element < logical.resources.size() && element < ShaderInfo::MaxSamplers; element++) { + elements.at(logical.resources[element]) = element; + } + } + return elements; +} + +std::uint32_t ImageSamplerMask(const ShaderInfo& info, const std::vector& samplerElements, std::uint32_t resource) { + const std::uint32_t root = info.images.at(resource).indirectRoot; + std::uint32_t mask = 0; + for (const SampledResourcePair& pair : info.sampledPairs) { + if (pair.image != resource && pair.image != root) { + continue; + } + if (pair.sampler >= samplerElements.size() || samplerElements[pair.sampler] >= ShaderInfo::MaxSamplers) { + fail("DescriptorBindingBuilder::Populate sampled image pair names a sampler outside the first " + std::to_string(ShaderInfo::MaxSamplers) + " elements of the sampler binding"); + } + mask |= 1u << samplerElements[pair.sampler]; + } + return mask; +} + std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) { std::vector result(layout.ShaderDataDwords(), 0u); for (std::size_t i = 0; i < layout.userDataRegisters.size(); i++) { @@ -199,6 +288,15 @@ std::vector ShaderDataDwordsFor(const IrBindingLayout& layout, st } +std::uint32_t PointFilteredSamplerWord(std::uint32_t word0, std::uint32_t filter) { + const bool reduced = ((word0 >> 29u) & 3u) != 0u; + if (reduced && (((filter >> 20u) & 0xfu) != 0u || ((filter >> 26u) & 3u) == 2u)) { + fail("DescriptorBindingBuilder: a min or max reduction sampler that filters between texels or mip levels samples an image that needs point filtering (sint, converted or depth-bits format), which is not implemented"); + } + const bool mipmapped = ((filter >> 26u) & 3u) != 0u; + return (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u); +} + void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const IrProgram& program, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { Populate(allocation, program.Info(), program.Resources().stage, program.Resources().userDataBase, snapshot, partialThreads); } @@ -206,6 +304,8 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, const ShaderInfo& info, IrShaderStage stage, std::uint32_t userDataBase, const ResourceSnapshot& snapshot, const std::array& partialThreads) const { const IrBindingLayout& layout = allocation.layout; const std::vector shaderData = ShaderDataDwordsFor(layout, userDataBase, snapshot, partialThreads); + const UnnormalizedProof unnormalized = ProveUnnormalized(info, snapshot); + const std::vector samplerElements = SamplerElements(layout, info); std::vector bindings; bindings.reserve(layout.descriptors.size()); @@ -242,6 +342,9 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con physical.imageWritten.push_back(image.written || image.atomic); physical.imageDepthCompare.push_back(image.depthCompare); physical.imageAtomic.push_back(image.atomic); + physical.imageAtomic64.push_back(image.atomic64); + physical.imageUnnormalized.push_back(unnormalized.images.at(resource)); + physical.imageSamplers.push_back(ImageSamplerMask(info, samplerElements, resource)); } break; case DescriptorRole::GuestSamplers: @@ -249,10 +352,10 @@ void DescriptorBindingBuilder::Populate(BindingAllocationResult& allocation, con for (std::size_t element = 0; element < logical.resources.size(); ++element) { const auto& sampler = info.samplers.at(logical.resources[element]); physical.samplerDepthCompare.push_back(sampler.depthCompare); + physical.samplerUnnormalized.push_back(unnormalized.samplers.at(logical.resources[element])); if (sampler.forcePointFiltering) { auto& filter = physical.guestDescriptor.at(element * 4u + 2u); - const bool mipmapped = ((filter >> 26u) & 3u) != 0u; - filter = (filter & ~(0xffu << 20u)) | (1u << 24u) | (mipmapped ? 1u << 26u : 0u); + filter = PointFilteredSamplerWord(physical.guestDescriptor.at(element * 4u), filter); } } break; diff --git a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp index f88ed74a3..08c9b1595 100644 --- a/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp @@ -46,6 +46,7 @@ struct DecodedImage { bool depthBits = false; bool depthUnorm16 = false; IrBufferFormat packedFormat = IrBufferFormat::Invalid; + bool srgbDecode = false; }; ShaderBufferResource decodeBufferDescriptor(const DescriptorValue& value) { @@ -130,7 +131,7 @@ std::uint32_t storageMipCount(const ImageResource& base, const DescriptorValue& return mipBase <= mipLast ? mipLast - mipBase + 1u : 0u; } -DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const ImageResource& base) { +DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const ImageResource& base, std::uint32_t srgbDecodeFormats) { DecodedImage decoded; decoded.mipCount = storageMipCount(base, descriptor); if (decoded.mipCount == 0u) { @@ -151,7 +152,10 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.dimension = descriptorDimension(descriptor, base.dimension); decoded.cube = descriptorIsCube(descriptor); const auto format = rawImageFormat(descriptor); - if (base.atomic && format != IrBufferFormat::Format32UInt && format != IrBufferFormat::Format32SInt && format != IrBufferFormat::Format32Float) { + if (base.atomic64 && format != IrBufferFormat::Format32_32UInt && format != IrBufferFormat::Format32_32SInt && format != IrBufferFormat::Format32_32Float) { + throw std::runtime_error("64-bit atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); + } + if (base.atomic && !base.atomic64 && format != IrBufferFormat::Format32UInt && format != IrBufferFormat::Format32SInt && format != IrBufferFormat::Format32Float) { throw std::runtime_error("atomic image descriptor uses an unsupported format " + std::to_string(static_cast(format))); } const bool storage = base.resourceClass == ImageResourceClass::Storage; @@ -166,10 +170,13 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag decoded.packedFormat = format; } decoded.conversionFormat = RemapTextureFormat(format) != format ? format : IrBufferFormat::Invalid; + decoded.srgbDecode = !storage && (srgbDecodeFormats & SrgbDecodeBit(format)) != 0u; if (storage || decoded.conversionFormat != IrBufferFormat::Invalid) { decoded.shaderSwizzle = descriptorImageSwizzle(descriptor); } - const bool rawSintStorage = storage && format == IrBufferFormat::Format32SInt && base.written && !base.read && !base.atomic; + const bool wideSint = format == IrBufferFormat::Format32SInt || format == IrBufferFormat::Format32_32SInt || format == IrBufferFormat::Format32_32_32_32SInt; + const bool narrowSint = format == IrBufferFormat::Format16SInt || format == IrBufferFormat::Format8_8SInt || format == IrBufferFormat::Format16_16SInt || format == IrBufferFormat::Format8_8_8_8SInt || format == IrBufferFormat::Format16_16_16_16SInt; + const bool rawSintStorage = storage && (wideSint || (narrowSint && !base.packed)) && base.written && !base.read && !base.atomic; decoded.numericClass = base.atomic ? IrTextureNumericClass::Uint : SampledTextureNumericClass(format); if (!storage && !base.depthCompare && IsDepthBitsTexture(descriptor.dwords[1], descriptor.dwords[3])) { decoded.depthBits = true; @@ -184,6 +191,9 @@ DecodedImage decodeImageDescriptor(const DescriptorValue& descriptor, const Imag if (rawSintStorage) { decoded.numericClass = IrTextureNumericClass::Uint; } + if ((rawSintStorage || (base.atomic && format == IrBufferFormat::Format32SInt)) && !base.packed) { + decoded.conversionFormat = format; + } } else if (decoded.numericClass == IrTextureNumericClass::Unsupported || (base.depthCompare && decoded.numericClass != IrTextureNumericClass::Float)) { throw std::runtime_error("sampled image descriptor uses an unsupported format"); } @@ -376,7 +386,7 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con bool usable = !nullImageDescriptor(candidate) && validImageDescriptor(candidate, image.r128); if (usable) { try { - decoded = decodeImageDescriptor(candidate, image); + decoded = decodeImageDescriptor(candidate, image, plan.srgbDecodeFormats); } catch (const std::exception&) { usable = false; } @@ -385,7 +395,7 @@ void resolveTableImage(const IrResourcePlan& plan, std::uint32_t imageIndex, con paddedNull++; continue; } - if (decoded.conversionFormat != IrBufferFormat::Invalid || decoded.fmask) { + if (decoded.conversionFormat != IrBufferFormat::Invalid || decoded.fmask || decoded.srgbDecode) { paddedConversion++; continue; } @@ -536,7 +546,7 @@ std::uint32_t emulatedCompareState(const IrResourcePlan& plan, const ResourceSna const auto clampY = (words[0] >> 3u) & 0x7u; const auto function = (words[0] >> 12u) & 0x7u; const bool unnormalized = ((words[0] >> 15u) & 0x1u) != 0u; - const auto lodBias = words[2] & 0x3fffu; + if (((words[0] >> 29u) & 0x3u) != 0u) throw std::runtime_error("comparison sampling of a color texture through a min or max reduction sampler is not implemented"); const auto magFilter = (words[2] >> 20u) & 0x3u; const auto minFilter = (words[2] >> 22u) & 0x3u; const auto addressMode = [](std::uint32_t clamp) { @@ -551,7 +561,7 @@ std::uint32_t emulatedCompareState(const IrResourcePlan& plan, const ResourceSna const bool border = addressX == EmulatedCompare::AddressBorder || addressY == EmulatedCompare::AddressBorder; if (border && borderType == 3u) throw std::runtime_error("comparison sampling of a color texture with a border color table is not implemented"); if (magFilter != minFilter || magFilter > 1u) throw std::runtime_error("comparison sampling of a color texture is implemented only with equal point or bilinear minification and magnification filters"); - if (unnormalized || lodBias != 0u) throw std::runtime_error("comparison sampling of a color texture does not implement unnormalized coordinates or an LOD bias"); + if (unnormalized) throw std::runtime_error("comparison sampling of a color texture does not implement unnormalized coordinates"); const auto state = EmulatedCompare::Enabled | (function << EmulatedCompare::FunctionShift) | (magFilter == 1u ? EmulatedCompare::Linear : 0u) | (addressX << EmulatedCompare::ClampXShift) | (addressY << EmulatedCompare::ClampYShift) | (border && borderType == 2u ? EmulatedCompare::BorderWhite : 0u); if (samplerState.has_value() && *samplerState != state) throw std::runtime_error("comparison sampling of a color texture through samplers that disagree is not implemented"); @@ -590,7 +600,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s result.images.reserve(plan.info.images.size()); for (std::uint32_t i = 0; i < plan.info.images.size(); i++) { const auto& image = plan.info.images[i]; - const DecodedImage decoded = decodeImageDescriptor(snapshot.images[i], image); + const DecodedImage decoded = decodeImageDescriptor(snapshot.images[i], image, plan.srgbDecodeFormats); if (decoded.fmask && std::any_of(plan.info.sampledPairs.begin(), plan.info.sampledPairs.end(), [i](const SampledResourcePair& pair) { return pair.image == i; })) { throw std::runtime_error("FMASK requires a direct image load"); } @@ -609,6 +619,7 @@ void buildResourceSpecialization(const IrResourcePlan& plan, ResourceSnapshot& s entry.depthUnorm16 = decoded.depthUnorm16; entry.packedFormat = decoded.packedFormat; entry.emulatedCompare = emulatedCompareState(plan, snapshot, i); + entry.srgbDecode = decoded.srgbDecode; result.images.push_back(entry); } @@ -697,6 +708,7 @@ void ResourceMaterializer::Apply(IrProgram& program, const ResourceSpecializatio image.depthUnorm16 = source.depthUnorm16; image.packedFormat = source.packedFormat; image.emulatedCompare = source.emulatedCompare; + image.srgbDecode = source.srgbDecode; if ((source.emulatedCompare & EmulatedCompare::Enabled) != 0u) image.depthCompare = false; image.indirectResources.clear(); } @@ -945,6 +957,7 @@ IrResourcePlan ResourceMaterializer::ExtractPlan(const IrProgram& program) const plan.shaderHash = source.shaderHash; plan.userDataBase = source.userDataBase; plan.userDataCount = source.userDataCount; + plan.srgbDecodeFormats = source.srgbDecodeFormats; plan.memoryInfo = source.memoryInfo; plan.descriptorSources = source.descriptorSources; plan.controlFlow = source.controlFlow; @@ -1026,7 +1039,7 @@ bool ResourceSpecialization::Buffer::operator==(const Buffer& other) const { } bool ResourceSpecialization::Image::operator==(const Image& other) const { - return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits && depthUnorm16 == other.depthUnorm16 && packedFormat == other.packedFormat && emulatedCompare == other.emulatedCompare; + return numericClass == other.numericClass && dimension == other.dimension && mipCount == other.mipCount && conversionFormat == other.conversionFormat && shaderSwizzle == other.shaderSwizzle && indirectRoot == other.indirectRoot && indirectMappingOffset == other.indirectMappingOffset && indirectSearchIterations == other.indirectSearchIterations && cube == other.cube && fmask == other.fmask && depthBits == other.depthBits && depthUnorm16 == other.depthUnorm16 && packedFormat == other.packedFormat && emulatedCompare == other.emulatedCompare && srgbDecode == other.srgbDecode; } bool ResourceSpecialization::operator==(const ResourceSpecialization& other) const { diff --git a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp index bcd33a91c..c6839c69f 100644 --- a/core/shader/recompiler/Optimization/src/ResourceTracker.cpp +++ b/core/shader/recompiler/Optimization/src/ResourceTracker.cpp @@ -831,9 +831,6 @@ private: return i; } } - if (m_info.buffers.size() >= ShaderInfo::MaxBuffers) { - return std::numeric_limits::max(); - } BufferResource resource; resource.source = source; resource.firstUsePc = pc; @@ -887,6 +884,11 @@ private: const auto access = ImageOpcodeInfoOf(op).access; const bool atomic = access == ImageAccess::Atomic; const bool write = access == ImageAccess::Write || atomic; + const bool atomic64 = IsImageAtomic64Opcode(op); + if ((image.read || image.written) && image.atomic64 != atomic64) { + throw std::runtime_error("an image accessed by 64-bit atomics is also accessed in another way"); + } + image.atomic64 = atomic64; image.firstUsePc = std::min(image.firstUsePc, pc); image.read = image.read || !write || atomic; image.written = image.written || write; @@ -982,9 +984,6 @@ private: } GetHandle(inst.Argument(0), IrOpcode::GetBufferResource, 4, handle, source); resource = AddBuffer(source, memory, op, flags.pc); - if (resource == std::numeric_limits::max()) { - fail("buffer resource limit exceeded"); - } AddHandlePatch(handle, resource); AddMemoryPatch(flags.index, resource, 0, false); return; diff --git a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp index 4fb2a3d89..e8a2c6ec0 100644 --- a/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp +++ b/core/shader/recompiler/Optimization/src/ShaderInfoCollector.cpp @@ -424,6 +424,50 @@ void CollectOutputs(const IrProgram& program, ShaderStageInputInfo inputInfo, Sh } } +std::uint8_t SamplerUseOf(IrOpcode opcode, std::uint32_t flags, IrShaderStage stage) { + if (opcode == IrOpcode::ImageGatherRaw) { + return SamplerUseGather; + } + if (opcode == IrOpcode::ImageQueryLod) { + return SamplerUseQueryLod; + } + std::uint32_t use = (flags & RdnaImageSampleFlagDerivative) != 0u ? SamplerUseGradient : ImageSampleExplicitLod(flags, stage) ? SamplerUseExplicitLod : SamplerUseImplicitLod; + if ((flags & RdnaImageSampleFlagOffset) != 0u) { + use |= SamplerUseOffset; + } + if ((flags & RdnaImageSampleFlagCompare) != 0u) { + use |= SamplerUseCompare; + } + if ((flags & RdnaImageSampleFlagAdjust) != 0u) { + use |= SamplerUseAdjust; + } + return static_cast(use); +} + +void CollectSamplerUses(const IrProgram& program, ShaderInfo& info) { + for (auto& sampler : info.samplers) { + sampler.uses = 0; + } + const auto& memoryInfo = program.Resources().memoryInfo; + for (const auto& block : program.Blocks()) { + for (const IrValue* inst : block->Instructions()) { + if (!ImageOpcodeInfoOf(inst->Opcode()).needsSampler) { + continue; + } + const auto index = inst->Flags().index; + if (index >= memoryInfo.size()) { + return Fail("image instruction has no memory metadata"); + } + const auto& memory = memoryInfo[index]; + if (memory.sampler >= info.samplers.size()) { + return Fail("image instruction references an unknown sampler"); + } + auto& sampler = info.samplers[memory.sampler]; + sampler.uses = static_cast(sampler.uses | SamplerUseOf(inst->Opcode(), memory.imageSampleFlags, program.Resources().stage)); + } + } +} + } void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo& inputInfo) const { @@ -460,6 +504,7 @@ void ShaderInfoCollector::Collect(IrProgram& program, const ShaderStageInputInfo } CollectBuiltinInputs(program, next); CollectOutputs(program, inputInfo, next); + CollectSamplerUses(program, next); program.Info() = std::move(next); program.Metadata().shaderInfoComplete = true; } diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp index c700ee099..32796f010 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaDescriptorFormat.hpp @@ -23,6 +23,22 @@ enum class ImageType : std::uint32_t { [[nodiscard]] std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3); [[nodiscard]] bool IsDepthBitsTexture(std::uint32_t word1, std::uint32_t word3); +[[nodiscard]] constexpr std::uint32_t SrgbDecodeBit(IrBufferFormat format) { + switch (format) { + case IrBufferFormat::Format8Srgb: return 1u; + case IrBufferFormat::Format8_8Srgb: return 2u; + default: return 0u; + } +} + +[[nodiscard]] constexpr IrBufferFormat SrgbUnormFormat(IrBufferFormat format) { + switch (format) { + case IrBufferFormat::Format8Srgb: return IrBufferFormat::Format8UNorm; + case IrBufferFormat::Format8_8Srgb: return IrBufferFormat::Format8_8UNorm; + default: return IrBufferFormat::Invalid; + } +} + } #endif diff --git a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp index ef4daa6c7..9cc00be2f 100644 --- a/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp +++ b/core/shader/recompiler/RdnaDecoder/include/RdnaDecoder/RdnaOpcode.hpp @@ -270,6 +270,7 @@ enum class RdnaOpcode : std::uint16_t { SMulHiI32, SMulkI32, SBfeU64, + SBfeI64, SBfmB32, SBfmB64, SCselectB32, @@ -858,6 +859,10 @@ enum class RdnaOpcode : std::uint16_t { DsXorB32, DsXorRtnB32, DsWrxchgRtnB32, + DsWrxchg2RtnB32, + DsWrxchg2st64RtnB32, + DsWrxchg2RtnB64, + DsWrxchg2st64RtnB64, DsMinF32, DsMaxF32, DsSwizzleB32, diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp index acc3b3135..21faa359a 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaDescriptorFormat.cpp @@ -13,10 +13,11 @@ struct FormatInfo { bool sint32; }; -constexpr std::array kFormatInfoTable {{ +constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format8UNorm, true, false, false}, {IrBufferFormat::Format8SNorm, false, false, false}, {IrBufferFormat::Format8UInt, true, true, false}, + {IrBufferFormat::Format8SInt, true, false, true}, {IrBufferFormat::Format16UNorm, true, false, false}, {IrBufferFormat::Format16SNorm, true, false, false}, {IrBufferFormat::Format16UInt, true, true, false}, @@ -34,6 +35,7 @@ constexpr std::array kFormatInfoTable {{ {IrBufferFormat::Format16_16UInt, true, true, false}, {IrBufferFormat::Format16_16SInt, true, false, true}, {IrBufferFormat::Format16_16Float, true, false, false}, + {IrBufferFormat::Format11_11_10UNorm, true, true, false}, {IrBufferFormat::Format11_11_10UInt, true, true, false}, {IrBufferFormat::Format11_11_10Float, true, false, false}, {IrBufferFormat::Format10_10_10_2UNorm, true, false, false}, @@ -118,7 +120,7 @@ IrTextureNumericClass SampledTextureNumericClass(IrBufferFormat format) { } IrBufferFormat RemapTextureFormat(IrBufferFormat format) { - return format == IrBufferFormat::Format11_11_10UInt ? IrBufferFormat::Format32UInt : format; + return format == IrBufferFormat::Format11_11_10UNorm || format == IrBufferFormat::Format11_11_10UInt ? IrBufferFormat::Format32UInt : format; } std::uint32_t DepthBitsTextureWidth(std::uint32_t word1, std::uint32_t word3) { diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp index b4d31ad94..4d0869dc2 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp @@ -348,7 +348,9 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span> 8u) & 15u; const bool compareSwap = info.opcode == RdnaOpcode::ImageAtomicCmpswap || info.opcode == RdnaOpcode::ImageAtomicFcmpswap; - if (dmask == 0u || (compareSwap ? dmask != 3u : (info.gather || info.atomic || msaaLoad) && !std::has_single_bit(dmask))) { + const bool floatAtomic = info.opcode == RdnaOpcode::ImageAtomicFcmpswap || info.opcode == RdnaOpcode::ImageAtomicFmin || info.opcode == RdnaOpcode::ImageAtomicFmax; + const bool atomic64 = info.atomic && !floatAtomic && !d16 && dmask == (compareSwap ? 15u : 3u); + if (dmask == 0u || (!atomic64 && (compareSwap ? dmask != 3u : (info.gather || info.atomic || msaaLoad) && !std::has_single_bit(dmask)))) { throw std::runtime_error("invalid MIMG data mask"); } if (d16 && !(info.sample || info.gather || opcode == 0u || opcode == 1u || opcode == 8u || opcode == 9u)) { @@ -397,7 +399,7 @@ RdnaInstruction DecodeRdnaMimg(std::uint32_t programCounter, std::span 256u) { + throw std::runtime_error("DS write exchange destination register range overflow"); + } + } if (info.opcode == RdnaOpcode::DsWriteAddtidB32 && data1 != 0u) { throw std::runtime_error("DS write addtid data1 operand is not supported"); } @@ -915,16 +931,16 @@ RdnaInstruction DecodeRdnaDs(std::uint32_t programCounter, std::span #include +#include #include #include #include "ControlFlow/include/ControlFlow/GraphBuilder.hpp" @@ -170,6 +171,7 @@ IrProgram PrepareResourceProgram(const RecompileRequest& request) { resourceTracker.Track(program); deadCodeEliminator.Eliminate(program); dumpIr("resources"); + program.Resources().srgbDecodeFormats = request.target.srgbDecodeFormats; return program; } @@ -183,6 +185,13 @@ struct ResultMemoEntry { std::shared_ptr result; }; +struct EmissionFailure { + std::uint64_t codeAddress; + BindingLayout layout; + ResourceSpecialization specialization; + std::exception_ptr failure; +}; + struct SourceEntry { std::mutex mutex; // The code the entry was built for: the key carries only a hash of it, so a candidate entry is @@ -196,6 +205,7 @@ struct SourceEntry { std::exception_ptr planFailure; std::unique_ptr program; std::vector> variants; + std::vector emissionFailures; // The result memo, most recently used first, at most ResultMemoEntries (under mutex). std::list memo; std::unordered_map::iterator> memoIndex; @@ -225,6 +235,11 @@ struct SourceKeyHash { } }; +bool FailureMemo() { + static const bool memo = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr; + return memo; +} + std::shared_ptr getSource(const RecompileRequest& request) { static std::shared_mutex mutex; // Entries whose code hashes alike share a bucket; the code comparison picks the right one. @@ -266,14 +281,13 @@ std::shared_ptr getSource(const RecompileRequest& request) { { std::lock_guard lock(source->mutex); if (source->plan == nullptr) { - static const bool memoFailures = std::getenv("APS5_NO_FAILURE_MEMO") == nullptr; - if (memoFailures && source->planFailure) std::rethrow_exception(source->planFailure); + if (FailureMemo() && source->planFailure) std::rethrow_exception(source->planFailure); try { ResourceProgram resource(request); source->plan = std::move(resource.plan); source->program = std::move(resource.program); } catch (...) { - if (memoFailures) source->planFailure = std::current_exception(); + if (FailureMemo()) source->planFailure = std::current_exception(); throw; } } @@ -290,7 +304,7 @@ std::uint64_t nextVariantId() { return variants.fetch_add(1, std::memory_order_relaxed) + 1; } -CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization) { +CompiledVariant compileVariant(const RecompileRequest& request, IrProgram program, const ResourceSnapshot& resourceSnapshot, const ResourceSpecialization& resourceSpecialization, std::exception_ptr* emissionFailure = nullptr) { const auto inputInfo = RequestInputInfo(request); constexpr DeadCodeEliminator deadCodeEliminator; constexpr ResourceMaterializer resourceMaterializer; @@ -320,11 +334,17 @@ CompiledVariant compileVariant(const RecompileRequest& request, IrProgram progra constexpr SpirvEmitter spirvEmitter; RecompileResult result; result.variantId = nextVariantId(); - result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); - + try { + result.spirv = spirvEmitter.Emit(program, inputInfo, bindings, targetOptions); #if ANYPS5_ENABLE_SPIRV_TOOLS - result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets); + result.spirv = ValidateAndOptimizeSpirv(result.spirv, request.target.vulkanVersion, request.target.spirvVersion, request.target.nonConstantImageOffsets); #endif + } catch (const std::bad_alloc&) { + throw; + } catch (...) { + if (emissionFailure != nullptr) *emissionFailure = std::current_exception(); + throw; + } result.bdaAbiVersion = program.Info().usesDma ? request.target.bdaAbiVersion : 0u; result.memoryOffsetDword = bindings.layout.memoryOffsetDword; @@ -383,6 +403,9 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, return candidate; } } + for (const auto& failure : source.emissionFailures) { + if (failure.codeAddress == request.shader.codeAddress && sameLayout(failure.layout, request.layout) && failure.specialization == specialization) std::rethrow_exception(failure.failure); + } cacheHit = false; const bool disk = ShaderDiskCache::Enabled() && !DebugProbeActive(); std::vector diskKey; @@ -400,7 +423,13 @@ std::shared_ptr findOrCompileVariant(SourceEntry& source, if (variant == nullptr) { auto program = source.program != nullptr ? std::move(*source.program) : PrepareResourceProgram(request); source.program.reset(); - variant = std::make_shared(compileVariant(request, std::move(program), snapshot, specialization)); + std::exception_ptr emissionFailure; + try { + variant = std::make_shared(compileVariant(request, std::move(program), snapshot, specialization, &emissionFailure)); + } catch (...) { + if (emissionFailure != nullptr && FailureMemo()) source.emissionFailures.push_back({request.shader.codeAddress, request.layout, specialization, emissionFailure}); + throw; + } if (disk) ShaderDiskCache::Store(std::move(diskKey), variant); } source.program.reset(); diff --git a/core/shader/recompiler/Recompiler.hpp b/core/shader/recompiler/Recompiler.hpp index 1b3aff3a7..9db87189b 100644 --- a/core/shader/recompiler/Recompiler.hpp +++ b/core/shader/recompiler/Recompiler.hpp @@ -197,6 +197,7 @@ struct SpirvTarget { std::optional mesh; std::optional tessellation; bool nonConstantImageOffsets = false; + std::uint32_t srgbDecodeFormats = 0; }; struct BindingLayout { @@ -318,6 +319,7 @@ struct DescriptorBinding { std::vector imageWritten; std::vector imageDepthCompare; std::vector imageAtomic; + std::vector imageAtomic64; // Guest buffer elements the shader updates atomically (one entry per element of a GuestBuffers // binding, empty otherwise). An atomic on a host-imported range is a serialized PCIe round trip // (~0.4-0.5 us each on NVIDIA), so a driver may keep these elements in device-local memory. @@ -328,6 +330,9 @@ struct DescriptorBinding { // skip the write-back and the pending-write note for the element; an element beyond the vector // (a producer that does not fill it) must be treated as written. std::vector bufferWritten; + std::vector samplerUnnormalized; + std::vector imageUnnormalized; + std::vector imageSamplers; }; struct VertexAttribute { diff --git a/core/shader/recompiler/ShaderDiskCache.cpp b/core/shader/recompiler/ShaderDiskCache.cpp index 18711d1e0..ed87d113e 100644 --- a/core/shader/recompiler/ShaderDiskCache.cpp +++ b/core/shader/recompiler/ShaderDiskCache.cpp @@ -64,7 +64,7 @@ namespace ShaderRecompiler::ShaderDiskCache { #if defined(__linux__) && defined(__x86_64__) && defined(__GLIBCXX__) static_assert(sizeof(RecompileResult) == 176, "RecompileResult changed: update EncodeResult and DecodeResult"); -static_assert(sizeof(DescriptorBinding) == 304, "DescriptorBinding changed: update the binding encoder"); +static_assert(sizeof(DescriptorBinding) == 448, "DescriptorBinding changed: update the binding encoder"); static_assert(sizeof(VertexAttribute) == 28, "VertexAttribute changed: update the attribute encoder"); static_assert(sizeof(FragmentParameter) == 12, "FragmentParameter changed: update the parameter encoder"); static_assert(sizeof(CompiledShaderInfo) == 304, "CompiledShaderInfo changed: update the info encoder"); @@ -80,7 +80,7 @@ static_assert(sizeof(IrDescriptorBinding) == 32, "IrDescriptorBinding changed: u static_assert(sizeof(BindingAllocationResult) == 120, "BindingAllocationResult changed: update the allocation encoder"); static_assert(sizeof(ResourceSpecialization) == 72, "ResourceSpecialization changed: update BuildKey"); static_assert(sizeof(ResourceSpecialization::Buffer) == 16, "ResourceSpecialization::Buffer changed: update BuildKey"); -static_assert(sizeof(ResourceSpecialization::Image) == 44, "ResourceSpecialization::Image changed: update BuildKey"); +static_assert(sizeof(ResourceSpecialization::Image) == 48, "ResourceSpecialization::Image changed: update BuildKey"); static_assert(sizeof(BindingLayout) == 16, "BindingLayout changed: update BuildKey"); #endif @@ -249,8 +249,12 @@ void encodeBinding(Writer& writer, const DescriptorBinding& binding) { writer.Flags(binding.imageWritten); writer.Flags(binding.imageDepthCompare); writer.Flags(binding.imageAtomic); + writer.Flags(binding.imageAtomic64); writer.Flags(binding.bufferAtomic); writer.Flags(binding.bufferWritten); + writer.Flags(binding.samplerUnnormalized); + writer.Flags(binding.imageUnnormalized); + writer.Values(std::span(binding.imageSamplers)); } void decodeBinding(Reader& reader, DescriptorBinding& binding) { @@ -268,8 +272,12 @@ void decodeBinding(Reader& reader, DescriptorBinding& binding) { reader.Flags(binding.imageWritten); reader.Flags(binding.imageDepthCompare); reader.Flags(binding.imageAtomic); + reader.Flags(binding.imageAtomic64); reader.Flags(binding.bufferAtomic); reader.Flags(binding.bufferWritten); + reader.Flags(binding.samplerUnnormalized); + reader.Flags(binding.imageUnnormalized); + reader.Values(binding.imageSamplers); } void encodeResult(Writer& writer, const RecompileResult& result) { @@ -401,9 +409,11 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(image.read); out.Value(image.written); out.Value(image.atomic); + out.Value(image.atomic64); out.Value(image.depthCompare); out.Value(image.cube); out.Value(image.r128); + out.Value(image.srgbDecode); out.Value(image.depthBits); out.Value(image.depthUnorm16); out.Value(image.packed); @@ -419,6 +429,7 @@ void encodeInfo(Writer& writer, const CompiledShaderInfo& compiled) { out.Value(sampler.firstUsePc); out.Value(sampler.forcePointFiltering); out.Value(sampler.depthCompare); + out.Value(sampler.uses); }); writer.List(info.sampledPairs, [](Writer& out, const SampledResourcePair& pair) { out.Value(pair.image); @@ -491,9 +502,11 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.read); in.Value(image.written); in.Value(image.atomic); + in.Value(image.atomic64); in.Value(image.depthCompare); in.Value(image.cube); in.Value(image.r128); + in.Value(image.srgbDecode); in.Value(image.depthBits); in.Value(image.depthUnorm16); in.Value(image.packed); @@ -504,11 +517,12 @@ void decodeInfo(Reader& reader, CompiledShaderInfo& compiled) { in.Value(image.indirectSearchIterations); in.Values(image.indirectResources); }); - reader.List(info.samplers, 10, [](Reader& in, SamplerResource& sampler) { + reader.List(info.samplers, 11, [](Reader& in, SamplerResource& sampler) { in.Value(sampler.source); in.Value(sampler.firstUsePc); in.Value(sampler.forcePointFiltering); in.Value(sampler.depthCompare); + in.Value(sampler.uses); }); reader.List(info.sampledPairs, 12, [](Reader& in, SampledResourcePair& pair) { in.Value(pair.image); @@ -815,6 +829,7 @@ void BuildKey(const RecompileRequest& request, std::uint32_t hostSubgroupSize, c out.Value(image.depthUnorm16); out.Value(image.packedFormat); out.Value(image.emulatedCompare); + out.Value(image.srgbDecode); }); writer.Values(std::span(specialization.boundDescriptors)); const auto& switches = switchKey(); diff --git a/core/shader/recompiler/ShaderDiskCache.hpp b/core/shader/recompiler/ShaderDiskCache.hpp index 84c7a081b..01a2e20bb 100644 --- a/core/shader/recompiler/ShaderDiskCache.hpp +++ b/core/shader/recompiler/ShaderDiskCache.hpp @@ -12,7 +12,7 @@ namespace ShaderRecompiler::ShaderDiskCache { -inline constexpr std::uint32_t FormatVersion = 4; +inline constexpr std::uint32_t FormatVersion = 7; enum class LoadStatus { Loaded, diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp index 0b433bfeb..f032957af 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvAnalysis.hpp @@ -23,6 +23,7 @@ struct SpirvRequirements { bool functionScratch = false; bool pixelValidMask = false; bool bufferInt64Atomics = false; + bool imageInt64Atomics = false; bool sharedInt64Atomics = false; bool float64 = false; bool coherentBuffers = false; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp index b06ea3e98..a1323899b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvBda.hpp @@ -13,6 +13,8 @@ std::uint32_t BdaConstant(SpirvEmitterState& state, std::uint64_t value); std::uint32_t BdaWord(SpirvEmitterState& state, std::uint32_t variable, std::uint32_t index); std::uint32_t BdaLoadWord(SpirvEmitterState& state, std::uint32_t index); std::uint32_t BdaLoadAddress(SpirvEmitterState& state, std::uint32_t index); +void DefineBdaFaultFunction(SpirvEmitterState& state); +void DefineBdaDwordReadFunctions(SpirvEmitterState& state); void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason); void ValidateBdaTarget(const IrProgram& program, const SpirvTargetOptions& target); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp index 201f4f9ae..7d8f0874c 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterHelpers.hpp @@ -3,9 +3,15 @@ #include "SpirvBackend/SpirvEmitterState.hpp" #include "Optimization/BindingAllocator.hpp" +#include namespace ShaderRecompiler { +[[noreturn]] void FailEmit(const std::string& reason); +IrShaderStage StageOf(const SpirvEmitterState& state); +const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state); +const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state); +const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state); const RdnaImageDimensionInfo& RdnaImageDimensionInfoFor(RdnaImageDimension dimension); std::uint32_t TypeVoid(SpirvEmitterState& state); std::uint32_t TypeBool(SpirvEmitterState& state); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index 6080354b9..f558e5b85 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -361,6 +361,17 @@ void EmitImageAtomicDec32(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageAtomicFCmpSwap32(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageAtomicFMin32(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitImageAtomicFMax32(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicSwap64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicIAdd64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicISub64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicUMin64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicUMax64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicSMin64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicSMax64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicAnd64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicOr64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicXor64(SpirvValueEmitContext& ctx, const IrValue& inst); +void EmitImageAtomicCmpSwap64(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitPhi(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitTessellationBase(SpirvValueEmitContext& ctx, const IrValue& inst); void EmitGetThreadBitScalarRegister(SpirvValueEmitContext& ctx, const IrValue& inst); diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp index 1e761cf73..8fb8e187b 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterState.hpp @@ -82,6 +82,7 @@ struct SpirvEmitterState { SpirvRequirements requirements; std::uint32_t laneCount = 1; + std::uint32_t hostSubgroupSize = 0; bool splitSubgroup = false; std::unordered_set sharedLaneValues; std::uint32_t laneHalf = 0; @@ -96,7 +97,7 @@ struct SpirvEmitterState { bool tableIndexNonUniform = true; std::uint32_t storageBufferVariable = 0; std::uint32_t storageBufferU64Variable = 0; - std::array memoryByteOffsets {}; + std::vector memoryByteOffsets; std::uint32_t bdaPagetableVariable = 0; std::uint32_t faultBufferVariable = 0; std::uint32_t bdaPointerFunction = 0; @@ -105,6 +106,9 @@ struct SpirvEmitterState { std::uint32_t bdaProbeFunction = 0; std::uint32_t bdaWritePointerFunction = 0; std::uint32_t bdaNoteWriteFunction = 0; + std::uint32_t bdaFaultFunction = 0; + std::array, 2> bdaDwordReadFunctions {}; + std::uint32_t bdaStopValue = 0; // False for programs with workgroup barriers: faulting BDA accesses then continue (see BdaInvocationsMayStop). bool bdaStopsInvocations = true; // Execution scope of the barriers that keep one guest wave's LDS accesses in program order across @@ -115,6 +119,7 @@ struct SpirvEmitterState { std::uint32_t loopGuardLimit = 0; std::uint32_t loopGuardVisits = 0; std::uint32_t loopGuardPc = 0; + std::uint32_t srgbTableVariable = 0; std::uint32_t gdsVariable = 0; std::uint32_t gdsLength = 0; std::uint32_t pushConstantVariable = 0; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp index ce1d7b620..430a6bd93 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvModule.hpp @@ -1,6 +1,8 @@ #ifndef CORE_SHADER_RECOMPILIER_SPIRVBACKEND_INCLUDE_SPIRVBACKEND_SPIRVMODULE_HPP #define CORE_SHADER_RECOMPILIER_SPIRVBACKEND_INCLUDE_SPIRVBACKEND_SPIRVMODULE_HPP +#include +#include #include #include #include @@ -25,6 +27,15 @@ struct SpirvDeferredPhi { }; class SpirvModule { +private: + template + static constexpr bool wordSized = (std::is_integral_v || std::is_enum_v) && sizeof(TOperand) <= sizeof(std::uint32_t); + + template + static std::uint32_t narrowWord(TOperand operand) { + return static_cast(operand); + } + public: explicit SpirvModule(std::uint32_t version = 0x00010300u); [[nodiscard]] std::uint32_t AllocateId(); @@ -51,6 +62,28 @@ public: return declareType(opcode, makeTypeKey(opcode, operands...)); } + std::uint32_t Type(std::uint32_t opcode) { + const std::uint32_t key[2] = {opcode, 0u}; + return interned(key, 2u, [this, opcode]() { return declareType(opcode, makeTypeKey(opcode)); }); + } + + template + requires wordSized + std::uint32_t Type(std::uint32_t opcode, TWord width) { + const auto narrowed = narrowWord(width); + const std::uint32_t key[3] = {opcode, 1u, narrowed}; + return interned(key, 3u, [this, opcode, width]() { return declareType(opcode, makeTypeKey(opcode, width)); }); + } + + template + requires (wordSized && wordSized) + std::uint32_t Type(std::uint32_t opcode, TWord width, TSign signness) { + const auto narrowedWidth = narrowWord(width); + const auto narrowedSignness = narrowWord(signness); + const std::uint32_t key[4] = {opcode, 2u, narrowedWidth, narrowedSignness}; + return interned(key, 4u, [this, opcode, width, signness]() { return declareType(opcode, makeTypeKey(opcode, width, signness)); }); + } + template std::uint32_t DecoratedType(std::uint32_t opcode, std::initializer_list annotations, const TOperands&... operands) { return declareDecoratedType(opcode, makeTypeKey(opcode, operands...), annotations); @@ -64,6 +97,29 @@ public: return declareConstant(opcode, std::move(key)); } + std::uint32_t Constant(std::uint32_t opcode, std::uint32_t type) { + const std::uint32_t key[2] = {opcode, type}; + return interned(key, 2u, [this, opcode, type]() { + std::vector keyWords; + keyWords.reserve(2u); + appendOperands(keyWords, opcode, type); + return declareConstant(opcode, std::move(keyWords)); + }); + } + + template + requires wordSized + std::uint32_t Constant(std::uint32_t opcode, std::uint32_t type, TValue value) { + const auto narrowed = narrowWord(value); + const std::uint32_t key[3] = {opcode, type, narrowed}; + return interned(key, 3u, [this, opcode, type, value]() { + std::vector keyWords; + keyWords.reserve(3u); + appendOperands(keyWords, opcode, type, value); + return declareConstant(opcode, std::move(keyWords)); + }); + } + template void AddExecutionMode(std::uint32_t entryPoint, std::uint32_t mode, const TOperands&... operands) { appendInstruction(executionModes, spv::OpExecutionMode, entryPoint, mode, operands...); @@ -80,6 +136,46 @@ public: } private: + static constexpr std::size_t DeclarationCacheSize = 512; + static constexpr std::size_t MaxCachedDeclarationWords = 4; + + struct DeclarationSlot { + std::array words = {}; + std::uint32_t length = 0; + std::uint32_t id = 0; + }; + + static std::size_t declarationCacheSlot(const std::uint32_t* words, std::uint32_t length) { + std::uint64_t hash = 14695981039346656037ull; + for (std::uint32_t index = 0; index < length; index++) { + hash ^= words[index]; + hash *= 1099511628211ull; + } + return static_cast(hash) & (DeclarationCacheSize - 1u); + } + + template + std::uint32_t interned(const std::uint32_t* words, std::uint32_t length, TDeclare&& declare) { + const auto start = declarationCacheSlot(words, length); + std::size_t target = start; + for (std::size_t probe = 0; probe < DeclarationCacheSize; ++probe) { + target = (start + probe) & (DeclarationCacheSize - 1u); + auto& slot = declarationCache[target]; + if (slot.id == 0) { + break; + } + if (slot.length == length && std::equal(words, words + length, slot.words.begin())) { + return slot.id; + } + } + const auto id = declare(); + auto& slot = declarationCache[target]; + std::copy(words, words + length, slot.words.begin()); + slot.length = length; + slot.id = id; + return id; + } + static void appendOperand(std::vector& words, std::uint32_t value) { words.push_back(value); } @@ -147,6 +243,7 @@ private: std::set requiredExtensions; std::map importIds; std::map, std::uint32_t> declarationIds; + std::array declarationCache = {}; std::size_t unpatchedPhiIncomings = 0; std::vector capabilities; std::vector extensions; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp index ec5d995ea..dd8c915aa 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace ShaderRecompiler { @@ -216,6 +217,15 @@ std::uint32_t EmitDsMaskedLaneRead(SpirvEmitterState& state, std::uint32_t sourc return Select(state, TypeU32(state), sourceActive, shuffled, ConstantU32(state, 0u)); } +bool HostSubgroupNarrowerThanWave(const SpirvEmitterState& state) { + return state.program.Resources().stage != IrShaderStage::Compute && state.laneCount == 1u && state.hostSubgroupSize < state.program.WaveSize(); +} + +[[noreturn]] void FailOutsideHostSubgroup(const SpirvValueEmitContext& ctx, const IrValue& inst, const std::string& access) { + const auto& state = ctx.state; + ctx.Fail(inst, (access + " is outside the " + std::to_string(state.hostSubgroupSize) + "-lane host subgroup that runs this wave" + std::to_string(state.program.WaveSize()) + " program at one lane per invocation").c_str()); +} + } std::uint32_t EmitConvertU16U32(SpirvEmitterState& state, std::uint32_t arg0) { @@ -547,6 +557,11 @@ std::uint32_t EmitReadFirstLane(SpirvValueEmitContext& ctx, const IrValue& inst) std::uint32_t EmitReadLane(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; + const IrValue* selector = inst.Argument(1)->Resolve(); + if (selector != nullptr && selector->HasImmediate() && HostSubgroupNarrowerThanWave(state)) { + const auto index = selector->ImmediateU32() & (state.program.WaveSize() - 1u); + if (index >= state.hostSubgroupSize) FailOutsideHostSubgroup(ctx, inst, "v_readlane_b32 of lane " + std::to_string(index)); + } const auto lane = Binary(state, spv::OpBitwiseAnd, TypeU32(state), ctx.Arg(inst, 1), ConstantU32(state, state.program.WaveSize() - 1u)); return ctx.Shuffle(inst, 0, lane); } @@ -562,6 +577,7 @@ std::uint32_t EmitWriteLane(SpirvValueEmitContext& ctx, const IrValue& inst) { std::uint32_t EmitPermlane16U32(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto flags = inst.Flags(); + if (flags.x16 && state.hostSubgroupSize <= 16u && HostSubgroupNarrowerThanWave(state)) FailOutsideHostSubgroup(ctx, inst, "v_permlanex16_b32 from lanes 16-31"); const auto subid = EmitSubgroupLocalInvocationId(state); const auto row = state.module.AllocateId(); const auto rowValue = state.module.AllocateId(); @@ -591,9 +607,9 @@ std::uint32_t EmitPermlane16U32(SpirvValueEmitContext& ctx, const IrValue& inst) if (flags.fetchInactive) { return shuffled; } - const auto sourceExec = ctx.Shuffle(inst, 3, target); + const auto sourceActive = EmitBallotLaneActiveBool(state, ctx.Ballot(inst.Argument(3)), target); const auto result = state.module.AllocateId(); - state.module.AddFunction(spv::OpSelect, TypeU32(state), result, sourceExec, shuffled, flags.boundControl ? ConstantU32(state, 0u) : ctx.Arg(inst, 4)); + state.module.AddFunction(spv::OpSelect, TypeU32(state), result, sourceActive, shuffled, flags.boundControl ? ConstantU32(state, 0u) : ctx.Arg(inst, 4)); return result; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp index 82fdd645b..9ea921ea8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAnalysis.cpp @@ -307,6 +307,9 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { if ((BufferAccessOf(inst->Opcode()) == BufferAccess::Atomic || addressAccess == AddressAccess::Atomic) && inst->Type() == IrType::U64) { requirements.bufferInt64Atomics = true; } + if (IsImageAtomic64Opcode(inst->Opcode())) { + requirements.imageInt64Atomics = true; + } if (IsFloat64Opcode(inst->Opcode())) { requirements.float64 = true; } @@ -363,6 +366,7 @@ SpirvRequirements AnalyzeProgramRequirements(const IrProgram& program) { requirements.functionLds = true; } else if (sharedAccess == SharedAccess::Atomic && inst->Type() == IrType::U64 && kind == ResourceKind::Lds) { requirements.ldsLock = true; + requirements.subgroupBallot = true; } if (sharedAccess == SharedAccess::Append || sharedAccess == SharedAccess::Consume) { requirements.subgroupBallot = true; diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp index 711619793..ad34e63e2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaFault.cpp @@ -1,6 +1,7 @@ #include "SpirvBackend/SpirvBda.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include namespace ShaderRecompiler { @@ -26,7 +27,21 @@ std::uint32_t BdaLoadAddress(SpirvEmitterState& state, std::uint32_t index) { return Binary(state, spv::OpBitwiseOr, TypeScalarU64(state), low, Binary(state, spv::OpShiftLeftLogical, TypeScalarU64(state), high, BdaConstant(state, 32u))); } -void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { +void DefineBdaFaultFunction(SpirvEmitterState& state) { + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + state.bdaFaultFunction = state.module.AllocateId(); + state.module.AddName(state.bdaFaultFunction, "record_bda_fault"); + state.module.AddFunction(spv::OpFunction, TypeVoid(state), state.bdaFaultFunction, spv::FunctionControlDontInlineMask, state.module.Type(spv::OpTypeFunction, TypeVoid(state), u32, u64, u32, u32)); + const auto reason = state.module.AllocateId(); + const auto address = state.module.AllocateId(); + const auto bytes = state.module.AllocateId(); + const auto instruction = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u32, reason); + state.module.AddFunction(spv::OpFunctionParameter, u64, address); + state.module.AddFunction(spv::OpFunctionParameter, u32, bytes); + state.module.AddFunction(spv::OpFunctionParameter, u32, instruction); + EmitLabel(state, state.module.AllocateId()); const auto pointer = BdaWord(state, state.faultBufferVariable, ConstantU32(state, 0u)); const auto previous = state.module.AllocateId(); const auto scope = ConstantU32(state, spv::ScopeDevice); @@ -37,7 +52,7 @@ void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32 const auto store = [&](std::uint32_t index, std::uint32_t value) { state.module.AddFunction(spv::OpStore, BdaWord(state, state.faultBufferVariable, ConstantU32(state, index)), value); }; - store(1, ConstantU32(state, static_cast(reason))); + store(1, reason); store(2, Unary(state, spv::OpUConvert, TypeU32(state), address)); store(3, Unary(state, spv::OpUConvert, TypeU32(state), Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), address, BdaConstant(state, 32u)))); store(4, bytes); @@ -47,6 +62,13 @@ void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32 const auto release = ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsUniformMemoryMask); state.module.AddFunction(spv::OpAtomicStore, pointer, scope, release, ConstantU32(state, static_cast(BdaAbi::FaultState::Ready))); }); + state.module.AddFunction(spv::OpReturn); + state.module.AddFunction(spv::OpFunctionEnd); +} + +void RecordBdaFault(SpirvEmitterState& state, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { + if (state.bdaFaultFunction == 0) throw std::runtime_error("BDA fault function is missing"); + state.module.AddFunction(spv::OpFunctionCall, TypeVoid(state), state.module.AllocateId(), state.bdaFaultFunction, ConstantU32(state, static_cast(reason)), address, bytes, instruction); } void ReturnBdaFailureIf(SpirvEmitterState& state, std::uint32_t condition, std::uint32_t address, std::uint32_t bytes, std::uint32_t instruction, BdaAbi::FaultReason reason) { @@ -67,7 +89,11 @@ void StopBdaInvocationIf(SpirvEmitterState& state, std::uint32_t condition) { state.module.AddFunction(spv::OpSelectionMerge, next, spv::SelectionControlMaskNone); state.module.AddFunction(spv::OpBranchConditional, condition, failed, next); EmitLabel(state, failed); - state.module.AddFunction(state.program.Resources().stage == IrShaderStage::Pixel ? spv::OpKill : spv::OpReturn); + if (state.bdaStopValue != 0) { + state.module.AddFunction(spv::OpReturnValue, state.bdaStopValue); + } else { + state.module.AddFunction(state.program.Resources().stage == IrShaderStage::Pixel ? spv::OpKill : spv::OpReturn); + } EmitLabel(state, next); } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp index f3ef2e128..98c1b24d2 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaLookup.cpp @@ -158,8 +158,10 @@ static std::uint32_t DefineBdaNoteWrite(SpirvEmitterState& state) { void DefineGetBdaPointer(SpirvEmitterState& state) { if (!state.program.Info().usesDma) return; + DefineBdaFaultFunction(state); state.bdaPointerFunction = DefineBdaLookup(state, "get_bda_pointer", true); if (!BdaByteReadsForced()) state.bdaProbeFunction = DefineBdaLookup(state, "probe_bda_pointer", false); + DefineBdaDwordReadFunctions(state); if (state.program.Info().bdaWrites) { state.bdaWritePointerFunction = DefineBdaLookup(state, "get_bda_write_pointer", true, BdaAbi::Write); state.bdaNoteWriteFunction = DefineBdaNoteWrite(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp index df12bfd78..db3fb5db8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvBdaRead.cpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace ShaderRecompiler { @@ -16,10 +17,18 @@ void EmitBdaOverflowCheck(SpirvEmitterState& state, std::uint32_t address, std:: StopBdaInvocationIf(state, overflow); } -std::uint32_t BdaAccessMask(const SpirvEmitterState& state, const IrValue& inst) { +bool BdaCoherent(const SpirvEmitterState& state, const IrValue& inst) { const auto index = inst.Flags().index; const auto& memory = state.program.Resources().memoryInfo; - return spv::MemoryAccessAlignedMask | (index < memory.size() && memory[index].coherent ? spv::MemoryAccessVolatileMask : 0u); + return index < memory.size() && memory[index].coherent; +} + +std::uint32_t BdaAccessMask(bool coherent) { + return spv::MemoryAccessAlignedMask | (coherent ? spv::MemoryAccessVolatileMask : 0u); +} + +std::uint32_t BdaAccessMask(const SpirvEmitterState& state, const IrValue& inst) { + return BdaAccessMask(BdaCoherent(state, inst)); } std::uint32_t EmitBdaByte(SpirvEmitterState& state, std::uint32_t guest, std::uint32_t instruction, std::uint32_t accessMask) { @@ -100,6 +109,40 @@ std::uint32_t BdaAddressUnaligned(SpirvEmitterState& state, std::uint32_t addres } +void DefineBdaDwordReadFunctions(SpirvEmitterState& state) { + const auto u32 = TypeU32(state); + const auto u64 = TypeScalarU64(state); + const auto pair = TypeU32Vector(state, 2); + const auto type = state.module.Type(spv::OpTypeFunction, pair, u64, u32); + const auto& memory = state.program.Resources().memoryInfo; + const bool coherentAccesses = std::any_of(memory.begin(), memory.end(), [](const MemoryInfo& info) { return info.coherent; }); + const bool stops = state.bdaStopsInvocations; + if (stops) state.bdaStopValue = state.module.Constant(spv::OpConstantComposite, pair, ConstantU32(state, 0u), ConstantU32(state, 1u)); + for (const bool stop : {false, true}) { + for (const bool coherent : {false, true}) { + if ((stop && !stops) || (coherent && !coherentAccesses)) continue; + const auto function = state.module.AllocateId(); + state.module.AddName(function, std::string("read_bda_dword_bytes") + (stop ? "_stop" : "") + (coherent ? "_coherent" : "")); + state.module.AddFunction(spv::OpFunction, pair, function, spv::FunctionControlDontInlineMask, type); + const auto address = state.module.AllocateId(); + const auto instruction = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionParameter, u64, address); + state.module.AddFunction(spv::OpFunctionParameter, u32, instruction); + EmitLabel(state, state.module.AllocateId()); + state.bdaStopsInvocations = stop; + EmitBdaOverflowCheck(state, address, 4u, instruction); + const auto value = EmitBdaBytes(state, address, 4u, instruction, BdaAccessMask(coherent)); + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, pair, result, value, ConstantU32(state, 0u)); + state.module.AddFunction(spv::OpReturnValue, result); + state.module.AddFunction(spv::OpFunctionEnd); + state.bdaDwordReadFunctions[stop][coherent] = function; + } + } + state.bdaStopsInvocations = stops; + state.bdaStopValue = 0; +} + // APS5_BDA_BYTE_READS=1 restores one lookup and one byte load per byte for every read. bool BdaByteReadsForced() { static const bool forced = std::getenv("APS5_BDA_BYTE_READS") != nullptr; @@ -243,11 +286,19 @@ std::array EmitBdaDwordReads(SpirvValueEmitContext& ctx, const std::array addresses{}; const auto dwordAddress = [&](std::uint32_t dword) { return AddBdaImmediate(ctx, inst, address, static_cast(offset + dword * 4u)); }; const auto readBytes = [&](std::array& into) { + const auto function = state.bdaDwordReadFunctions[state.bdaStopsInvocations][BdaCoherent(state, inst)]; + if (function == 0) ctx.Fail(inst, "BDA dword read function is missing"); for (std::uint32_t dword = first; dword <= last; ++dword) { if (!isUsed(dword)) continue; if (dword != first) addresses[dword] = dwordAddress(dword); - EmitBdaOverflowCheck(state, addresses[dword], 4u, instruction); - into[dword] = EmitBdaBytes(state, addresses[dword], 4u, instruction, BdaAccessMask(state, inst)); + const auto read = state.module.AllocateId(); + state.module.AddFunction(spv::OpFunctionCall, TypeU32Vector(state, 2), read, function, addresses[dword], instruction); + into[dword] = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), into[dword], read, 0u); + if (!state.bdaStopsInvocations) continue; + const auto stopped = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), stopped, read, 1u); + StopBdaInvocationIf(state, Binary(state, spv::OpINotEqual, TypeBool(state), stopped, ConstantU32(state, 0u))); } }; addresses[first] = dwordAddress(first); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp index 8dcc53b53..596d281b1 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvEmitter.cpp @@ -248,6 +248,7 @@ std::vector SpirvEmitter::Emit(const IrProgram& program, const Sh state.supportedCapabilities = target.supportedCapabilities; state.supportedExtensions = target.supportedExtensions; state.nonConstantImageOffsets = target.nonConstantImageOffsets; + state.hostSubgroupSize = target.subgroupSize; state.splitSubgroup = program.WaveSize() == 32u && target.subgroupSize > 32u; if (state.splitSubgroup && (state.requirements.subgroupBallot || state.requirements.subgroupShuffle)) state.requirements.subgroupLocalInvocationId = true; const auto* workgroup = ShaderWorkgroupInputFor(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index 3f09507e4..496f4658d 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -656,6 +656,17 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::ImageAtomicFCmpSwap32: return Invoke(EmitImageAtomicFCmpSwap32, ctx, inst); case IrOpcode::ImageAtomicFMin32: return Invoke(EmitImageAtomicFMin32, ctx, inst); case IrOpcode::ImageAtomicFMax32: return Invoke(EmitImageAtomicFMax32, ctx, inst); + case IrOpcode::ImageAtomicSwap64: return Invoke(EmitImageAtomicSwap64, ctx, inst); + case IrOpcode::ImageAtomicIAdd64: return Invoke(EmitImageAtomicIAdd64, ctx, inst); + case IrOpcode::ImageAtomicISub64: return Invoke(EmitImageAtomicISub64, ctx, inst); + case IrOpcode::ImageAtomicUMin64: return Invoke(EmitImageAtomicUMin64, ctx, inst); + case IrOpcode::ImageAtomicUMax64: return Invoke(EmitImageAtomicUMax64, ctx, inst); + case IrOpcode::ImageAtomicSMin64: return Invoke(EmitImageAtomicSMin64, ctx, inst); + case IrOpcode::ImageAtomicSMax64: return Invoke(EmitImageAtomicSMax64, ctx, inst); + case IrOpcode::ImageAtomicAnd64: return Invoke(EmitImageAtomicAnd64, ctx, inst); + case IrOpcode::ImageAtomicOr64: return Invoke(EmitImageAtomicOr64, ctx, inst); + case IrOpcode::ImageAtomicXor64: return Invoke(EmitImageAtomicXor64, ctx, inst); + case IrOpcode::ImageAtomicCmpSwap64: return Invoke(EmitImageAtomicCmpSwap64, ctx, inst); case IrOpcode::GetAttribute: return Invoke(EmitGetAttribute, ctx, inst); case IrOpcode::GetInterpolationParameter: return Invoke(EmitGetInterpolationParameter, ctx, inst); case IrOpcode::SetAttribute: return Invoke(EmitSetAttribute, ctx, inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp index 419bf78ed..a79e1055f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp @@ -327,6 +327,66 @@ std::uint32_t ResultVector(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac return result; } +constexpr std::array SrgbToLinear{ + 0x00000000u, 0x399f0000u, 0x3a1f0000u, 0x3a6f0000u, 0x3a9f0000u, 0x3ac70000u, 0x3aef0000u, 0x3b0b0000u, + 0x3b1f0000u, 0x3b330000u, 0x3b470000u, 0x3b5b0000u, 0x3b710000u, 0x3b840000u, 0x3b900000u, 0x3b9d0000u, + 0x3baa0000u, 0x3bb80000u, 0x3bc60000u, 0x3bd50000u, 0x3be50000u, 0x3bf60000u, 0x3c030000u, 0x3c0c0000u, + 0x3c160000u, 0x3c1f0000u, 0x3c290000u, 0x3c340000u, 0x3c3e0000u, 0x3c490000u, 0x3c550000u, 0x3c600000u, + 0x3c6d0000u, 0x3c790000u, 0x3c830000u, 0x3c8a0000u, 0x3c910000u, 0x3c980000u, 0x3c9f0000u, 0x3ca60000u, + 0x3cae0000u, 0x3cb60000u, 0x3cbe0000u, 0x3cc60000u, 0x3cce0000u, 0x3cd70000u, 0x3ce00000u, 0x3ce90000u, + 0x3cf20000u, 0x3cfc0000u, 0x3d030000u, 0x3d080000u, 0x3d0d0000u, 0x3d120000u, 0x3d170000u, 0x3d1c0000u, + 0x3d220000u, 0x3d280000u, 0x3d2d0000u, 0x3d330000u, 0x3d390000u, 0x3d3f0000u, 0x3d450000u, 0x3d4c0000u, + 0x3d520000u, 0x3d590000u, 0x3d5f0000u, 0x3d660000u, 0x3d6d0000u, 0x3d740000u, 0x3d7b0000u, 0x3d810000u, + 0x3d850000u, 0x3d880000u, 0x3d8c0000u, 0x3d900000u, 0x3d940000u, 0x3d980000u, 0x3d9c0000u, 0x3da00000u, + 0x3da40000u, 0x3da90000u, 0x3dad0000u, 0x3db10000u, 0x3db60000u, 0x3dba0000u, 0x3dbf0000u, 0x3dc30000u, + 0x3dc80000u, 0x3dcd0000u, 0x3dd10000u, 0x3dd60000u, 0x3ddb0000u, 0x3de00000u, 0x3de50000u, 0x3dea0000u, + 0x3df00000u, 0x3df50000u, 0x3dfa0000u, 0x3e000000u, 0x3e020000u, 0x3e050000u, 0x3e080000u, 0x3e0b0000u, + 0x3e0e0000u, 0x3e110000u, 0x3e140000u, 0x3e170000u, 0x3e1a0000u, 0x3e1d0000u, 0x3e200000u, 0x3e230000u, + 0x3e260000u, 0x3e290000u, 0x3e2c0000u, 0x3e300000u, 0x3e330000u, 0x3e360000u, 0x3e3a0000u, 0x3e3d0000u, + 0x3e400000u, 0x3e440000u, 0x3e470000u, 0x3e4b0000u, 0x3e4e0000u, 0x3e520000u, 0x3e560000u, 0x3e590000u, + 0x3e5d0000u, 0x3e610000u, 0x3e650000u, 0x3e680000u, 0x3e6c0000u, 0x3e700000u, 0x3e740000u, 0x3e780000u, + 0x3e7c0000u, 0x3e800000u, 0x3e820000u, 0x3e840000u, 0x3e860000u, 0x3e880000u, 0x3e8a0000u, 0x3e8d0000u, + 0x3e8f0000u, 0x3e910000u, 0x3e930000u, 0x3e950000u, 0x3e980000u, 0x3e9a0000u, 0x3e9c0000u, 0x3e9e0000u, + 0x3ea10000u, 0x3ea30000u, 0x3ea50000u, 0x3ea80000u, 0x3eaa0000u, 0x3ead0000u, 0x3eaf0000u, 0x3eb20000u, + 0x3eb40000u, 0x3eb60000u, 0x3eb90000u, 0x3ebc0000u, 0x3ebe0000u, 0x3ec10000u, 0x3ec30000u, 0x3ec60000u, + 0x3ec80000u, 0x3ecb0000u, 0x3ece0000u, 0x3ed10000u, 0x3ed30000u, 0x3ed60000u, 0x3ed90000u, 0x3edb0000u, + 0x3ede0000u, 0x3ee10000u, 0x3ee40000u, 0x3ee70000u, 0x3eea0000u, 0x3eed0000u, 0x3ef00000u, 0x3ef20000u, + 0x3ef50000u, 0x3ef80000u, 0x3efb0000u, 0x3efe0000u, 0x3f010000u, 0x3f020000u, 0x3f040000u, 0x3f050000u, + 0x3f070000u, 0x3f090000u, 0x3f0a0000u, 0x3f0c0000u, 0x3f0d0000u, 0x3f0f0000u, 0x3f110000u, 0x3f120000u, + 0x3f140000u, 0x3f160000u, 0x3f170000u, 0x3f190000u, 0x3f1b0000u, 0x3f1c0000u, 0x3f1e0000u, 0x3f200000u, + 0x3f210000u, 0x3f230000u, 0x3f250000u, 0x3f270000u, 0x3f290000u, 0x3f2a0000u, 0x3f2c0000u, 0x3f2e0000u, + 0x3f300000u, 0x3f320000u, 0x3f330000u, 0x3f350000u, 0x3f370000u, 0x3f390000u, 0x3f3b0000u, 0x3f3d0000u, + 0x3f3f0000u, 0x3f410000u, 0x3f430000u, 0x3f450000u, 0x3f470000u, 0x3f490000u, 0x3f4b0000u, 0x3f4d0000u, + 0x3f4f0000u, 0x3f510000u, 0x3f530000u, 0x3f550000u, 0x3f570000u, 0x3f590000u, 0x3f5b0000u, 0x3f5d0000u, + 0x3f5f0000u, 0x3f610000u, 0x3f630000u, 0x3f650000u, 0x3f680000u, 0x3f6a0000u, 0x3f6c0000u, 0x3f6e0000u, + 0x3f700000u, 0x3f730000u, 0x3f750000u, 0x3f770000u, 0x3f790000u, 0x3f7b0000u, 0x3f7e0000u, 0x3f800000u, +}; + +std::uint32_t DecodeSrgbTexel(SpirvEmitterState& state, std::uint32_t color) { + const auto pointerType = TypePointer(state, spv::StorageClassPrivate, TypeU32(state)); + std::array words{}; + for (std::size_t index = 0; index < words.size(); ++index) words[index] = ConstantU32(state, SrgbToLinear[index]); + const auto arrayType = state.module.Type(spv::OpTypeArray, TypeU32(state), ConstantU32(state, static_cast(words.size()))); + if (state.srgbTableVariable == 0) state.srgbTableVariable = state.module.DefineGlobalVariable(TypePointer(state, spv::StorageClassPrivate, arrayType), spv::StorageClassPrivate); + state.module.AddFunction(spv::OpStore, state.srgbTableVariable, state.module.Constant(spv::OpConstantComposite, arrayType, std::span(words))); + std::uint32_t components[4] = {}; + for (std::uint32_t index = 0; index < 4u; index++) { + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeF32(state), value, color, index); + const auto scaled = Binary(state, spv::OpFAdd, TypeF32(state), Binary(state, spv::OpFMul, TypeF32(state), value, ConstantF32Value(state, 255.0f)), ConstantF32Value(state, 0.5f)); + const auto code = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), code, GlslStd450(state), GLSLstd450UMin, Unary(state, spv::OpConvertFToU, TypeU32(state), scaled), ConstantU32(state, 255u)); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpAccessChain, pointerType, pointer, state.srgbTableVariable, code); + const auto bits = state.module.AllocateId(); + state.module.AddFunction(spv::OpLoad, TypeU32(state), bits, pointer); + components[index] = Unary(state, spv::OpBitcast, TypeF32(state), bits); + } + const auto result = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), result, components[0], components[1], components[2], components[3]); + return result; +} + std::uint32_t QueryDimensions(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; const auto dimension = access.image.dimension; @@ -417,24 +477,46 @@ SpirvBufferFormatInfo ImageConversionFormat(const ImageResource& image) { return {}; } const auto info = GetFormatInfo(format); - if (SampledTextureNumericClass(format) != IrTextureNumericClass::Uint || RemapTextureFormat(format) == format || info.type != SpirvFormatComponentType::Uint || !info.packedBitfield || info.byteSize != sizeof(std::uint32_t) || info.componentCount == 0u || info.componentCount > 4u) { - throw std::runtime_error("image conversion format is not a packed 32-bit unsigned integer format"); + if (SampledTextureNumericClass(format) != IrTextureNumericClass::Uint || RemapTextureFormat(format) == format || (info.type != SpirvFormatComponentType::Uint && info.type != SpirvFormatComponentType::Unorm) || !info.packedBitfield || info.byteSize != sizeof(std::uint32_t) || info.componentCount == 0u || info.componentCount > 4u) { + throw std::runtime_error("image conversion format is not a packed 32-bit unsigned integer or unorm format"); } return info; } +void RequireConvertedUnormAccess(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SpirvBufferFormatInfo& info) { + if (access.mem.dataBits == 16u) { + ctx.Fail(access.inst, "reads or writes a converted unorm image with 16-bit data, which is not implemented"); + } + for (std::uint32_t component = 0; component < 4u; component++) { + const auto selector = (access.image.shaderSwizzle >> (component * 3u)) & 7u; + if (selector >= 4u && selector - 4u >= info.componentCount) { + ctx.Fail(access.inst, "selects a channel the converted image format does not have"); + } + } +} + std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t texel) { auto& state = ctx.state; + if (access.image.srgbDecode) { + return DecodeSrgbTexel(state, texel); + } const auto info = ImageConversionFormat(access.image); if (info.format == IrBufferFormat::Invalid) { return texel; } + const bool unorm = info.type == SpirvFormatComponentType::Unorm; + if (unorm) { + RequireConvertedUnormAccess(ctx, access, info); + } const auto packed = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), packed, texel, 0u); std::uint32_t components[4] = {ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0)}; for (std::uint32_t component = 0; component < info.componentCount; component++) { components[component] = state.module.AllocateId(); state.module.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), components[component], packed, ConstantU32(state, info.componentBitOffset[component]), ConstantU32(state, info.componentBits[component])); + if (unorm) { + components[component] = NormalizeFormatComponent(state, info, component, components[component]); + } } for (std::uint32_t component = info.componentCount; component < 4u; component++) { components[component] = components[component % info.componentCount]; @@ -444,7 +526,7 @@ std::uint32_t UnpackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess for (std::uint32_t component = 0; component < 4u; component++) { const auto selector = (swizzle >> (component * 3u)) & 7u; if (selector == 1u) { - selected[component] = ConstantU32(state, 1u); + selected[component] = ConstantU32(state, FormattedConstantBits(info, SpirvFormattedSourceKind::One)); } else if (selector >= 4u) { selected[component] = components[selector - 4u]; } else { @@ -538,7 +620,7 @@ std::uint32_t UnpackImageGather(SpirvValueEmitContext& ctx, const ImageEmitAcces return result; } -std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t coord) { +std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, const SampleSetup& setup) { auto& state = ctx.state; const auto numericClass = access.image.numericClass; state.module.EmitCapability(spv::CapabilityImageQuery); @@ -547,8 +629,11 @@ std::uint32_t EmitOneDimensionalGatherLz(SpirvValueEmitContext& ctx, const Image state.module.AddFunction(spv::OpImageQuerySizeLod, TypeU32(state), width, image, ConstantU32(state, 0)); const auto widthF32 = state.module.AllocateId(); state.module.AddFunction(spv::OpConvertUToF, TypeF32(state), widthF32, width); - const auto left = state.module.AllocateId(); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), left, GlslStd450(state), GLSLstd450Floor, Binary(state, spv::OpFSub, TypeF32(state), Binary(state, spv::OpFMul, TypeF32(state), coord, widthF32), ConstantF32(state, 0x3f000000u))); + auto left = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeF32(state), left, GlslStd450(state), GLSLstd450Floor, Binary(state, spv::OpFSub, TypeF32(state), Binary(state, spv::OpFMul, TypeF32(state), setup.coord, widthF32), ConstantF32(state, 0x3f000000u))); + if (setup.layout.offset != NoImageComponent) { + left = Binary(state, spv::OpFAdd, TypeF32(state), left, Unary(state, spv::OpConvertSToF, TypeF32(state), PackedOffset(ctx, access, setup.layout))); + } const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler, access.slot); const auto vectorType = ImageVectorType(state, numericClass, 4); const auto scalarType = ImageScalarType(state, numericClass); @@ -572,13 +657,22 @@ std::uint32_t PackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& if (info.format == IrBufferFormat::Invalid) { return texel; } + const bool unorm = info.type == SpirvFormatComponentType::Unorm; + if (unorm) { + RequireConvertedUnormAccess(ctx, access, info); + } auto packed = ConstantU32(state, 0u); for (std::uint32_t component = 0; component < info.componentCount; component++) { const auto value = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, texel, component); - const auto maximum = ConstantU32(state, info.componentBits[component] == 32u ? UINT32_MAX : (1u << info.componentBits[component]) - 1u); - const auto within = Binary(state, spv::OpULessThan, TypeBool(state), value, maximum); - const auto clamped = Select(state, TypeU32(state), within, value, maximum); + auto clamped = value; + if (unorm) { + clamped = EmitFormatStoreComponent(state, info, component, value); + } else { + const auto maximum = ConstantU32(state, info.componentBits[component] == 32u ? UINT32_MAX : (1u << info.componentBits[component]) - 1u); + const auto within = Binary(state, spv::OpULessThan, TypeBool(state), value, maximum); + clamped = Select(state, TypeU32(state), within, value, maximum); + } const auto shifted = info.componentBitOffset[component] == 0u ? clamped : Binary(state, spv::OpShiftLeftLogical, TypeU32(state), clamped, ConstantU32(state, info.componentBitOffset[component])); packed = Binary(state, spv::OpBitwiseOr, TypeU32(state), packed, shifted); } @@ -588,9 +682,25 @@ std::uint32_t PackImageTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& return result; } +SpirvBufferFormatInfo SintStorageFormat(const ImageResource& image) { + const auto format = image.conversionFormat; + if (image.resourceClass != ImageResourceClass::Storage || format == IrBufferFormat::Invalid || SampledTextureNumericClass(format) != IrTextureNumericClass::Sint) { + return {}; + } + const auto info = GetFormatInfo(format); + if (image.numericClass != IrTextureNumericClass::Uint || info.type != SpirvFormatComponentType::Sint || info.packedBitfield || info.componentCount == 0u || info.byteSize == 12u) { + throw std::runtime_error("storage image conversion format is not a SINT format written through a UINT view"); + } + return info; +} + std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t data, bool integer) { auto& state = ctx.state; const auto& mem = access.mem; + const auto sint = SintStorageFormat(access.image); + if (sint.format != IrBufferFormat::Invalid && mem.dataBits == 16u) { + ctx.Fail(access.inst, "stores 16-bit data to an image of a SINT format"); + } const auto swizzle = access.image.shaderSwizzle; std::uint32_t values[4] = {}; const auto dmask = EffectiveDmask(mem); @@ -609,10 +719,13 @@ std::uint32_t StoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& acce } } values[component] = integer ? raw : mem.dataBits == 16u ? EmitF16BitsToF32(state, raw) : Unary(state, spv::OpBitcast, TypeF32(state), raw); + if (component < sint.componentCount) { + values[component] = EmitFormatStoreComponent(state, sint, component, values[component]); + } } const auto texel = state.module.AllocateId(); state.module.AddFunction(spv::OpCompositeConstruct, integer ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4), texel, values[0], values[1], values[2], values[3]); - return PackImageTexel(ctx, access, texel); + return sint.format != IrBufferFormat::Invalid ? texel : PackImageTexel(ctx, access, texel); } std::uint32_t PackedStoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, std::uint32_t data) { @@ -660,24 +773,34 @@ std::uint32_t PackedStoreTexel(SpirvValueEmitContext& ctx, const ImageEmitAccess std::uint32_t ImageAtomicOpcode(IrOpcode opcode) { switch (opcode) { case IrOpcode::ImageAtomicSwap32: + case IrOpcode::ImageAtomicSwap64: return spv::OpAtomicExchange; case IrOpcode::ImageAtomicIAdd32: + case IrOpcode::ImageAtomicIAdd64: return spv::OpAtomicIAdd; case IrOpcode::ImageAtomicUMin32: + case IrOpcode::ImageAtomicUMin64: return spv::OpAtomicUMin; case IrOpcode::ImageAtomicUMax32: + case IrOpcode::ImageAtomicUMax64: return spv::OpAtomicUMax; case IrOpcode::ImageAtomicAnd32: + case IrOpcode::ImageAtomicAnd64: return spv::OpAtomicAnd; case IrOpcode::ImageAtomicOr32: + case IrOpcode::ImageAtomicOr64: return spv::OpAtomicOr; case IrOpcode::ImageAtomicXor32: + case IrOpcode::ImageAtomicXor64: return spv::OpAtomicXor; case IrOpcode::ImageAtomicISub32: + case IrOpcode::ImageAtomicISub64: return spv::OpAtomicISub; case IrOpcode::ImageAtomicSMin32: + case IrOpcode::ImageAtomicSMin64: return spv::OpAtomicSMin; case IrOpcode::ImageAtomicSMax32: + case IrOpcode::ImageAtomicSMax64: return spv::OpAtomicSMax; default: throw std::runtime_error("opcode is not an image atomic"); @@ -695,6 +818,9 @@ void EmitQueryDimensionsOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& ac void EmitQueryLodOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { auto& state = ctx.state; + if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { + ctx.Fail(access.inst, "queries the level of detail of a converted unorm image, which is not implemented"); + } state.module.EmitCapability(spv::CapabilityImageQuery); const auto sampled = MakeSampledImage(state, access.mem.resource, access.mem.sampler, access.slot); const auto coord = CoordF32(ctx, access, 0, RdnaImageDimensionInfoFor(access.image.dimension).spatialComponents, AddressDimension(access).spatialComponents); @@ -756,8 +882,27 @@ void EmitAtomicOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { ctx.Fail(access.inst, "atomics through a bindless image table are unsupported"); } const auto opcode = access.inst.Opcode(); + const auto condition = ctx.Arg(access.inst, access.inst.ArgumentCount() - 1u); + if (IsImageAtomic64Opcode(opcode)) { + ctx.Define(access.inst, EmitValueOrDefaultIfCondition(state, condition, TypeU64(state), ConstantU64(state, 0u), [&]() { + const auto scalar = TypeScalarU64(state); + const auto pointer = state.module.AllocateId(); + state.module.AddFunction(spv::OpImageTexelPointer, TypePointer(state, spv::StorageClassImage, scalar), pointer, StorageImageDescriptorPointer(state, access.mem.resource), CoordU32(ctx, access), ConstantU32(state, 0)); + const auto value = Unary(state, spv::OpBitcast, scalar, ctx.Arg(access.inst, 2)); + const auto old = state.module.AllocateId(); + if (opcode == IrOpcode::ImageAtomicCmpSwap64) { + const auto comparator = Unary(state, spv::OpBitcast, scalar, ctx.Arg(access.inst, 3)); + state.module.AddFunction(spv::OpAtomicCompareExchange, scalar, old, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), ConstantU32(state, spv::MemorySemanticsMaskNone), value, comparator); + } else { + state.module.AddFunction(ImageAtomicOpcode(opcode), scalar, old, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone), value); + } + EmitDeviceAtomicMemoryBarrier(state); + return Unary(state, spv::OpBitcast, TypeU64(state), old); + })); + return; + } const auto value = ctx.Arg(access.inst, 2); - ctx.Define(access.inst, EmitValueOrZeroIfCondition(state, ctx.Arg(access.inst, access.inst.ArgumentCount() - 1u), [&]() { + ctx.Define(access.inst, EmitValueOrZeroIfCondition(state, condition, [&]() { const auto pointer = state.module.AllocateId(); const auto pointerType = TypePointer(state, spv::StorageClassImage, TypeU32(state)); state.module.AddFunction(spv::OpImageTexelPointer, pointerType, pointer, StorageImageDescriptorPointer(state, access.mem.resource), CoordU32(ctx, access), ConstantU32(state, 0)); @@ -795,6 +940,9 @@ SampleSetup MakeSampleSetup(SpirvValueEmitContext& ctx, const ImageEmitAccess& a if (dref && access.image.conversionFormat != IrBufferFormat::Invalid) { ctx.Fail(access.inst, "uses depth comparison with a packed integer image"); } + if (ImageConversionFormat(access.image).type == SpirvFormatComponentType::Unorm) { + ctx.Fail(access.inst, "samples or gathers a converted unorm image, which needs filtering in the shader and is not implemented"); + } const auto coord = CoordF32(ctx, access, layout.coord, dimensionInfo.coordinateComponents, AddressDimension(access).coordinateComponents); return {dimensionInfo, layout, access.image.numericClass, dref, coord}; } @@ -810,10 +958,10 @@ void EmitGatherOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con ctx.Fail(access.inst, "is a comparison gather of a color texture, which is not implemented"); } if (dimension == RdnaImageDimension::Dim1D) { - if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagOffset) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { + if (setup.dref || !HasFlag(mem, RdnaImageSampleFlagLevelZero) || HasFlag(mem, RdnaImageSampleFlagGatherHorizontal)) { ctx.Fail(access.inst, "has an unsupported 1D gather variant"); } - const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup.coord); + const auto sample = EmitOneDimensionalGatherLz(ctx, access, setup); ctx.Define(access.inst, TableResult(ctx, access, ResultVector(ctx, access, UnpackImageGather(ctx, access, sample), setup.numericClass, false, true))); return; } @@ -904,7 +1052,6 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto compare = image.emulatedCompare; if (access.slot != 0) ctx.Fail(access.inst, "compares against a color texture through a bindless image table, which is not implemented"); if (HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod)) ctx.Fail(access.inst, "compares against a color texture with gradients or an explicit LOD, which is not implemented"); - if (setup.layout.bias != NoImageComponent || setup.layout.offset != NoImageComponent || setup.layout.clamp != NoImageComponent) ctx.Fail(access.inst, "compares against a color texture with an LOD bias, texel offset or LOD clamp, which is not implemented"); if (!HasFlag(mem, RdnaImageSampleFlagLevelZero) && (compare & EmulatedCompare::SingleLevel) == 0u) ctx.Fail(access.inst, "compares against a color texture across mip levels, which is not implemented"); const bool arrayed = image.dimension == RdnaImageDimension::Dim2DArray; if (image.dimension != RdnaImageDimension::Dim2D && !arrayed) ctx.Fail(access.inst, "compares against a color texture that is not a 2D or 2D array view, which is not implemented"); @@ -982,9 +1129,19 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess }; const auto scaledU = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 0u), Unary(state, spv::OpConvertSToF, f32, width)); const auto scaledV = Binary(state, spv::OpFMul, f32, extract(f32, setup.coord, 1u), Unary(state, spv::OpConvertSToF, f32, height)); + auto offsetX = ConstantI32(state, 0); + auto offsetY = ConstantI32(state, 0); + if (setup.layout.offset != NoImageComponent) { + const auto offset = PackedOffset(ctx, access, setup.layout); + offsetX = extract(i32, offset, 0u); + offsetY = extract(i32, offset, 1u); + } + const auto texelIndex = [&](std::uint32_t coordinate, std::uint32_t offset) { + return Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {coordinate})), offset); + }; std::uint32_t result; if ((compare & EmulatedCompare::Linear) == 0u) { - result = compareTexel(Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledU})), Unary(state, spv::OpConvertFToS, i32, ext(f32, GLSLstd450Floor, {scaledV}))); + result = compareTexel(texelIndex(scaledU, offsetX), texelIndex(scaledV, offsetY)); } else { const auto half = f32Constant(0.5f); const auto centreU = Binary(state, spv::OpFSub, f32, scaledU, half); @@ -993,8 +1150,8 @@ void EmitEmulatedCompareSample(SpirvValueEmitContext& ctx, const ImageEmitAccess const auto floorV = ext(f32, GLSLstd450Floor, {centreV}); const auto weightU = Binary(state, spv::OpFSub, f32, centreU, floorU); const auto weightV = Binary(state, spv::OpFSub, f32, centreV, floorV); - const auto x0 = Unary(state, spv::OpConvertFToS, i32, floorU); - const auto y0 = Unary(state, spv::OpConvertFToS, i32, floorV); + const auto x0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorU), offsetX); + const auto y0 = Binary(state, spv::OpIAdd, i32, Unary(state, spv::OpConvertFToS, i32, floorV), offsetY); const auto x1 = Binary(state, spv::OpIAdd, i32, x0, ConstantI32(state, 1)); const auto y1 = Binary(state, spv::OpIAdd, i32, y0, ConstantI32(state, 1)); const auto top = ext(f32, GLSLstd450FMix, {compareTexel(x0, y0), compareTexel(x1, y0), weightU}); @@ -1012,7 +1169,7 @@ void EmitSampleOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access, con EmitEmulatedCompareSample(ctx, access, setup); return; } - const bool explicitLod = HasFlag(mem, RdnaImageSampleFlagDerivative) || HasFlag(mem, RdnaImageSampleFlagLod) || HasFlag(mem, RdnaImageSampleFlagLevelZero) || state.program.Resources().stage != IrShaderStage::Pixel; + const bool explicitLod = ImageSampleExplicitLod(mem.imageSampleFlags, state.program.Resources().stage); std::uint32_t opcode = spv::OpImageSampleImplicitLod; if (explicitLod) { opcode = setup.dref ? spv::OpImageSampleDrefExplicitLod : spv::OpImageSampleExplicitLod; @@ -1117,6 +1274,9 @@ TableSelection TableSlot(SpirvValueEmitContext& ctx, const IrValue& inst, const } void EmitSamplingOp(SpirvValueEmitContext& ctx, const ImageEmitAccess& access) { + if (access.image.srgbDecode) { + ctx.Fail(access.inst, "samples or gathers an sRGB image the device cannot sample, which is not implemented"); + } const auto setup = MakeSampleSetup(ctx, access); if (access.inst.Opcode() == IrOpcode::ImageGatherRaw) { EmitGatherOp(ctx, access, setup); @@ -1146,6 +1306,10 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { ctx.Fail(inst, "has no image address"); } const ImageEmitAccess access{inst, mem, image, *address, TableSlot(ctx, inst, mem, image)}; + if (imageInfo.access == ImageAccess::Atomic) { + EmitAtomicOp(ctx, access); + return; + } switch (irOpcode) { case IrOpcode::ImageQueryDimensions: EmitQueryDimensionsOp(ctx, access); @@ -1163,24 +1327,6 @@ void EmitImage(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::ImageGatherRaw: EmitSamplingOp(ctx, access); return; - case IrOpcode::ImageAtomicSwap32: - case IrOpcode::ImageAtomicIAdd32: - case IrOpcode::ImageAtomicUMin32: - case IrOpcode::ImageAtomicUMax32: - case IrOpcode::ImageAtomicAnd32: - case IrOpcode::ImageAtomicOr32: - case IrOpcode::ImageAtomicXor32: - case IrOpcode::ImageAtomicCmpSwap32: - case IrOpcode::ImageAtomicISub32: - case IrOpcode::ImageAtomicSMin32: - case IrOpcode::ImageAtomicSMax32: - case IrOpcode::ImageAtomicInc32: - case IrOpcode::ImageAtomicDec32: - case IrOpcode::ImageAtomicFCmpSwap32: - case IrOpcode::ImageAtomicFMin32: - case IrOpcode::ImageAtomicFMax32: - EmitAtomicOp(ctx, access); - return; default: ctx.Fail(inst, "has no image SPIR-V emitter"); } @@ -1286,6 +1432,50 @@ void EmitImageAtomicFMax32(SpirvValueEmitContext& ctx, const IrValue& inst) { EmitImage(ctx, inst); } +void EmitImageAtomicSwap64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicIAdd64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicISub64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicUMin64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicUMax64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicSMin64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicSMax64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicAnd64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicOr64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicXor64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + +void EmitImageAtomicCmpSwap64(SpirvValueEmitContext& ctx, const IrValue& inst) { + EmitImage(ctx, inst); +} + void EmitImageBvhIntersectRay(SpirvValueEmitContext& ctx, const IrValue& inst) { auto& state = ctx.state; const auto u32 = TypeU32(state); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp index 9c7ee7228..5185ac073 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvBufferAccess.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvDescriptors.hpp" #include @@ -11,27 +12,6 @@ namespace ShaderRecompiler { namespace { - [[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); - } - - const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state) { - switch (state.program.Resources().stage) { - case IrShaderStage::Compute: - if (state.inputInfo.compute == nullptr) { - FailEmit("compute input info is missing"); - } - return state.inputInfo.compute; - case IrShaderStage::Mesh: - if (state.inputInfo.vertex == nullptr) { - FailEmit("vertex input info is missing"); - } - return &state.inputInfo.vertex->mesh; - default: - return nullptr; - } - } - void EnsureLdsStorage(SpirvEmitterState& state) { if (state.ldsVariable != 0) { return; @@ -82,9 +62,7 @@ IrBufferFormat StorageBufferFormat(const SpirvEmitterState& state, const MemoryI void EmitMemoryOffsets(SpirvEmitterState& state) { const IrBindingLayout& layout = state.program.Metadata().bindings; - if (layout.memoryOffsetCount > state.memoryByteOffsets.size()) { - FailEmit("memory offset count exceeds the buffer limit"); - } + state.memoryByteOffsets.assign(layout.memoryOffsetCount, 0u); for (std::uint32_t i = 0; i < layout.memoryOffsetCount; i++) { const auto word = EmitShaderDataDwordLoad(state, layout.memoryOffsetDword + i / 4u); const auto shift = ConstantU32(state, (i % 4u) * 8u); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvConstants.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvConstants.cpp index ab2d368b0..c4a0e2627 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvConstants.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvConstants.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include #include @@ -6,14 +7,6 @@ namespace ShaderRecompiler { -namespace { - -[[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); -} - -} - std::uint32_t ConstantU32(SpirvEmitterState& state, std::uint32_t value) { return state.module.Constant(spv::OpConstant, TypeU32(state), value); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp index b0fee41fa..f74ae1b0b 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp @@ -12,14 +12,6 @@ namespace ShaderRecompiler { namespace { -[[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); -} - -IrShaderStage StageOf(const SpirvEmitterState& state) { - return state.program.Resources().stage; -} - constexpr RdnaImageDimensionInfo ImageDimensions[] = { {RdnaImageDimension::Dim1D, spv::Dim1D, 1, 1, 0, 0}, {RdnaImageDimension::Dim2D, spv::Dim2D, 2, 2, 0, 0}, @@ -126,13 +118,14 @@ std::uint32_t ImageType(SpirvEmitterState& state, const ImageResource& image) { if (image.numericClass != IrTextureNumericClass::Uint) { FailEmit("atomic storage image must be uint"); } - format = spv::ImageFormatR32ui; + format = image.atomic64 ? spv::ImageFormatR64ui : spv::ImageFormatR32ui; } } else { FailEmit("invalid image resource class"); } const auto& info = RdnaImageDimensionInfoFor(image.dimension); - return state.module.Type(spv::OpTypeImage, ImageScalarType(state, image.numericClass), info.spirvDimension, image.depthCompare ? 1u : 0u, info.arrayed, info.multisampled, sampled, format); + const auto scalar = image.atomic64 ? TypeScalarU64(state) : ImageScalarType(state, image.numericClass); + return state.module.Type(spv::OpTypeImage, scalar, info.spirvDimension, image.depthCompare ? 1u : 0u, info.arrayed, info.multisampled, sampled, format); } std::uint32_t ImageViewSizeType(SpirvEmitterState& state, RdnaImageDimension dimension) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp index c7b0c1c01..28a7bdf07 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp @@ -12,12 +12,21 @@ namespace ShaderRecompiler { -namespace { - -[[noreturn]] void FailEmit(const std::string& reason) { +void FailEmit(const std::string& reason) { throw std::runtime_error("SPIR-V module emission failed: " + reason); } +IrShaderStage StageOf(const SpirvEmitterState& state) { + return state.program.Resources().stage; +} + +const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state) { + if (state.inputInfo.vertex == nullptr) { + FailEmit("vertex input info is missing"); + } + return *state.inputInfo.vertex; +} + const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { if (state.inputInfo.pixel == nullptr) { FailEmit("pixel input info is missing"); @@ -25,6 +34,25 @@ const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { return *state.inputInfo.pixel; } +const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state) { + switch (state.program.Resources().stage) { + case IrShaderStage::Compute: + if (state.inputInfo.compute == nullptr) { + FailEmit("compute input info is missing"); + } + return state.inputInfo.compute; + case IrShaderStage::Mesh: + if (state.inputInfo.vertex == nullptr) { + FailEmit("vertex input info is missing"); + } + return &state.inputInfo.vertex->mesh; + default: + return nullptr; + } +} + +namespace { + constexpr std::uint32_t NoBuiltIn = std::numeric_limits::max(); std::uint32_t BuiltInForInput(StageInputKind kind) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp index 80675b2f0..c2497d5f5 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvFormatConvert.cpp @@ -15,8 +15,24 @@ namespace ShaderRecompiler { namespace { - [[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); + std::uint32_t UnormToF32Bits(SpirvEmitterState& state, std::uint32_t value, std::uint32_t bits) { + const std::uint32_t maximum = (1u << bits) - 1u; + const auto zero = EmitCompareU32Constant(state, spv::OpIEqual, value, 0u); + const auto safe = EmitSelectValueU32(state, zero, ConstantU32(state, 1u), value); + const auto msb = state.module.AllocateId(); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), msb, GlslStd450(state), GLSLstd450FindUMsb, safe); + const auto leading = EmitBinaryU32(state, spv::OpISub, ConstantU32(state, bits - 1u), msb); + const auto high = EmitBinaryU32(state, spv::OpShiftLeftLogical, safe, leading); + const auto low = EmitBinaryU32(state, spv::OpShiftRightLogical, safe, EmitBinaryU32(state, spv::OpISub, ConstantU32(state, bits), leading)); + const auto rotated = EmitAndConstant(state, EmitOrU32(state, high, low), maximum); + auto pattern = EmitBinaryU32(state, spv::OpShiftLeftLogical, rotated, ConstantU32(state, 32u - bits)); + for (std::uint32_t shift = bits; shift < 32u; shift *= 2u) { + pattern = EmitOrU32(state, pattern, EmitBinaryU32(state, spv::OpShiftRightLogical, pattern, ConstantU32(state, shift))); + } + const auto round = EmitAndConstant(state, EmitBinaryU32(state, spv::OpShiftRightLogical, pattern, ConstantU32(state, 7u)), 1u); + const auto rounded = EmitAddU32(state, EmitBinaryU32(state, spv::OpShiftRightLogical, pattern, ConstantU32(state, 8u)), round); + const auto exponent = EmitBinaryU32(state, spv::OpShiftLeftLogical, EmitAddU32(state, msb, ConstantU32(state, 126u - bits)), ConstantU32(state, 23u)); + return EmitSelectValueU32(state, zero, ConstantU32(state, 0u), EmitAddU32(state, exponent, rounded)); } } @@ -71,24 +87,16 @@ std::uint32_t NormalizeFormatComponent(SpirvEmitterState& state, const SpirvBuff state.module.AddFunction(spv::OpConvertSToF, TypeF32(state), value, signedRaw); return EmitBitcastF32ToU32(state, value); } - case SpirvFormatComponentType::Unorm: { - const auto value = state.module.AllocateId(); - const auto normalized = state.module.AllocateId(); - const auto maxValue = static_cast((1u << bits) - 1u); - state.module.AddFunction(spv::OpConvertUToF, TypeF32(state), value, raw); - state.module.AddFunction(spv::OpFDiv, TypeF32(state), normalized, value, ConstantF32Value(state, maxValue)); - return EmitBitcastF32ToU32(state, normalized); - } + case SpirvFormatComponentType::Unorm: + return UnormToF32Bits(state, raw, bits); case SpirvFormatComponentType::Snorm: { - const auto signedRaw = EmitTBufferBitcastU32ToI32(state, raw); - const auto value = state.module.AllocateId(); - const auto normalized = state.module.AllocateId(); + const std::uint32_t maximum = (1u << (bits - 1u)) - 1u; + const auto negative = Binary(state, spv::OpSLessThan, TypeBool(state), EmitTBufferBitcastU32ToI32(state, raw), EmitTBufferBitcastU32ToI32(state, ConstantU32(state, 0u))); + const auto magnitude = EmitSelectValueU32(state, negative, Unary(state, spv::OpSNegate, TypeU32(state), raw), raw); const auto clamped = state.module.AllocateId(); - const auto maxValue = static_cast((1u << (bits - 1u)) - 1u); - state.module.AddFunction(spv::OpConvertSToF, TypeF32(state), value, signedRaw); - state.module.AddFunction(spv::OpFDiv, TypeF32(state), normalized, value, ConstantF32Value(state, maxValue)); - state.module.AddFunction(spv::OpExtInst, TypeF32(state), clamped, GlslStd450(state), GLSLstd450FMax, normalized, ConstantF32Value(state, -1.0f)); - return EmitBitcastF32ToU32(state, clamped); + state.module.AddFunction(spv::OpExtInst, TypeU32(state), clamped, GlslStd450(state), GLSLstd450UMin, magnitude, ConstantU32(state, maximum)); + const auto sign = EmitSelectValueU32(state, negative, ConstantU32(state, 0x80000000u), ConstantU32(state, 0u)); + return EmitOrU32(state, sign, UnormToF32Bits(state, clamped, bits - 1u)); } case SpirvFormatComponentType::Float: if (bits == 32u) { diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp index 6ee03acfa..72f796336 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvInputOutput.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvMemory/SpirvInputOutput.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" #include "SpirvBackend/SpirvMemory/SpirvSubgroup.hpp" #include "SpirvBackend/SpirvMemory/SpirvBufferAccess.hpp" @@ -18,28 +19,6 @@ namespace ShaderRecompiler constexpr std::uint32_t PsInputFlatShade = 0x00000400u; constexpr std::uint32_t PixelParameterLimit = 32u; - [[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); - } - - IrShaderStage StageOf(const SpirvEmitterState& state) { - return state.program.Resources().stage; - } - - const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state) { - if (state.inputInfo.vertex == nullptr) { - FailEmit("vertex input info is missing"); - } - return *state.inputInfo.vertex; - } - - const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { - if (state.inputInfo.pixel == nullptr) { - FailEmit("pixel input info is missing"); - } - return *state.inputInfo.pixel; - } - bool IsVertexLikeStage(const SpirvEmitterState& state) { return StageOf(state) == IrShaderStage::Vertex || StageOf(state) == IrShaderStage::Local; } diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp index 3657db5d6..dee96aab9 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp @@ -580,11 +580,35 @@ void FormattedStore(SpirvValueEmitContext& ctx, const IrValue& inst, const Memor }); } +std::uint32_t LoadCoherentPair(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, const MemoryResourceAccess& resource) { + auto& state = ctx.state; + const auto wide = PrepareStorageBufferResourceAccess(state, mem, state.storageBufferU64Variable, TypeStorageBufferU64Pointer(state)); + const auto byteAddress = Binary(state, spv::OpIAdd, TypeU32(state), ByteAddress(ctx, inst, mem), wide.byteOffset); + const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), byteAddress, ConstantU32(state, 3u)); + const auto aligned = Binary(state, spv::OpIEqual, TypeBool(state), Binary(state, spv::OpBitwiseAnd, TypeU32(state), byteAddress, ConstantU32(state, 7u)), ConstantU32(state, 0u)); + return EmitValueIfElse(state, AndCondition(state, aligned, EmitMemoryElementInBounds(state, wide, index)), TypeU32Composite(state, 2u), [&]() { + const auto pointer = EmitStorageBufferElementPointer(state, wide, index, TypeStorageBufferU64ElementPointer(state)); + const auto value = state.module.AllocateId(); + state.module.AddFunction(spv::OpAtomicLoad, TypeScalarU64(state), value, pointer, ConstantU32(state, spv::ScopeDevice), ConstantU32(state, spv::MemorySemanticsMaskNone)); + const auto high = Binary(state, spv::OpShiftRightLogical, TypeScalarU64(state), value, BdaConstant(state, 32u)); + return ConstructU32Composite(state, 2u, {Unary(state, spv::OpUConvert, TypeU32(state), value), Unary(state, spv::OpUConvert, TypeU32(state), high), 0u, 0u}); + }, [&]() { + std::array values{}; + for (std::uint32_t component = 0; component < 2u; component++) { + values.at(component) = LoadWordPrepared(ctx, inst, RebaseRawComponent(mem, component), resource); + } + return ConstructU32Composite(state, 2u, values); + }); +} + std::uint32_t LoadWideBuffer(SpirvValueEmitContext& ctx, const IrValue& inst, const MemoryInfo& mem, std::uint32_t components) { auto& state = ctx.state; return EmitValueOrDefaultIfCondition(state, ActiveArgument(ctx, inst), TypeU32Composite(state, components), ConstantU32CompositeZero(state, components), [&]() { const auto resource = PrepareMemoryResourceAccess(state, mem); const auto info = MemoryFormatInfo(state, mem); + if (info.type == SpirvFormatComponentType::Unknown && components == 2u && mem.coherent && state.storageBufferU64Variable != 0u) { + return LoadCoherentPair(ctx, inst, mem, resource); + } if (info.type != SpirvFormatComponentType::Unknown) { const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info, components, FormattedAccess::Load); return EmitValueOrDefaultIfCondition(state, plan.inBounds, TypeU32Composite(state, components), FormattedOutOfBoundsValue(ctx, mem, plan, components), [&]() { @@ -1180,11 +1204,17 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo } const auto& mem = SharedMemory(ctx, inst); const bool wave64 = state.laneCount == 2u; - const auto m0 = ctx.Arg(inst, 0); - const auto base = Binary(state, spv::OpShiftRightLogical, TypeU32(state), m0, ConstantU32(state, 16u)); - const auto size = Binary(state, spv::OpBitwiseAnd, TypeU32(state), m0, ConstantU32(state, 0xffffu)); - const auto address = Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, mem.offset)); - const auto rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + const bool gds = mem.kind == ResourceKind::Gds; + std::uint32_t rawIndex = ConstantU32(state, mem.offset >> 2u); + std::uint32_t m0Bounds = 0; + if (gds) { + const auto m0 = ctx.Arg(inst, 0); + const auto base = Binary(state, spv::OpShiftRightLogical, TypeU32(state), m0, ConstantU32(state, 16u)); + const auto size = Binary(state, spv::OpBitwiseAnd, TypeU32(state), m0, ConstantU32(state, 0xffffu)); + const auto address = Binary(state, spv::OpIAdd, TypeU32(state), base, ConstantU32(state, mem.offset)); + rawIndex = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2u)); + m0Bounds = Binary(state, spv::OpINotEqual, TypeBool(state), size, ConstantU32(state, 0u)); + } const auto access = PrepareMemoryResourceAccess(state, mem); const auto index = EmitMemoryElementIndex(state, access, rawIndex); const auto exec = ctx.Arg(inst, 1); @@ -1198,12 +1228,12 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo const auto sourceLane = wave64 ? Binary(state, spv::OpBitwiseAnd, TypeU32(state), first, ConstantU32(state, 31u)) : first; const auto isFirst = Binary(state, spv::OpIEqual, TypeBool(state), EmitSubgroupLocalInvocationId(state), sourceLane); const auto storageBounds = EmitMemoryElementInBounds(state, access, index); - const auto m0Bounds = mem.kind == ResourceKind::Gds ? Binary(state, spv::OpINotEqual, TypeBool(state), size, ConstantU32(state, 0u)) : Binary(state, spv::OpULessThan, TypeBool(state), ConstantU32(state, mem.offset + 3u), size); + const auto bounds = gds ? AndCondition(state, storageBounds, m0Bounds) : storageBounds; const auto lanesActive = wave64 ? Binary(state, spv::OpINotEqual, TypeBool(state), count, ConstantU32(state, 0u)) : exec; - const auto condition = AndCondition(state, isFirst, AndCondition(state, lanesActive, AndCondition(state, storageBounds, m0Bounds))); + const auto condition = AndCondition(state, isFirst, AndCondition(state, lanesActive, bounds)); const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() { const auto value = state.module.AllocateId(); - state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, mem.kind == ResourceKind::Gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), count); + state.module.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state), value, EmitMemoryElementPointer(state, access, index), ConstantU32(state, gds ? spv::ScopeDevice : spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsMaskNone), count); return value; }); const auto result = state.module.AllocateId(); @@ -1214,35 +1244,73 @@ std::uint32_t AppendConsume(SpirvValueEmitContext& ctx, const IrValue& inst, boo template std::uint32_t LockedLdsUpdate(SpirvEmitterState& state, TUpdate&& update) { const auto lock = EmitLdsLockPointer(state); + const auto entry = state.currentLabel; const auto header = state.module.AllocateId(); const auto body = state.module.AllocateId(); + const auto pending = state.module.AllocateId(); + const auto spinHeader = state.module.AllocateId(); + const auto spinBody = state.module.AllocateId(); + const auto spinCont = state.module.AllocateId(); const auto critical = state.module.AllocateId(); - const auto after = state.module.AllocateId(); + const auto servedMerge = state.module.AllocateId(); + const auto pendingMerge = state.module.AllocateId(); const auto cont = state.module.AllocateId(); const auto merge = state.module.AllocateId(); + const auto done = state.module.AllocateId(); + const auto result = state.module.AllocateId(); + const auto doneNext = state.module.AllocateId(); + const auto resultNext = state.module.AllocateId(); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, header); + state.module.AddFunction(spv::OpPhi, TypeBool(state), done, ConstantBool(state, false), entry, doneNext, cont); + state.module.AddFunction(spv::OpPhi, TypeU64(state), result, ConstantU64(state, 0u), entry, resultNext, cont); state.module.AddFunction(spv::OpLoopMerge, merge, cont, spv::LoopControlMaskNone); state.module.AddFunction(spv::OpBranch, body); EmitLabel(state, body); + state.module.AddFunction(spv::OpSelectionMerge, pendingMerge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, done, pendingMerge, pending); + EmitLabel(state, pending); + const auto elected = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformElect, TypeBool(state), elected, ConstantU32(state, spv::ScopeSubgroup)); + state.module.AddFunction(spv::OpSelectionMerge, servedMerge, spv::SelectionControlMaskNone); + state.module.AddFunction(spv::OpBranchConditional, elected, spinHeader, servedMerge); + EmitLabel(state, spinHeader); + state.module.AddFunction(spv::OpLoopMerge, critical, spinCont, spv::LoopControlMaskNone); + state.module.AddFunction(spv::OpBranch, spinBody); + EmitLabel(state, spinBody); const auto previous = state.module.AllocateId(); state.module.AddFunction(spv::OpAtomicCompareExchange, TypeU32(state), previous, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsAcquireMask | spv::MemorySemanticsWorkgroupMemoryMask), ConstantU32(state, spv::MemorySemanticsMaskNone), ConstantU32(state, 1u), ConstantU32(state, 0u)); const auto acquired = Binary(state, spv::OpIEqual, TypeBool(state), previous, ConstantU32(state, 0u)); - state.module.AddFunction(spv::OpSelectionMerge, after, spv::SelectionControlMaskNone); - state.module.AddFunction(spv::OpBranchConditional, acquired, critical, after); + state.module.AddFunction(spv::OpBranchConditional, acquired, critical, spinCont); + EmitLabel(state, spinCont); + state.module.AddFunction(spv::OpBranch, spinHeader); EmitLabel(state, critical); const auto old = update(); state.module.AddFunction(spv::OpAtomicStore, lock, ConstantU32(state, spv::ScopeWorkgroup), ConstantU32(state, spv::MemorySemanticsReleaseMask | spv::MemorySemanticsWorkgroupMemoryMask), ConstantU32(state, 0u)); const auto criticalExit = state.currentLabel; - state.module.AddFunction(spv::OpBranch, after); - EmitLabel(state, after); - const auto result = state.module.AllocateId(); - state.module.AddFunction(spv::OpPhi, TypeU64(state), result, old, criticalExit, ConstantU64(state, 0u), body); - state.module.AddFunction(spv::OpBranchConditional, acquired, merge, cont); + state.module.AddFunction(spv::OpBranch, servedMerge); + EmitLabel(state, servedMerge); + const auto served = state.module.AllocateId(); + const auto servedResult = state.module.AllocateId(); + state.module.AddFunction(spv::OpPhi, TypeBool(state), served, ConstantBool(state, true), criticalExit, ConstantBool(state, false), pending); + state.module.AddFunction(spv::OpPhi, TypeU64(state), servedResult, old, criticalExit, result, pending); + state.module.AddFunction(spv::OpBranch, pendingMerge); + EmitLabel(state, pendingMerge); + state.module.AddFunction(spv::OpPhi, TypeBool(state), doneNext, served, servedMerge, done, body); + state.module.AddFunction(spv::OpPhi, TypeU64(state), resultNext, servedResult, servedMerge, result, body); + const auto ballot = state.module.AllocateId(); + state.module.AddFunction(spv::OpGroupNonUniformBallot, TypeU32Vector(state, 4u), ballot, ConstantU32(state, spv::ScopeSubgroup), Unary(state, spv::OpLogicalNot, TypeBool(state), doneNext)); + std::uint32_t remaining = ConstantU32(state, 0u); + for (std::uint32_t component = 0; component < 4u; ++component) { + const auto word = state.module.AllocateId(); + state.module.AddFunction(spv::OpCompositeExtract, TypeU32(state), word, ballot, component); + remaining = Binary(state, spv::OpBitwiseOr, TypeU32(state), remaining, word); + } + state.module.AddFunction(spv::OpBranchConditional, Binary(state, spv::OpINotEqual, TypeBool(state), remaining, ConstantU32(state, 0u)), cont, merge); EmitLabel(state, cont); state.module.AddFunction(spv::OpBranch, header); EmitLabel(state, merge); - return result; + return resultNext; } template diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9100ce94f..395c88156 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -10,49 +10,6 @@ namespace ShaderRecompiler { -namespace { - -[[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); -} - -IrShaderStage StageOf(const SpirvEmitterState& state) { - return state.program.Resources().stage; -} - -const ShaderVertexInputInfo& VertexInfo(const SpirvEmitterState& state) { - if (state.inputInfo.vertex == nullptr) { - FailEmit("vertex input info is missing"); - } - return *state.inputInfo.vertex; -} - -const ShaderPixelInputInfo& PixelInfo(const SpirvEmitterState& state) { - if (state.inputInfo.pixel == nullptr) { - FailEmit("pixel input info is missing"); - } - return *state.inputInfo.pixel; -} - -const ShaderWorkgroupInputInfo* ShaderWorkgroupInput(const SpirvEmitterState& state) { - switch (state.program.Resources().stage) { - case IrShaderStage::Compute: - if (state.inputInfo.compute == nullptr) { - FailEmit("compute input info is missing"); - } - return state.inputInfo.compute; - case IrShaderStage::Mesh: - if (state.inputInfo.vertex == nullptr) { - FailEmit("vertex input info is missing"); - } - return &state.inputInfo.vertex->mesh; - default: - return nullptr; - } -} - -} - void EmitModuleHeader(SpirvModule& module, const IrProgram& program, const BindingAllocationResult& bindings) { CheckBindings(program, bindings); @@ -126,16 +83,28 @@ void DefineModule(SpirvEmitterState& state) { if (state.requirements.sharedInt64Atomics) { state.module.EmitCapability(spv::CapabilityInt64); } + if (state.requirements.imageInt64Atomics) { + if (std::find(state.supportedCapabilities.begin(), state.supportedCapabilities.end(), static_cast(spv::CapabilityInt64ImageEXT)) == state.supportedCapabilities.end()) { + FailEmit("64-bit image atomics need VK_EXT_shader_image_atomic_int64"); + } + state.module.EmitCapability(spv::CapabilityInt64); + state.module.EmitCapability(spv::CapabilityInt64Atomics); + state.module.EmitCapability(spv::CapabilityInt64ImageEXT); + state.module.EmitExtension("SPV_EXT_shader_image_int64"); + } if (state.clipDistanceVariable != 0) { state.module.EmitCapability(spv::CapabilityClipDistance); } if (state.cullDistanceVariable != 0) { state.module.EmitCapability(spv::CapabilityCullDistance); } - if (state.layerVariable != 0 || InputVariableForKind(state, StageInputKind::Layer) != 0) { + if (state.layerVariable != 0) { state.module.RequireVersion(0x00010500u); state.module.EmitCapability(spv::CapabilityShaderLayer); } + if (InputVariableForKind(state, StageInputKind::Layer) != 0) { + state.module.EmitCapability(spv::CapabilityGeometry); + } if (state.viewportIndexVariable != 0) { state.module.RequireVersion(0x00010500u); state.module.EmitCapability(spv::CapabilityShaderViewportIndex); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp index 6532d500d..cde24c0ba 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvSubgroup.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvEmitter.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include #include #include @@ -13,10 +14,6 @@ namespace ShaderRecompiler namespace { - [[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); - } - std::uint32_t HostInvocationId(SpirvEmitterState& state) { if (state.subgroupLocalInvocationIdVariable == 0) { FailEmit("SubgroupLocalInvocationId was not declared before function emission"); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp index acefd6b60..bdc7ced72 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvTypes.cpp @@ -1,4 +1,5 @@ #include "SpirvBackend/SpirvMemory/SpirvTypes.hpp" +#include "SpirvBackend/SpirvEmitterHelpers.hpp" #include "SpirvBackend/SpirvMemory/SpirvConstants.hpp" #include #include @@ -8,10 +9,6 @@ namespace ShaderRecompiler { namespace { -[[noreturn]] void FailEmit(const std::string& reason) { - throw std::runtime_error("SPIR-V module emission failed: " + reason); -} - std::uint32_t StorageBufferType(SpirvEmitterState& state) { const auto array = state.module.DecoratedType(spv::OpTypeRuntimeArray, {{spv::OpDecorate, {spv::DecorationArrayStride, 4u}}}, TypeU32(state)); return state.module.DecoratedType(spv::OpTypeStruct, diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 243a1ed5b..d87a7e86e 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -95,6 +95,7 @@ private: bool bufferAtomic(const RdnaInstruction& inst, IrOpcode opcode); bool imageAtomic(const RdnaInstruction& inst, IrOpcode opcode); bool dsAtomic(const RdnaInstruction& inst, IrOpcode opcode, bool returnsValue); + bool dsWrxchg2(const RdnaInstruction& inst); bool flatLoad(const RdnaInstruction& inst); bool flatStore(const RdnaInstruction& inst); bool flatAtomic(const RdnaInstruction& inst, IrOpcode opcode); @@ -239,7 +240,7 @@ private: IrU32 extractBits32(IrU32 source, IrU32 offset, IrU32 rawCount, bool sign); bool sBfmB64(const RdnaInstruction& inst); bool sBfeU32(const RdnaInstruction& inst, bool sign); - bool sBfeU64(const RdnaInstruction& inst); + bool sBfeU64(const RdnaInstruction& inst, bool sign); bool vBfeU32(const RdnaInstruction& inst, bool sign); bool vBfiB32(const RdnaInstruction& inst); bool sBitcmpB32(const RdnaInstruction& inst, bool expected); @@ -255,6 +256,7 @@ private: bool integerDot(const RdnaInstruction& inst, std::uint32_t elementBits, bool sign, bool accumulator); bool vCndmaskB32(const RdnaInstruction& inst); bool packB16(const RdnaInstruction& inst, bool high0, bool high1); + bool vCvtPk16I32(const RdnaInstruction& inst, bool sign); void sSubvectorLoop(const RdnaInstruction& inst, bool begin); void sSaveexec(const RdnaInstruction& inst, IrOpcode operation, bool negateExec, bool negateSource, bool write64, bool negateResult = false, bool writeResult = false); void addU32(const RdnaInstruction& inst, bool vector, bool useCarryIn); diff --git a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp index cff0d65b7..b3b6c383d 100644 --- a/core/shader/recompiler/Translation/src/InstructionTranslator.cpp +++ b/core/shader/recompiler/Translation/src/InstructionTranslator.cpp @@ -167,7 +167,9 @@ void includeInstructionVectorRegisters(const RdnaInstruction& instruction, std:: case RdnaOpcode::DsWrite2B32: case RdnaOpcode::DsWrite2st64B32: case RdnaOpcode::DsWrite2B64: - case RdnaOpcode::DsWrite2st64B64: { + case RdnaOpcode::DsWrite2st64B64: + case RdnaOpcode::DsWrxchg2RtnB64: + case RdnaOpcode::DsWrxchg2st64RtnB64: { const std::uint32_t width = std::max(instruction.dataDwordCount / 2u, 1u); includeVector(instruction.source1, width); includeVector(instruction.source2, width); diff --git a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp index ba6b25f67..4a9400e1a 100644 --- a/core/shader/recompiler/Translation/src/IntegerInstructions.cpp +++ b/core/shader/recompiler/Translation/src/IntegerInstructions.cpp @@ -548,10 +548,11 @@ bool TranslationContext::sBitreplicateB64B32(const RdnaInstruction& inst) { } IrU32 TranslationContext::readRelativeScalar(std::uint32_t base, IrValue& offset) { - IrU32 result(ir.GetScalarReg(static_cast(base))); + IrValue& inRange = ir.ULessThan(ir.ISub(offset, ir.Constant(1u)), ir.Constant(NumScalarRegs - base - 1u)); + IrU32 result(ir.Select(inRange, ir.Constant(0u), ir.GetScalarReg(static_cast(base)))); for (std::uint32_t reg = base + 1u; reg < NumScalarRegs; ++reg) { IrValue& hit = ir.IEqual(offset, ir.Constant(reg - base)); - result = IrU32(ir.Select(hit, ir.GetScalarReg(static_cast(reg)), result.Value())); + result = IrU32(ir.BitwiseOr(result.Value(), ir.Select(hit, ir.GetScalarReg(static_cast(reg)), ir.Constant(0u)))); } return result; } @@ -672,7 +673,7 @@ bool TranslationContext::sBfeU32(const RdnaInstruction& inst, bool sign) { return true; } -bool TranslationContext::sBfeU64(const RdnaInstruction& inst) { +bool TranslationContext::sBfeU64(const RdnaInstruction& inst, bool sign) { const IrU64 source = readU64(sourceAt(inst, 0u)); const IrU32 field = readU32(sourceAt(inst, 1u)); const IrU32 offset(ir.Emit(IrOpcode::BitFieldUExtract, IrType::U32, {&field.Value(), &ir.Constant(0u), &ir.Constant(6u)})); @@ -680,8 +681,16 @@ bool TranslationContext::sBfeU64(const RdnaInstruction& inst) { const IrU32 available(ir.ISub(ir.Constant(64u), offset.Value())); const IrU32 count(ir.Emit(IrOpcode::UMin32, IrType::U32, {&rawCount.Value(), &available.Value()})); const IrU64 shifted(ir.Emit(IrOpcode::ShiftRightLogical64, IrType::U64, {&source.Value(), &offset.Value()})); - const IrU64 mask = rightMask64(count); - const IrU64 result(ir.Emit(IrOpcode::BitwiseAnd64, IrType::U64, {&shifted.Value(), &mask.Value()})); + IrU64 mask = rightMask64(count); + IrU64 extended = shifted; + if (sign) { + const IrU32 discard(ir.BitwiseAnd(ir.ISub(ir.Constant(64u), count.Value()), ir.Constant(63u))); + const IrU64 aligned(ir.Emit(IrOpcode::ShiftLeftLogical64, IrType::U64, {&shifted.Value(), &discard.Value()})); + extended = IrU64(ir.Emit(IrOpcode::ShiftRightArithmetic64, IrType::U64, {&aligned.Value(), &discard.Value()})); + const IrU32 keep(ir.Select(ir.INotEqual(count.Value(), ir.Constant(0u)), ir.Constant(0xffffffffu), ir.Constant(0u))); + mask = IrU64(ir.ConstructU64(keep.Value(), keep.Value())); + } + const IrU64 result(ir.Emit(IrOpcode::BitwiseAnd64, IrType::U64, {&extended.Value(), &mask.Value()})); writeOperand(inst.destination, &result.Value()); ir.SetScc(ir.Emit(IrOpcode::INotEqual64, IrType::U1, {&result.Value(), &ir.ConstantU64(0)})); return true; @@ -873,6 +882,19 @@ bool TranslationContext::packB16(const RdnaInstruction& inst, bool high0, bool h return true; } +bool TranslationContext::vCvtPk16I32(const RdnaInstruction& inst, bool sign) { + const auto saturate = [&](std::uint32_t index) { + const IrU32 source = readU32(sourceAt(inst, index)); + if (sign) { + return IrU32(ir.Emit(IrOpcode::SMax32, IrType::U32, {&ir.Emit(IrOpcode::SMin32, IrType::U32, {&source.Value(), &ir.Constant(0x7fffu)}), &ir.Constant(0xffff8000u)})); + } + return IrU32(ir.Emit(IrOpcode::UMin32, IrType::U32, {&source.Value(), &ir.Constant(0xffffu)})); + }; + const IrU32 result = packU16Lanes(saturate(0u), saturate(1u)); + writeOperand(inst.destination, &result.Value()); + return true; +} + void TranslateIntegerInstruction(TranslationContext& context, const RdnaInstruction& instruction) { throw std::runtime_error("TranslateIntegerInstruction not implemented"); } diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp index 0207f3e53..6be88ccce 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp @@ -41,6 +41,23 @@ MemoryInfo imageMemoryInfoFromInstruction(const RdnaInstruction& inst) { return memory; } +IrOpcode imageAtomic64Opcode(IrOpcode opcode) { + switch (opcode) { + case IrOpcode::ImageAtomicSwap32: return IrOpcode::ImageAtomicSwap64; + case IrOpcode::ImageAtomicIAdd32: return IrOpcode::ImageAtomicIAdd64; + case IrOpcode::ImageAtomicISub32: return IrOpcode::ImageAtomicISub64; + case IrOpcode::ImageAtomicUMin32: return IrOpcode::ImageAtomicUMin64; + case IrOpcode::ImageAtomicUMax32: return IrOpcode::ImageAtomicUMax64; + case IrOpcode::ImageAtomicSMin32: return IrOpcode::ImageAtomicSMin64; + case IrOpcode::ImageAtomicSMax32: return IrOpcode::ImageAtomicSMax64; + case IrOpcode::ImageAtomicAnd32: return IrOpcode::ImageAtomicAnd64; + case IrOpcode::ImageAtomicOr32: return IrOpcode::ImageAtomicOr64; + case IrOpcode::ImageAtomicXor32: return IrOpcode::ImageAtomicXor64; + case IrOpcode::ImageAtomicCmpSwap32: return IrOpcode::ImageAtomicCmpSwap64; + default: throw std::runtime_error("64-bit " + std::string(IrOpcodeName(opcode)) + " is not implemented"); + } +} + } bool TranslationContext::imageBvhIntersectRay(const RdnaInstruction& inst) { @@ -72,17 +89,28 @@ bool TranslationContext::imageBvhIntersectRay(const RdnaInstruction& inst) { } bool TranslationContext::imageAtomic(const RdnaInstruction& inst, IrOpcode opcode) { + if (inst.dataBits == 64u) { + opcode = imageAtomic64Opcode(opcode); + } const MemoryInfo memory = imageMemoryInfoFromInstruction(inst); IrValue* resource = getImageResource(memory); IrValue* address = makeImageAddress(inst, inst.source0); const MemoryFlags flags = addMemoryInfo(memory, inst.programCounter); - const IrU32 value = readU32(inst.destination); IrValue& exec = ir.GetExec(); IrValue* result; - if (opcode == IrOpcode::ImageAtomicCmpSwap32 || opcode == IrOpcode::ImageAtomicFCmpSwap32) { + if (opcode == IrOpcode::ImageAtomicCmpSwap64) { + const IrU64 value = readU64(inst.destination); + const IrU64 comparator = readU64(offsetOperand(inst.destination, 2u)); + result = &ir.Emit(opcode, IrOpcodeType(opcode), {resource, address, &value.Value(), &comparator.Value(), &exec}, flags); + } else if (IsImageAtomic64Opcode(opcode)) { + const IrU64 value = readU64(inst.destination); + result = &ir.Emit(opcode, IrOpcodeType(opcode), {resource, address, &value.Value(), &exec}, flags); + } else if (opcode == IrOpcode::ImageAtomicCmpSwap32 || opcode == IrOpcode::ImageAtomicFCmpSwap32) { + const IrU32 value = readU32(inst.destination); const IrU32 comparator = readU32(offsetOperand(inst.destination, 1u)); result = &ir.Emit(opcode, IrOpcodeType(opcode), {resource, address, &value.Value(), &comparator.Value(), &exec}, flags); } else { + const IrU32 value = readU32(inst.destination); result = &ir.Emit(opcode, IrOpcodeType(opcode), {resource, address, &value.Value(), &exec}, flags); } if (inst.glc) { diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp index 5e0a602da..593cf1c43 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/MemoryInstructionDispatch.cpp @@ -473,6 +473,11 @@ bool TranslationContext::emitMemory(const RdnaInstruction& inst) { case RdnaOpcode::DsWrite2B64: case RdnaOpcode::DsWrite2st64B64: return dsWrite2(inst); + case RdnaOpcode::DsWrxchg2RtnB32: + case RdnaOpcode::DsWrxchg2st64RtnB32: + case RdnaOpcode::DsWrxchg2RtnB64: + case RdnaOpcode::DsWrxchg2st64RtnB64: + return dsWrxchg2(inst); case RdnaOpcode::DsWriteB8: case RdnaOpcode::DsWriteB16: case RdnaOpcode::DsWriteB16D16Hi: diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp index 11dc351fa..47327d850 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp @@ -42,6 +42,29 @@ bool TranslationContext::dsAtomic(const RdnaInstruction& inst, IrOpcode opcode, return true; } +bool TranslationContext::dsWrxchg2(const RdnaInstruction& inst) { + MemoryInfo first = sharedMemoryInfoFromInstruction(inst); + const std::uint32_t width = first.dataDwords / 2u; + if (width == 2u && inst.gds) { + throw std::runtime_error("64-bit GDS atomics are not supported"); + } + first.dataDwords = width; + first.componentCount = width; + MemoryInfo second = first; + second.offset = first.secondaryOffset; + const IrU32 address = readU32(inst.source0); + const auto data = [&](const RdnaOperand& operand) { return width == 2u ? &readU64(operand).Value() : &readU32(operand).Value(); }; + IrValue* firstValue = data(inst.source1); + IrValue* secondValue = data(inst.source2); + IrValue& active = ir.GetExec(); + const IrOpcode opcode = width == 2u ? IrOpcode::SharedAtomicSwap64 : IrOpcode::SharedAtomicSwap32; + IrValue& firstOld = ir.Emit(opcode, IrOpcodeType(opcode), {&address.Value(), firstValue, &active}, addMemoryInfo(first, inst.programCounter)); + IrValue& secondOld = ir.Emit(opcode, IrOpcodeType(opcode), {&address.Value(), secondValue, &active}, addMemoryInfo(second, inst.programCounter)); + writeOperand(inst.destination, &firstOld); + writeOperand(offsetOperand(inst.destination, width), &secondOld); + return true; +} + IrValue* TranslationContext::loadSharedU32(std::uint32_t width, IrU32 address, const MemoryInfo& memory, std::uint32_t pc) { IrOpcode opcode; switch (width) { diff --git a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp index 1a3051635..505d91545 100644 --- a/core/shader/recompiler/Translation/src/ScalarInstructions.cpp +++ b/core/shader/recompiler/Translation/src/ScalarInstructions.cpp @@ -381,7 +381,9 @@ bool TranslationContext::emitScalar(const RdnaInstruction& inst) { case RdnaOpcode::SBfeI32: return sBfeU32(inst, true); case RdnaOpcode::SBfeU64: - return sBfeU64(inst); + return sBfeU64(inst, false); + case RdnaOpcode::SBfeI64: + return sBfeU64(inst, true); case RdnaOpcode::SBitcmp0B32: return sBitcmpB32(inst, false); case RdnaOpcode::SBitcmp1B32: diff --git a/core/shader/recompiler/Translation/src/VectorInstructions.cpp b/core/shader/recompiler/Translation/src/VectorInstructions.cpp index cf769044f..1a60e2525 100644 --- a/core/shader/recompiler/Translation/src/VectorInstructions.cpp +++ b/core/shader/recompiler/Translation/src/VectorInstructions.cpp @@ -722,8 +722,9 @@ bool TranslationContext::emitVector(const RdnaInstruction& inst) { vPackB32F16(inst); return true; case RdnaOpcode::VCvtPkU16U32: + return vCvtPk16I32(inst, false); case RdnaOpcode::VCvtPkI16I32: - return packB16(inst, false, false); + return vCvtPk16I32(inst, true); case RdnaOpcode::VLshlrevB16: return integer16Shift(inst, IrOpcode::ShiftLeftLogical32, false); case RdnaOpcode::VLshrrevB16: diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md new file mode 100644 index 000000000..1a617546a --- /dev/null +++ b/docs/dev/ARCHITECTURE.md @@ -0,0 +1,69 @@ +# Architecture + +How a PS5 executable becomes a native Linux or Windows program. Nothing is emulated: the converted executable runs as a normal process and calls native implementations of the system libraries. + +## Overview + +```mermaid +flowchart LR + subgraph input["PS5 game"] + elf["input.elf"] + mods["sce_module/*"] + end + + subgraph relinker["relinker (core/relinker)"] + direction TB + intel["--to-intel:
lower AMD-only instructions
(codegen)"] + pipeline["RelinkerPipeline:
read imports by NID,
check syscalls, filter unused NIDs,
build SysV dynamic section"] + guest["GuestModuleBuilder:
convert bundled modules"] + patcher["LinuxElfPatcher / WindowsPePatcher"] + intel -.-> pipeline --> guest --> patcher + end + + subgraph build["build (core/libs)"] + direction TB + prx["core/libs/prx/*
shared libraries"] + nid["nid_patcher:
rename exports to their NIDs"] + prx --> nid + end + + subgraph out["Native program"] + app["app.elf / app.exe"] + app0["app0/sce_module/*"] + libs["libs/*.prx"] + end + + elf --> intel + mods --> guest + patcher --> app + guest --> app0 + nid --> libs + app -- "OS loader binds imports by NID" --> libs + app0 -- "OS loader binds imports by NID" --> libs +``` + +- [`core/relinker/main.cpp`](../../core/relinker/main.cpp) runs the steps in this order. `--to-intel` is optional, see [USAGE.md](../user/USAGE.md). +- Each library in [`core/libs/prx`](../../core/libs/prx) builds as a shared library. After the build, `nid_patcher` ([`core/libs/nid`](../../core/libs/nid)) renames every export to its NID, computed from the function name. `APS5_EXPORT("", func)` sets the NID directly when the name is unknown. + +## Graphics + +```mermaid +flowchart LR + game["Game:
command buffers"] --> submit["libSceAgcDriver/Submit
DCB / ACB"] + submit --> pm4["Execution/Pm4:
state, draws,
dispatches"] + pm4 -- "shader + state" --> cache{"Compiled variant
in memory or
on disk?"} + cache -- yes --> vk + cache -- no --> dec + + subgraph recompiler["core/shader/recompiler"] + dec["RdnaDecoder"] --> cf["ControlFlow:
graph + structurize"] + cf --> tr["Translation:
RDNA to IR"] + tr --> opt["Optimization:
SSA, resources,
bindings"] + opt --> spv["SpirvBackend:
emit SPIR-V"] + end + + spv --> vk["libSceAgcDriver/Graphics:
Vulkan pipeline"] +``` + +- [`Recompiler.cpp`](../../core/shader/recompiler/Recompiler.cpp) runs the stages in this order. With `ANYPS5_ENABLE_SPIRV_TOOLS`, the SPIR-V is also validated and optimized with SPIRV-Tools. +- `ShaderRecompiler::Recompile` keeps compiled variants in memory, and `ShaderDiskCache` stores them on disk so later runs reuse them. diff --git a/docs/dev/HW_ORACLE.md b/docs/dev/HW_ORACLE.md new file mode 100644 index 000000000..917efdc65 --- /dev/null +++ b/docs/dev/HW_ORACLE.md @@ -0,0 +1,64 @@ +# Hardware oracle + +`tools/hw-oracle/` runs a few lines of RDNA assembly on a local AMD GPU, one input row per lane, and returns what the +hardware computed. Use it to settle instruction semantics that the ISA documentation, LLVM or Mesa leave open, then pin the +measured rows as expected values in `tests/execution/`. + +The PS5 GPU is gfx1013. Other RDNA2 GPUs (gfx103x) share the VALU, SALU, DS, MUBUF and MIMG encodings, apart from ops that +exist only on gfx10.1 (`v_mad_legacy_f32`, `v_mac_legacy_f32`, `v_mul_lo_i32`, `s_dcache_discard`, ...). RDNA3 also runs +the tool, but differs from gfx1013 in more places. Say which GPU you measured on in the PR. + +## Requirements + +- Linux with the `amdgpu` driver and read/write access to `/dev/kfd` (usually the `render` group). Root is not needed. +- The ROCm HSA runtime with its headers: `libhsa-runtime-dev` (Debian, Ubuntu), `rocm-runtime-devel` (Fedora), + `hsa-rocr` (Arch), or AMD's ROCm packages. A runtime under `ROCM_PATH` or `/opt/rocm` is used first. The rest of ROCm + is not needed. +- A C compiler, and `clang` + `ld.lld` with the AMDGPU target (`llc --version` lists `amdgcn`). If they aren't in + `PATH`, they are taken from `$ROCM_PATH/llvm/bin`. + +The first run builds `oracle.c` into `~/.cache/anyps5-hw-oracle/` (`HW_ORACLE_CACHE` overrides it). The target is read +from the GPU (`HW_ORACLE_TARGET` overrides it). + +## Kernel + +`template.s` wraps the body under test. Per lane: + +- `v4`-`v7` hold the row's 4 input dwords. `v10`-`v25` start at 0 and are stored as the result (16 dwords). +- Leave `v0` (lane id), `v1` (input offset), `v2` (output offset) and `s[4:7]` (input and output addresses) unchanged. +- 4 KiB of LDS. Rows run in workgroups of up to 1024 lanes, padded to whole waves with zero rows. +- Bytes passed as `extra` follow the rows of each dispatch, at `s[4:5] + 16 * rows`: buffer contents, texels, etc. + Build buffer and image descriptors in SGPRs from that address. + +The FLOAT_MODE of the dispatch is set with `ieee` (0/1), `denorm32` and `denorm16` (0-3, 3 = denormals kept). Float +atomics need `coarse`: on fine-grained system memory they do nothing. + +## Use + +```sh +cat > body.s <<'EOF' + v_add_f16 v10, v4, v5 +EOF +printf '0x3c00 0x3c00 0 0\n0x7e01 0x3c00 0 0\n' > rows.txt +python3 tools/hw-oracle/hw_oracle.py body.s rows.txt --outs 1 --ieee 0 +``` + +prints one line per row with the output dwords in hex. From Python: + +```python +import sys +sys.path.insert(0, "tools/hw-oracle") +from hw_oracle import run + +rows = [(a, b, 0, 0) for a in values for b in values] +results = run(body, rows, wave64=True, denorm32=0) +``` + +Usual workflow: run the instruction on rows that separate the candidate models (edge values, NaN payloads, rounding +ties), write a model that matches every row, implement it, and put measured rows in the execution test. + +## Limits + +Only compute kernels. Pixel-shader ops that read their inputs from LDS also work: `v_interp_*` reads the attribute +parameters from LDS at `M0`, so writing them there by hand is enough. Behaviour that depends on the graphics pipeline +(parameter cache, exports, rasterization, `SPI_*` state) can't be measured with it. diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 6a9221f01..b8b01fa24 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -15,70 +15,103 @@ Throughout the project, every function at every stage either **does exactly what - [libSceMsgDialog.native](../../core/libs/prx/libSceMsgDialog.native/Export.cpp) - dialogs finish at `sceMsgDialogOpen` without UI and answer with button 1, so `sceMsgDialogClose` returns `NOT_RUNNING`; its result before `sceMsgDialogInitialize` follows libSceMsgDialog - [libSceCommonDialog](../../core/libs/prx/libSceCommonDialog/Export.cpp) - [libSceErrorDialog](../../core/libs/prx/libSceErrorDialog/Export.cpp) - the error dialog runs the state machine but shows nothing; the error code passed to `sceErrorDialogOpen` is only logged +- [libSceLoginDialog](../../core/libs/prx/libSceLoginDialog/Export.cpp) - the login dialog runs the common-dialog state machine but shows nothing and reports the user as having cancelled - [libSceHmd](../../core/libs/prx/libSceHmd/Export.cpp) implements only the disconnected-headset path: initialization succeeds, device queries report `NotDetected`, and opening a device returns `DeviceDisconnected`. Headset support, tracking, rendering and additional HMD exports are not implemented; SDK-level ABI compatibility and in-game behaviour remain unverified. - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) implements only the no-acoustic-propagation path: the system keeps a 16-byte header in the title's CPU memory (no GPU memory), rooms, portals, sources and materials are tracked handles, attributes are validated and have no effect, no rays are requested and sources report no audio paths. Ray results, path calculation and audio paths are accepted only when empty and throw otherwise; `sceAudioPropagationSourceRender` writes silence (zeroes) to each source's output buffer for the given size, without a dry signal. Objects still alive when their system is destroyed become unusable, and their later destroy or unregister is a no-op: PPSA21567 releases its reference-counted room, portal and source owners just before `sceAudioPropagationSystemDestroy`, so the order for every object is not proven. Occlusion, reflections and reverb from level geometry are absent; in-game behaviour remains unverified. - [libSceNpCommerce](../../core/libs/prx/libSceNpCommerce/Export.cpp) - the PS Store icon show/hide calls and `sceNpCommerceSetPsStoreIconLayout` do nothing -- [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick` and `sceSystemServiceReportAbnormalTermination` return success and do nothing: the host has no auto power-off timer to reset and no system crash reporter to notify +- [libSceSystemService](../../core/libs/prx/libSceSystemService/Export.cpp) - `sceSystemServicePowerTick`, `sceSystemServiceReportAbnormalTermination`, `sceSystemServiceDisableMusicPlayer` and `sceSystemServiceReenableMusicPlayer` return success and do nothing: the host has no auto power-off timer to reset, no system crash reporter to notify and no system music player to pause or resume - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) validates the priority and CPU affinity that the open param requests for the VideoOut service thread but does not apply them: the port's present and vblank threads are host threads, and guest priorities and affinities do not reach host scheduling -- The shader recompiler [skips baryctric coordinates](../../core/shader/recompiler/Recompiler.cpp) (is not even passed to SpirvTargetOptions at row 212). -- [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: the library has no bed or object exports to give it audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them +- Without `VK_KHR_fragment_shader_barycentric` the shader recompiler [does not load the barycentric VGPRs](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp): [`v_interp_p1_f32` is dropped and `v_interp_p2_f32` reads the attribute Vulkan interpolated](../../core/shader/recompiler/Translation/src/AttributeInstructions.cpp), so pixel shaders that use the barycentrics for anything other than `v_interp` get wrong values. +- [libSceAudio3d](../../core/libs/prx/libSceAudio3d/Export.cpp) - the port produces no sound: objects can be reserved but the library has no bed exports or `sceAudio3dObjectSetAttributes` to give them audio, so `sceAudio3dPortPush` only paces the queue at `granularity` samples per frame at 48 kHz. `sceAudio3dPortSetAttribute` accepts the late reverb level and the downmix spread attributes and ignores them - [libSceAvPlayer](../../core/libs/prx/libSceAvPlayer/Export.cpp): `sceAvPlayerSetLogCallback` accepts a callback that is never called, as the player produces no log messages, and `sceAvPlayerSetAvailableBandwidth` has no effect, as it governs HLS sources, which `sceAvPlayerAddSource` does not implement. - [libSceNpTrophy2](../../core/libs/prx/libSceNpTrophy2/src/GameInfo.cpp): `sceNpTrophy2RegisterUnlockCallback` accepts a callback that is never called, as no trophy is ever unlocked, and `sceNpTrophy2UnregisterUnlockCallback` only returns success. - [sceAvPlayerSetTrickSpeed](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) with a negative speed runs the clock backwards but delivers no frames; when a forward speed is set again, playback resumes from the rewound time. - [ulobjmgr](../../core/libs/prx/ulobjmgr/Export.cpp) registers no object: `_sceUlobjmgrRegisterObject` always hands out id 0 and `_sceUlobjmgrUnregisterObject` releases nothing, as shadPS4 does +- [libSceHttp](../../core/libs/prx/libSceHttp/Export.cpp) - no request reaches the network, so `sceHttpSetResponseHeaderMaxSize` has no response header to limit and `sceHttpRedirectCacheFlush` no redirect to forget; `sceHttpsUnloadCert` returns success like `sceHttpsLoadCert`, which keeps no certificate ### Unknown function info +- PPSA01341 imports declared without parameters, signatures unknown: [sceAgcSetSemaphoreMemory](../../core/libs/prx/libSceAgc/Unimplemented.cpp), [sceAgcDriverRegisterMultipleResources](../../core/libs/prx/libSceAgcDriver/Unimplemented.cpp). Names from the shadPS4 aerolib NID list +- [7CxI50-xlCk, pMxXhNozUX8](../../core/libs/prx/libSceNpPartner001/Export.cpp) (libSceNpPartner001) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [sceAgcWaitRegMemPatchMask](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - follows `sceAgcWaitRegMemPatchReference`: the mask is taken as a 32-bit value and written to the low mask word of a 32- or 64-bit wait, as [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) does; [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.PacketPatching.cs) writes both words of a 64-bit wait's mask. A mask above 32 bits throws +- [0GAw7SmkwII, 1ic5q-kdOsc, 5z2gBlqxJ+0, Kyy1baXgaVU, ZLL31lzzxr4, gMduXCLYrNg, lrJwpLjKXRc, m9JLPc3wOQw, o+NM86gEwFE](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_mfsr2) - unknown names and signatures, imported by PPSA23566; declared without parameters +- [sceAgcDcbSetCfRegisterDirect, sceAgcDcbSetCfRegisterRangeDirect](../../core/libs/prx/libSceAgc/DcbState/src/Registers.cpp) (libSceAgc) - signatures follow [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs), which agree; the SET_CONFIG_REG (0x68) packet is OpenAGC's. The driver does not execute SET_CONFIG_REG, so a submitted packet throws +- PPSA23566 imports declared without parameters, signatures unknown: [sceAudioOut2UserGetSupportedAttributes](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp), [sceConvertKeycodeGetCharacterFromKeyboardData](../../core/libs/prx/libSceConvertKeycode/Export.cpp), [sceHttpSetCookieRecvCallback, sceHttpSetRedirectCallback, sceHttpsGetSslError, sceHttpsSetSslVersion](../../core/libs/prx/libSceHttp/Export.cpp), [sce::Json::MemAllocator::notifyError, sce::Json::Value::clear](../../core/libs/prx/libSceJson2/Export.cpp), [sceNetResolverAbort](../../core/libs/prx/libSceNet/Export.cpp), [sceNpCommerceDialogOpen2](../../core/libs/prx/libSceNpCommerce/Export.cpp), [sceNpEntitlementAccessGetPftFlag](../../core/libs/prx/libSceNpEntitlementAccess/Export.cpp), [sceProprietaryVoiceChatHelper{Initialize,Terminate,SetVoiceChatState,GetVoiceChatUsageState}](../../core/libs/prx/libSceProprietaryVoiceChatHelperPrx/Export.cpp), [sceVoice{GetResourceInfo,ResetPort,EnableChat,DisableChat}](../../core/libs/prx/libSceVoice/Export.cpp). Names from the shadPS4 aerolib NID list - [sceAvPlayerAddSource](../../core/libs/prx/libSceAvPlayer/src/Source.cpp) (libSceAvPlayer) - requests up to 4 additional decode-ahead video framebuffers from the memory replacement beyond `num_output_video_framebuffers` when memory is available; the console's internal buffering behaviour is unknown +- [sceAgcDcbClearState](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - the (buffer, command) signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Agc/SceAgc.cs) and [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c), and the 2-dword CLEAR_STATE (0x12) packet is OpenAGC's; OpenAGC masks the command to 4 bits, here a command above 0xf throws, as the driver rejects that payload - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - buffer option 8 (STRICT_COLORIMETRY) is presented as option 0, with the stored values unchanged; what the console changes for it is unknown. Only options 0 and 8 are accepted +- [sceAgcGetRegisterDefaultsInternal](../../core/libs/prx/libSceAgc/Misc/src/RegisterDefaults.cpp) (libSceAgc) - returns the version 0 internal table, as `sceAgcGetRegisterDefaults` returns the version 0 public one; [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) returns the table of the version passed to `sceAgcInit` instead, and null before it - [sceVideoOutRegisterBuffers2](../../core/libs/prx/libSceVideoOut/src/Buffer.cpp) (libSceVideoOut) - DCC buffers (category 1): `dcc_control` is taken as the CB_DCC_CONTROL block layout, only bits 0x10026c are accepted; `dcc_cb_register_clear_color` as the register-clear texel, only 32-bit values are presented +- [sceVideoOutIsOutputSupported](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - output mode encoding unknown: every mode except the default reports not supported (0), and `sceVideoOutConfigureOutput` returns `VIDEO_OUT_ERROR_UNAVAILABLE_OUTPUT_MODE` for it, instead of returning `VIDEO_OUT_ERROR_UNSUPPORTED_OUTPUT_MODE` for values the console does not define. PPSA12544 probes `0xd000000a` and selects 7680 pixels when it is supported, 3840 otherwise - [sceVideoOutOpen](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - the open param's first word is unknown (PPSA21564 passes 16; it is not the byte size, since the affinity mask is at offset 16); only 16 is accepted. Whether the param continues past offset 24 is also unknown -- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`); a string without a zone is taken as UTC, and anything outside this shape throws instead of returning a parse error, unverified against hardware +- [s_setkill](../../core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp) (core/shader/recompiler) - decoded as `s_endpgm` for any immediate: measured on RDNA2 hardware (gfx1035), a store before it is visible and nothing after it runs ([#771](https://github.com/boykopovar/AnyPS5/pull/771)); whether the KILL status bit it sets is observable differently from `s_endpgm` is unverified +- [sceRtcParseDateTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - accepted format assumed to be the ISO 8601 subset `YYYY-MM-DDTHH:MM:SS[.ffffff]` (T/t/space separator, optional fractional seconds) with the sibling RFC 3339 zone suffix (`Z` or `±HH:MM`), plus the RFC 2822 shape `Ddd, DD Mon YYYY HH:MM:SS[ GMT| ±HHMM]` and the asctime shape `Ddd Mon DD HH:MM:SS YYYY` (day zero- or space-padded) that shadPS4 parses at fixed positions; the weekday name is not checked against the date, a `±HHMM` offset is subtracted to get UTC like the RFC 3339 path (shadPS4 adds it), a string without a zone is taken as UTC, and anything outside these shapes throws instead of returning a parse error, unverified against hardware - [sceAudio3dPortOpen](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - buffer modes 0 and 1 (also selected by the 0x10 and 0x18 parameter sizes) and 3 beds depend on an SDK version check in the module ([prosper](https://github.com/mattias800/prosper)) and throw. A repeated `sceAudio3dInitialize` (`NOT_READY` in shadPS4, 0 in prosper) and `sceAudio3dPortAdvance` on a full queue (`NOT_READY` in shadPS4, unhandled in KytyPS5) throw too +- [sceAudio3dObjectReserve](../../core/libs/prx/libSceAudio3d/Export.cpp) (libSceAudio3d) - object ids, the `max_objects` limit and the checks of `sceAudio3dObjectReserve` and `sceAudio3dObjectUnreserve` follow [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4), the only reference with a body: ids count up from 1 per port, skipping 0, 0xFFFFFFFF and ids in use, and a failed reserve writes 0xFFFFFFFF - [AMPR WriteAddressFromCounterPairOnCompletion](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - the pair of an even counter n is assumed to give counter n in the low 32 bits and n + 1 in the high 32 bits. Wait compare values, wait flush and counter index ranges follow the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation; the flush is accepted and has no effect, as commands run in order - [AMPR command sizes](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - commands use the emulator's own encoding with an 8-byte header, so `Nop(n)` takes 8 + 4n bytes where the console record is n dwords including its header (`NopWithData(n)`: (n + 1) dwords). The measure functions agree with the appended sizes, but a title that pads to an exact offset or the end of the buffer with `Nop(remaining / 4)` gets `SCE_AMPR_ERROR_BUFFER_FULL` or a shifted offset. The at-start flag of the `_04_00` writes is ignored; commands run in order, each after the previous one completes - [AMPR counter access](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and checks of `WaitOnCounter_04_00`, `WriteCounter_04_00`, `ConstructNop` and `ConstructMarker` taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands counter commands to the console and does not show their effect. The counters are taken as 32 bits each: the 8-byte access of counter n spans n (low) and n + 1 (high), the 2- and 1-byte accesses select a half or a byte of the counter by its offset, waits compare at the access width (signed and wrapped compares included), the AND mask applies to both sides and the atomic writes wrap within the access. The `ConstructNop` type is not recorded - [APR gather and scatter reads](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, argument checks and the read cursor taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: each read leaves the file, the next file offset and the next destination; a gather reads that file at a new offset into the next destination, a scatter continues in the file into a new destination, a gather-scatter takes both. Recording one needs a read file since the last `ResetGatherScatterState`; like ampr_emu, `sceAmprCommandBufferReset` keeps that state, and a gather or scatter with no read before it in the submitted buffer throws instead of returning `SCE_AMPR_ERROR_APR_INVALIDGATHERSCATTERSTATE` - [AMM map, map direct and unmap](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection mask (CPU, GPU, AMPR and ACP read and write) taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, which hands the remaining checks and the mapping to the console kernel; the mapper model taken from [PS5PCEM](https://github.com/iStark/PS5PCEM): `sceAmprAmmGiveDirectMemory` usage 1 gives the allocated direct memory to the pool plain maps take pages from, usage 0 only allocates it. Not confirmed on a title. The AMM virtual address ranges are a 32 GiB range and a 32 GiB multimap range reserved in the guest arena on first use, not the console's; maps outside them throw. Addresses, sizes and direct offsets must be 16 KiB aligned. The memory type and the GPU mask are ignored, AMPR and ACP access maps as CPU access, unmapped pages go back to the pool and stay reserved. Submission runs the buffer before returning, the submit result is always 0 and a failing command throws; the AMM measures return errors sign-extended, as ampr_emu does. `sceAmprAmmGetVirtualAddressRanges` throws for a null output - [AMM remap, multimap and protection changes](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures and the protection and mask checks taken from ampr_emu; the effects are inferred from the names, not confirmed on a title. A remap moves every page of a fully mapped range to the new address with the given protection and leaves the old range reserved; a multimap maps the same pages at a second address, anywhere in the AMM ranges, and a pool page goes back to the pool once its last mapping is unmapped; a multimap onto its own source range throws. A protection change sets the masked bits on every page of a fully mapped range; the memory type is recorded and ignored. A command on a range that is not fully mapped throws +- [sceVideodec2GetAvcPictureInfo](../../core/libs/prx/libSceVideodec2/Export.cpp) (libSceVideodec2) - runs the `sceVideodec2GetPictureInfo` body (KytyPS5 binds both NIDs to one function; SharpProspero gives both the same `outputInfo, first, second` signature); the second (bottom field) picture info is never written, since only progressive H.264 output is modelled. prosper calls the AVC layout unestablished (no title observed calling it) - [AMM PRT ranges](../../core/libs/prx/libkernel/Apr/src/Apr.cpp) (libSceAmpr) - signatures, checks and their order taken from ampr_emu, where `MapAsPrt` is a map with the PRT flag and protection 0 and `AllocatePaForPrt` a memory type and protection change with mask 1019, both checking the buffer memory after its size; the effects are inferred from the names, not confirmed on a title. Unbacked PRT pages are zero-filled pages with GPU read access, so CPU reads see zeros instead of faulting and GPU writes fault instead of being dropped. `AllocatePaForPrt` backs each unbacked page with a pool page and sets the type and protection of backed ones; `RemapIntoPrt` moves a fully mapped range into a PRT range, its opcode argument (1011 when 0, as in ampr_emu) is recorded and ignored; `UnmapToPrt` returns the pages to the pool and zero-fills them again. A plain map, unmap or remap over a PRT range ends it; `AllocatePaForPrt`, `RemapIntoPrt` or `UnmapToPrt` outside a PRT range throws - [APR map begin and end](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - as in ampr_emu, `MapBegin`/`MapDirectBegin` map when they run and `MapEnd` only closes the region; while a region is open a second begin and every completion write return `EPERM`, the at-start `_04_00` writes are accepted - [AMPR markers](../../core/libs/prx/libSceAmpr/Export.cpp) (libSceAmpr) - signatures taken from the [ampr_emu](https://github.com/drakmor/ampr_emu) reimplementation, not confirmed on a title: `sceAmprCommandBufferSetMarkerWithColor` takes the color by pointer, the push and measure variants by value. Colors are not recorded - [sceFontGetRenderScaledKerning](../../core/libs/prx/libSceFont/src/Render.cpp) (libSceFont) - arguments, the cleared output and the bound renderer check taken from `cellFontGetRenderScaledKerning` in [RPCS3](https://github.com/RPCS3/rpcs3/blob/master/rpcs3/Emu/Cell/Modules/cellFont.cpp) (PS3 predecessor of the library), not confirmed on a title. The kerning is the `sceFontGetKerning` one at the render scale - [sceFontGetPixelResolution](../../core/libs/prx/libSceFont/src/Library.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title. The value is the one the library's driver reports (64 for FreeType); the argument checks and the cleared output follow `sceFontGetLibrary` +- [sceFontGetFontGlyphsCount, sceFontGetCharGlyphCode](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signatures taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and matching the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The count is the face's glyph count and the glyph code its glyph index, as the library's driver reports them; a code with no glyph returns `NO_SUPPORT_GLYPH` and code 0 `NO_SUPPORT_CODE`, as `sceFontRenderCharGlyphImage` does. The argument checks and the cleared output follow `sceFontGetPixelResolution` +- [sceFontGetFontResolution](../../core/libs/prx/libSceFont/src/Open.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json), not confirmed on a title; the outputs are inferred from the names. The resolution is the face's units per EM and the scale pixel the driver's face scale (units per EM / 64 for FreeType), the pixel scale a font has when opened. Either output may be null, as in `sceFontGetResolutionDpi` - [sceFontCreateWritingLine, sceFontWritingLineWritesOrder](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signatures and the `SceFontWritingLineStep`/`SceFontCreateWritingLineDetail` layouts (detail id `0x0FD5`) taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFontStructs.cs), not confirmed on a title; the meaning of the 64-bit writing attribute is unknown +- [sceFontWritingLineGetOrderingSpace](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - signature taken from the IDA type database of [Orbital](https://github.com/AlexAltea/orbital/blob/master/tools/ida/db_types.json) and the [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Font/SceFont.Engine.cs) bindings, not confirmed on a title. The modelled line applies no spacing, so the head, inline, tail and advance spaces are 0; what spacing the console applies is unknown. A null output throws - [sceSaveDataTransferringMountPs4, sceSaveDataDirNameSearchPs4](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - arguments assumed to be those of `sceSaveDataTransferringMount` and `sceSaveDataDirNameSearch`: KytyPS5 binds both NIDs to the PS5 functions, and [prosper](https://github.com/mattias800/prosper) captured the same result layout on a title for the search. There is no PS4 save area, so the mount returns `NOT_FOUND` and the search reports zero hits +- [sce::Json::Parser::parse](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - a number that overflows a double (e.g. `1e309`) throws: whether the console rejects the document or stores an infinite real is unknown +- [sceUserServiceGetNpAccountId](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the account id for a user with no linked PSN account is assumed to be 0 with success; libSceNpManager's `sceNpGetAccountIdA` answers the same question with `SCE_NP_ERROR_SIGNED_OUT` +- [sceUserServiceGetUserColor](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the default profile colour is assumed to be blue (0) +- [kP2L8t3j-aM](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, signature +- [LibwuIonIBw](../../core/libs/prx/libSceVideoOut/src/Output.cpp) (libSceVideoOut) - unknown name, signature - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted +- [User data registers](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Diagnostics.cpp) (AGC driver) - a `SPI_SHADER_USER_DATA_*` or `COMPUTE_USER_DATA_*` register the title never wrote reads as 0 when a stage's RSRC2 count covers it, as Kyty (zeroed `UserSgprInfo`) and SharpEmu (missing registers read 0) do, and as AMD PAL and Mesa initialize them; the console's value is not confirmed - [SET_PREDICATION](../../core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp) (AGC driver) - boolean predication follows the AMD PM4 encoding [Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/common/sid.h) uses: operation 3 reads 64 bits and 4 reads 32 bits, and bit 8 runs predicated packets when the value is non-zero (clear: when it is zero); the bit 12 hint is ignored. The address must have its low 4 bits clear, as `sceAgcDcbSetPredication` writes it. Query predication (Z-pass, primitive count), a predicated COND_EXEC, predicated command buffer chains and flips in predicated command buffers throw - [sceAgcGetGsOversubscription](../../core/libs/prx/libSceAgc/Shader/src/GsOversubscription.cpp) (libSceAgc) - signature and occupancy formula (base and expanded vertex/export capacities, budget shift, `GE_PC_ALLOC` / `SPI_SHADER_PGM_RSRC4_GS` encoding) follow KytyPS5 only, not confirmed on hardware or a title; a negative or NaN factor that gives a negative target, a missing GS context register or a zero `GE_MAX_OUTPUT_PER_SUBGROUP` throws +- [sceAgcCbCondWrite](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) (libSceAgc) - argument order and the 9-dword COND_WRITE layout follow the clean-room [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [sharpemu](https://github.com/sharpemu/sharpemu/blob/main/src/SharpEmu.Libs/Agc/AgcExports.CommandPackets.cs), which agree on them; SharpProspero lists another order. The write space is accepted only as 1 (memory), the one value both encode the same way, and other values throw. - [sceAgcGetIsTrinityMode](../../core/libs/prx/libSceAgc/Misc/src/Platform.cpp) (libSceAgc) - signature from the only PPSA26344 call (a pointer to a one-byte flag, result ignored); a null pointer throws because its error code is unknown - [zARR5aCmkoY](../../core/libs/prx/libSceAgc/DcbFlow/src/Control.cpp) (libSceAgc) - unknown name, signature - [qj7QZpgr9Uw](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - unknown name - [sceAgcDcbContextStateOpGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - sizes from KytyPS5 only (5, 27, 27 and 32 dwords), equal to what `qj7QZpgr9Uw` writes; KytyPS5 returns 0 for an operation above 3, here it throws +- [sceAgcWriteDataPatchSetDst, sceAgcWriteDataPatchSetCachePolicy and their Async forms](../../core/libs/prx/libSceAgc/Patch/src/WaitRegMem.cpp) (libSceAgc) - the signatures `(packet, uint8 value)` are inferred from the other patch functions, as no public declaration was found; the value is taken in the encoding of the `dst` and `cachePolicy` arguments of `sceAgcDcbWriteData` (graphics) and `sceAgcAcbWriteData` (Async, compute), and the patched packet is the one those calls would write. `sceAgcAsyncWriteDataPatchSetAddressOrOffset` is assumed to patch the address like `sceAgcWriteDataPatchSetAddressOrOffset`, as the compute WRITE_DATA packet keeps it in the same dwords - [fd5Bp5tGTgo](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name - [dolOmWH+huQ](../../core/libs/prx/libSceAgc/Misc/src/ShaderFusion.cpp) (libSceAgc) - unknown name - [dbOlWdppb4o](../../core/libs/prx/libSceAgc/Shader/src/InterpolantMapping.cpp) (libSceAgc) - unknown name; same arguments as `sceAgcCreateInterpolantMapping`; the `SPI_PS_INPUT_CNTL` bits for `is_f16` 2 inputs follow KytyPS5, not confirmed on hardware - [V++UgBtQhn0](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - unknown name - [sceAgcGetDataPacketPayloadRange](../../core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp) (libSceAgc) - signature, the `{base, size in bytes}` output and the type 0 range (header + 1, one dword longer than the type 1 payload) from KytyPS5 only, matching `V++UgBtQhn0`; prosper returns header + 2 for both types from that address function +- [sceAgcAsyncCondExecPatchSetCommandAddress, sceAgcAsyncCondExecPatchSetEnd](../../core/libs/prx/libSceAgc/Patch/src/CondExec.cpp) (libSceAgc) - no reference has a body for them; they are assumed to patch the COND_EXEC packet like `sceAgcCondExecPatchSetCommandAddress` and `sceAgcCondExecPatchSetEnd`, since `sceAgcAcbCondExec` writes the same packet as `sceAgcDcbCondExec`, as `sceAgcAsyncRewindPatchSetRewindState` does for REWIND - [gQkqkLttcpw](../../core/libs/prx/libSceAgc/Acb/src/Control.cpp) (libSceAgc) - unknown name, signature - [sceAgcDcbPrimeUtcl2](../../core/libs/prx/libSceAgc/DcbState/src/Display.cpp) (libSceAgc) - assumed to write the packet `sceAgcAcbPrimeUtcl2` writes: PRIME_UTCL2 has the same 5 dwords on the graphics and compute rings. Not confirmed on a title. Both write a NOP of that size, as there is no TLB to prime +- [sceAgcDcbQueueEndOfShaderActionGetSize](../../core/libs/prx/libSceAgc/DcbState/src/ContextState.cpp) (libSceAgc) - the builder is inline in the SDK and not exported; the size is assumed to be that of the RELEASE_MEM packet `sceAgcCbReleaseMem` writes (32 bytes), the same as `sceAgcAcbQueueEndOfShaderActionGetSize`, as in [OpenAGC](https://github.com/OpenAGC/OpenAGC/blob/main/src/game_compat.c) and [REmu](https://github.com/xnetcat/REmu/blob/main/crates/remu-runtime/src/agc_dcb.rs) - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature +- [sceKernelGetModuleInfoFromAddr](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x1A8-byte info layout follows the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation; a title's `libc.prx` reads only `id` (PPSA14632). The meaning of `flags` is unknown; only 2 is accepted, as is only the full `st_size`. The id is the `sceKernelLoadStartModule` handle of an image opened through the loader and otherwise a stable id from the same range. The name is the host file name without `.guest.prx`. An image with more than 4 load segments (the converted executable has 7) reports its first 4. `tls_offset` is 0 and `ref_count` is 1, as the host loader exposes neither. Not implemented on Windows +- [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified +- [Memory protection bits 0x100 and 0x200](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits - [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored - [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed +- [sceKernelAioSubmitReadCommandsMultiple / sceKernelAioSubmitWriteCommandsMultiple / sceKernelAioWaitRequests / sceKernelAioCancelRequest / sceKernelAioCancelRequests / sceKernelAioDeleteRequests](../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) - behaviour from shadPS4 (one id per request for the Multiple submits, cancel turns any request aborted and id 0 reports processing, wait mode 2 returns once one request completed); the 128 requests or ids per batch limit is from SharpProspero, with an unknown error, so larger batches throw; wait modes other than 1 (and) and 2 (or) throw; null pointers, negative counts and invalid ids return the codes the single-request siblings use, checked before any state is written - [sceKernelGetAvailableCpumask](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) - returns the default thread affinity, 0x1FFF (CPUs 0 to 12), as KytyPS5 does. Titles pin threads within it (PPSA02664 passes 0x1FFB, Hades II PPSA36082 0x3 and 0x3F); whether CPU 13 is available to a title is not confirmed - [sceKernelMapNamedFlexibleMemoryInternal](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - flag 0x8000 unknown; only the sceKernelMapNamedFlexibleMemory flags are accepted +- [sceKernelMtypeprotect](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - sets the memory type of the direct mappings in the page-rounded range and of the direct memory behind them, as shadPS4 and prosper do (KytyPS5 ignores the type); the type is not validated, flexible pages in the range only get the protection, and a later map of the retyped direct memory reports the type it was allocated with. `sceKernelBatchMap2` operation 4 takes the same path with the entry type - [Guest arena](../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature - [sceLibcInternalHeapErrorReportForGame](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - unknown signature -- [std::_Fiopen](../../core/libs/prx/libc/src/CxxAbiSupport.cpp) (libc) - `_ZSt7_FiopenPKcNSt5_IosbIiE9_OpenmodeEi` (NID `vYWK2Pz8vGE`; the name comes from the public NID list): the values of the `_Openmode` argument and the meaning of the protection argument are unknown, so it throws when called. It is exported so that a title that imports it (PPSA21564 and PPSA21567 do) loads against the host libc when its own `libc.prx` is left out of the guest conversion. - [_sceLibcInternalThreadAtexit, _sceLibcInternalThreadDtors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - FreeBSD `__cxa_thread_atexit` semantics assumed; a null or non-image destructor and an allocation failure throw +- [_sceKernelRtldThreadAtexitIncrement, _sceKernelRtldThreadAtexitDecrement](../../core/libs/prx/libkernel/System/src/Rtld.cpp) (libkernel) - the argument is the `dso` address a title's `libc.prx` passes to `__cxa_thread_atexit` (PPSA14632), which fails when Increment returns nonzero. As in the [fpPS4](https://github.com/red-prig/fpPS4) reimplementation, Increment takes a reference on the loaded image containing the address and Decrement releases one; the variants without the leading underscore are assumed to match - [_sceLibcInternalForceTlsDestructor](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) - unknown return type and behaviour; a title's `libc.prx` calls it from `__cxa_finalize` with the module handle of a non-null dso +- [std_execute_once](../../core/libs/prx/libc/Export.cpp) (libc) - assumed to be the plain-named twin of `std::_Execute_once` (`_ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1_`, the callee of Dinkumware `call_once`): it delegates there and gets the [thread.once.callonce] semantics already verified for that symbol (one invocation per flag, concurrent callers block until it completes, and a failing or throwing invocation leaves the flag unready so the next caller runs it). The name equivalence is not confirmed on a title, and the callback convention (called with (null, arg, null), non-zero return meaning success) is the twin's, unverified for this symbol - [__progname](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - unknown data export -- [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED throws: FreeBSD 9.0 libthr rejects it with EINVAL and 11.0 accepts it, and which one the console follows is unknown +- [pthread_barrierattr_setpshared](../../core/libs/prx/libkernel/Pthread/Posix/Barrier.cpp) (libkernel) - PTHREAD_PROCESS_SHARED is accepted on the assumption that the console follows FreeBSD 11.0 libthr, which accepts it (9.0 rejects it with EINVAL); unverified on hardware. Guest code runs in one process, so a shared barrier behaves as a private one - [sceSslClose](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [sceSslGetSerialNumber](../../core/libs/prx/libSceSsl/Export.cpp) (libSceSsl) - unknown signature - [X+4jdIS75P0](../../core/libs/prx/libSceAudioIn/Export.cpp) (libSceAudioIn) - unknown name, signature -- [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the only known call sets just edi and esi; only `(0xFF, 2)` is accepted +- [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) (libSceAudioOut) - assumed to take `(user_id, latency)`: KytyPS5 reads `(user_id, output, latency_us)`, but the known calls set just edi and esi; only `(0xFF, 2)` (PPSA26344) and `(0xFF, 1)` (PPSA14632) are accepted - [sceAudioOut2MasteringInit](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) - flag values unknown; only 0 is accepted - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - effect of a negative mix level unknown; only 0 to 32768 (0 dB) is accepted - [sceAudioOutSetMixLevelPadSpk](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - no source applies the level (shadPS4 only stores it); it is assumed to be a linear gain on the port's own output, and the -9 dB default (11626) is applied to every pad speaker port, including those of titles that never call the function @@ -89,61 +122,121 @@ Throughout the project, every function at every stage either **does exactly what - [scePsmlMfsrGetContextBufferRequirement1100, scePsmlMfsrCreateContext1100, scePsmlMfsrGetDispatchMfsrPacket1100](../../core/libs/prx/libScePsml_debug/Export.cpp) (libScePsml_debug) - argument layouts unknown; only 0x8A810001 (not initialized) is returned - [BnMAMrsfVWo](../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libc) - unknown name, signature - [scePngEncEncode](../../core/libs/prx/libScePngEnc/Export.cpp) (libScePngEnc) - whether rows may stay unfiltered when `filter_type` names a subset of the filters is unknown; as in shadPS4, they may only for 0 and for the all-filters mask +- [sceJpegEncEncode](../../core/libs/prx/libSceJpegEnc/Export.cpp) (libSceJpegEnc) - `restart_interval` follows SharpProspero's field doc (0 none, -1 every row of blocks, positive a block count); a positive value is taken as the JPEG DRI interval in MCUs and -1 as one restart per MCU row, which no reference confirms. Other negative values throw - [sceVoiceQoSInit](../../core/libs/prx/libSceVoiceQoS/Export.cpp) (libSceVoiceQoS) - the error codes are unknown: a null or empty memory block, an app type other than 0x20000000 (PPSA14632) or 0x10000000 (PPSA26344) and a second initialization throw. The minimum memory size is unknown; the app type and the block are not used, as no endpoint is implemented -- AudioIn, NpSessionSignaling and PlayerInvitationDialog exports added without an implementation have assumed signatures +- AudioIn and NpSessionSignaling exports added without an implementation have assumed signatures - [vieBRwlh1Lw](../../core/libs/prx/libSceAgc/Unimplemented.cpp) (libSceAgc) - unknown name, signature - [fCWdlnmB1Ks](../../core/libs/prx/libScePad/Export.cpp) (libScePad) - unknown name, signature - [sceKernelGetOperationMode](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) - the signature `(int* mode, int* submode)` comes from PPSA12544, which logs both values after the call; the values are unknown and 0 is reported for both, as in the [prosper](https://github.com/mattias800/prosper/pull/4153) reimplementation. Null outputs throw - Font, Http2, Net, Ssl, SystemService and libkernel exports imported by PPSA12544 and added without an implementation have unknown signatures +- [libSceVrSetupDialog](../../core/libs/prx/libSceVrSetupDialog/Export.cpp), and the Share, NpTrophy2, NpEntitlementAccess and WebBrowserDialog exports imported by PPSA12544's Unity plugins and added without an implementation, have unknown signatures +- [Z4HDpe1ZEa4, oXShHcqMcX0](../../core/libs/prx/libSceAjm.native/Export.cpp) (libSceAjmWVorbis, implemented in libSceAjm.native) - unknown name, signature +- Acm, GameLiveStreaming, Net, NpEntitlementAccess, NpManager, NpWebApi2, VoiceQoS, WebBrowserDialog and libkernel exports imported by PPSA14632 and added without an implementation are declared without parameters - [libSceAudioPropagation](../../core/libs/prx/libSceAudioPropagation/Export.cpp) (libSceAudioPropagation) - signatures and struct descriptors recovered from PPSA21567's calls (#481), not from documentation. The system options layout is unknown (only checked for non-null); the fourth argument of `sceAudioPropagationSourceCalculateAudioPaths` is unknown; `sceAudioPropagationSourceGetAudioPath` is assumed to output an 8-byte path handle. The meaning of the `RenderInfo` word at +0x28 is unknown (PPSA21567 and PPSA21564 pass 2); only 2 is accepted. Both titles pass one `RenderInfo` per call: a count of 0 throws, and larger counts are handled element by element without proof. No error code is known, so invalid input throws instead of returning one +- [NGS2 mastering voice gain](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - the param id 0x30000004 is taken from PPSA22520 and the layout from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2MasteringVoiceGainParam`; no reference gives the units or range. The levels are assumed to be linear factors applied when the mastering voice is mixed into the render buffer, so the voice's own samples stay unscaled; the LFE level applies to channel 3 of 6- and 8-channel voices, assuming the L R C LFE order of the AudioOut2 entries above, and the full-band level to the rest. Negative levels are accepted, and a new setup resets both levels to 1 +- [NGS2 stereo mastering voice into a surround buffer](../../core/libs/prx/libSceNgs2.native/src/Render.cpp) (libSceNgs2.native) - PPSA21402 renders a 2-channel mastering voice into a 6- or 8-channel render buffer, which the mixing stage rejected. The console's conversion is unknown: the left and right channels are assumed to go to the first two buffer channels (front left and right) and the remaining channels stay silent, with no centre, LFE or surround content. Any other channel count mismatch still throws - [sceAvPlayerStartEx](../../core/libs/prx/libSceAvPlayer/Export.cpp) (libSceAvPlayer) - start info layout unknown; it is ignored and playback starts as with `sceAvPlayerStart` - [sceAvPlayerInit / sceAvPlayerInitEx](../../core/libs/prx/libSceAvPlayer/src/Player.cpp) (libSceAvPlayer) - behaviour without a memory replacement unknown; frame and sample buffers then come from the guest heap - [SceAvPlayerVideoEx](../../core/libs/SceTypes.hpp) (libSceAvPlayer) - frame rate field and encoding unknown; it is left zero in frame and stream info +- [sceRazorCpuFlushOccurred](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signature and return values (1 = flush since the last call, 0 = none) from [SharpProspero](https://github.com/SvenGDK/SharpProspero); without a capture nothing is flushed, so it returns 0 and writes 0 cycles to the optional out pointer (whether the real library writes it when nothing was flushed is unknown) - [sceRazorCpuPushMarkerStatic, sceRazorCpuPopMarker](../../core/libs/prx/libSceRazorCpu/Export.cpp) (libSceRazorCpu) - signatures from PPSA26344's calls; (name, color, flags) assumed for the push; both do nothing without a capture +- [sceAjmBatchJobGetInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - writes the result and format sideband of a run job without buffers with the `SIDEBAND_FORMAT` flag (0x400000000000), as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does, not confirmed on a PS5 title; the format reports bitrate 0 for ATRAC9 as in KytyPS5, while [shadPS4](https://github.com/shadps4-emu/shadPS4) derives it from the superframe size; before the instance is initialized it returns the not-initialized result (1) and no format, like the other run jobs here, while KytyPS5 returns result 0 with the format of the uninitialized decoder - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - signature, result layout and rate tables taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title: the layer field is not checked and MPEG-2.5 stops at 64 kbps; the original file length (`parse_ofl`) layout (Xing/Info + LAME tag, VBRI, Fraunhofer `0xB4` block with its CRC, encoder delay = samples per frame + LAME delay + 529) also comes from shadPS4 only, and the Fraunhofer block has no other public description - [sceAjmBatchJobGetCodecInfo, sceAjmBatchJobGetGaplessDecode](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - the `RUN_GET_CODEC_INFO` flag (0x800), the ATRAC9 codec info layout and its next frame size are taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [RPCSX](https://github.com/RPCSX/rpcsx) reimplementations, not confirmed on a PS5 title; a run job without buffers returns result 0, as in shadPS4. The gapless sideband reports the total and skip counts the title set and does not count them down: RPCSX does the same, shadPS4 counts them down and BryKytyPS5 returns its current counts - [AJM Opus decoder](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native, codec 24) - the initialize parameter layout (`u32 channels, u32 sample rate, u32`, seen as `2, 48000, 0`) and the little-endian `u16` byte count before each packet come from one title (The Smurfs Dreams); only 48000 Hz and a zero third word are accepted +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET (bit 13), INITIALIZE (bit 14) and the sideband input order (gapless decode, then initialize parameters) come from [shadPS4](https://github.com/shadps4-emu/shadPS4) (PS4); other flags throw +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - RESET is assumed to clear the context as sceAjmBatchJobClearContext does +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - initialize parameters are 8 bytes for ATRAC9, 12 for Opus and none for MP3, as one title passes them; other codecs and sizes, and an output other than the 8-byte result, throw +- [sceAjmBatchJobControl](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) - SIDEBAND_GAPLESS_DECODE with RESET overwrites both counts as in [RPCSX](https://github.com/RPCSX/rpcsx); shadPS4 ignores a zero count when the other is nonzero. Without RESET they differ, so it throws +- [NGS2 custom submixer rack](../../core/libs/prx/libSceNgs2.native/src/Custom.cpp) (libSceNgs2.native, rack 0x4002) - structure layouts from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `ngs2_custom.h`; module id 0x1f for UserFx2, the voice parameter id `0x40001f00 | module index`, the process flags (1 on the first process call after setup, 2 after a parameter change, 0 otherwise) and the call order (setup per voice at rack creation, process after the inputs are mixed, cleanup at destruction) come only from [KytyPS5](https://github.com/KytyPS5/KytyPS5). Only UserFx2 modules on a single buffer are accepted: other module ids, more buffers, a module control handler, different input and output channel counts, setup flags and default rack options throw. Each module's state is a separate buffer, not the `state_offset` slice of the voice state, and `sceNgs2VoiceGetState` still throws for this rack. As in KytyPS5, process runs only on grains where an input produced samples, so an effect tail stops with the input and a pending flag 2 waits for the next audible grain - [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); parsing and error codes follow [shadPS4's reverse-engineered PS4 library](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Folded values retain their line breaks and whitespace; an empty first line starts the value scan after its line feed. A consumed byte count above `INT_MAX` throws instead of wrapping. - [sceHttpParseStatusLine](../../core/libs/prx/libSceHttp/src/Parse.cpp) (libSceHttp) - behaviour and error codes follow the [shadPS4](https://github.com/shadps4-emu/shadPS4) reimplementation of the PS4 library, not confirmed on a PS5 title; unlike it, no byte past `lineLen` is read +- [sceHttpUriMerge](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); validation, size formula, absolute/relative split and path join follow the disassembly-like body in [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. The base URL goes through our `sceHttpUriParse`, which removes dot segments from the base path, while the relative part is appended unswept; base query and fragment are dropped. The relative reference is only classified by its scheme and a leading `//`, and only references with `//` are validated; as in shadPS4, `//host/x` is returned unchanged instead of taking the base scheme, a reference with a scheme but no `//` (`mailto:x`) is joined onto the base directory, and `?q` and `#f` are appended to the base directory instead of the base path. - [sceHttpUriUnescape](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - signature follows [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Net/Http.cs); percent decoding, invalid sequences and error codes follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp), not confirmed on a PS5 title or hardware. Size includes the final NUL; malformed escapes and `+` remain unchanged, and decoding runs once. +- [sceHttpUriSweepPath](../../core/libs/prx/libSceHttp/src/Uri.cpp) (libSceHttp) - behaviour follows the [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) reimplementation of the PS4 library and its tests (including the PS4 SDK example), not confirmed on a PS5 title or hardware. It is not RFC 3986 5.2.4: a path that doesn't start with `/` is copied unchanged and a trailing `.` or `..` without a slash is kept. Unlike shadPS4, nothing at or past `srcSize - 1` is read, and `srcSize == 1` writes only the terminator +- [sceHttpSetResponseHeaderMaxSize, sceHttpRedirectCacheFlush, sceHttpsUnloadCert](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - signatures follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/network/http.cpp) (PS4); no PS5 reference has them - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature +- [libSceHttp2 cookie boxes](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - `sceHttp2CreateCookieBox` is taken to take the library context id first, as in the [prosper](https://github.com/mattias800/prosper) reimplementation; further arguments are unknown and ignored. `sceHttp2SetRequestNoContentLength` is assumed to take only the request id. Cookies are never stored, so `sceHttp2CookieFlush` has nothing to discard - [sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature - [sceHttpSetRequestStatusCallback](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - no reference implements it (shadPS4 only logs and returns 0); when the callback runs and what status it gets is unknown -- [sce::Np::CppWebApi::Common::IntrusivePtr](../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x18-byte layout (object, deleter, `LibContext*`) and the offset of the 32-bit reference count in the pointee (0x00, 0x30 for `Vector`) come from the module bundled with one title (PPSA21564); other SDK versions are not checked. The destructor throws on the last reference of a pointer without a deleter: destroying the object needs its destructor and the `LibContext` allocator, which are not implemented - [sceNpSessionSignalingGetMemoryInfo](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature - [sceNpSessionSignalingGetConnectionStatistics](../../core/libs/prx/libSceNpSessionSignaling/Export.cpp) (libSceNpSessionSignaling) - unknown signature -- [sce::Np::CppWebApi::Common::String](../../core/libs/prx/libSceNpCppWebApi/Export.cpp) (libSceNpCppWebApi) - the 0x20-byte layout (32-bit reference count, buffer at 0x08, buffer size at 0x10, `LibContext*` at 0x18) comes from the module bundled with one title (PPSA21564); other SDK versions are not checked. The destructor throws for a non-null buffer: releasing it needs the `LibContext` allocator, which is not implemented - [sceUserServiceGetAgeLevel](../../core/libs/prx/libSceUserService/Export.cpp) (libSceUserService) - the meaning of the value is unknown; 0 is reported, as for the unset game presets. PPSA12544 (Unity) only stores it for its user profile - [sceGameUpdateGetAddcontLatestVersion](../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - a null `entitlement_label` throws: KytyPS5 accepts it, prosper returns `0x80412803`. Bytes after `found` are zeroed as in KytyPS5; prosper leaves them untouched - [sceSystemServiceDisableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceReenableMediaPlay](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - unknown signature - [sceSystemServiceParamGetString](../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name; other parameters and buffers of 1 to 64 bytes throw +- [sceNgs2SystemSetSampleRate, sceNgs2SystemSetUserData, sceNgs2SystemGetUserData, sceNgs2SystemLock, sceNgs2SystemUnlock](../../core/libs/prx/libSceNgs2.native/src/System.cpp) (libSceNgs2.native) - shadPS4 only checks the system handle (`INVALID_SYSTEM_HANDLE`) and SharpProspero names what each call does; neither gives the other limits. A sample rate of 0 and a null user data pointer throw, any other rate is accepted, and changing the rate while a sampler filter is enabled throws because the filter coefficients were computed for the old rate. Lock and unlock take the same lock as `sceNgs2RackLock` +- [sceNgs2RackGetInfo](../../core/libs/prx/libSceNgs2.native/src/Rack.cpp) (libSceNgs2.native) - field order from [shadPS4](https://github.com/shadps4-emu/shadPS4)'s `OrbisNgs2RackInfo` with the 64-byte name of the other info structures (216 bytes); `type` is assumed to be the rack id divided by 0x1000, `min_grain_samples` 64 (the system minimum) and `state_flags` 1; `uid` comes from a per-process rack counter, `render_count` is the owning system's render count and `active_voice_count` counts voices that are not empty (paused and stopped voices included). The last process ratio and tick and `active_channel_work_count` are left zero +- [sceNgs2VoiceQueryInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - only info id 0x4001 is known: PPSA22520 passes an 8-byte buffer and branches on the first word being 1 or 2, so it is assumed to be the voice channel count (0 before the voice is set up); the second word is zeroed and other ids throw +- [sceNgs2VoiceGetPortInfo](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - layout from shadPS4; the delay and destination input are zero because ports have neither yet +- [sceSaveDataInitialize3](../../core/libs/prx/libSceSaveData.native/Export.cpp) (libSceSaveData.native) - a repeated initialize succeeds: PPSA12544's executable and its Unity SaveData plugin both initialize, and the plugin fails on any error. Initializations are counted and `sceSaveDataTerminate` ends the session at the last one; how the console pairs them is unknown - [sceVoiceSetMuteFlag](../../core/libs/prx/libSceVoice/Export.cpp) (libSceVoice) - unknown signature +- [sceTextToSpeech2GetSystemStatus](../../core/libs/prx/libSceTextToSpeech2/Export.cpp) (libSceTextToSpeech2) - unknown signature +- [sceVdecswQueryComputeMemoryInfo](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswFinalizeDecodeSequence](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswQueryDecoderMemoryInfo](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswSetDecodeInput](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswReleaseComputeQueue](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswAllocateComputeQueue](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswGetAvcPictureInfo](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswTrySyncDecodeOutput](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswTrySyncDecodeInput](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswSetDecodeOutput](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceVdecswResetDecoder](../../core/libs/prx/libSceVdecsw/Export.cpp) (libSceVdecsw) - unknown signature +- [sceCoredumpAttachUserMemoryFile](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature +- [sceCoredumpAttachMemoryRegion](../../core/libs/prx/libkernel/System/src/Coredump.cpp) (libkernel) - unknown signature +- [sceKernelAddTimerEvent](../../core/libs/prx/libkernel/Equeue/Equeue.cpp) (libkernel) - `usec` is taken as the period in microseconds; re-adding an existing timer keeps its pending expiration count and restarts the period from the time of the call, where FreeBSD 12+ `filt_timertouch` clears pending expirations on `EV_ADD`; `usec == 0` fires on every wait. None of this is confirmed on a PS5 title +- [sceRtcFormatRFC2822, sceRtcFormatRFC2822LocalTime, sceRtcFormatRFC3339LocalTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - only shadPS4 (PS4) has bodies. A null tick returns `INVALID_POINTER` like the existing `sceRtcFormatRFC3339`, where shadPS4 formats the current time instead; offsets outside ±1439 minutes return `INVALID_VALUE` like `sceRtcFormatRFC3339`; the local-time variants use the host's offset at the given instant (shadPS4 uses `sceKernelGettimezone`, the current offset) - [snwprintf_s](../../core/libs/prx/libc/src/FormattingWide.cpp) (libc) - follows C11 K.3.9.1.3 with `RSIZE_MAX` assumed to be `SIZE_MAX >> 1`. A runtime-constraint violation only returns a negative value: `set_constraint_handler_s` is not exported and the console's default handler is unknown. An invalid multibyte `%s` argument is copied byte by byte instead of being reported as an encoding error, as in `vswprintf` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) - the added `sceUltQueuePop`, `sceUltQueueTryPush`, `sceUltQueueDestroy` and `sceUltQueueDataResourcePoolDestroy` signatures are inferred from the existing push/pop and object APIs. Queue destruction is assumed to reject blocked callers with `ULT_ERROR_BUSY` and otherwise discard queued data; pool exhaustion returns `ULT_ERROR_AGAIN`. These rules, alignment checks and shared slot limits have not been confirmed on PS5 hardware or a title. Non-null queue and data-pool option parameters throw because their layouts are unknown. +- [sceKernelReadv / sceKernelWritev / sceKernelPreadv / sceKernelPwritev](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) are implemented on Linux only; on Windows they throw +- [LinuxProtFromSce](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - protection bits 0x100 and 0x200 are accepted and assumed not to affect host CPU access; host protection is derived only from the read, write and execute bits - [_sceUlobjmgrRegisterObject](../../core/libs/prx/ulobjmgr/Export.cpp) (ulobjmgr) - parameters taken from shadPS4 (PS4): a nonzero 64-bit object, a nonzero 32-bit kind and a 32-bit id output; the meaning of the first two and the PS5 signature are unverified. The id bound of `_sceUlobjmgrUnregisterObject` (below 0x4000) and the raw `EINVAL` (22) return also come from shadPS4 - [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown - [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes) -- [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified +- [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified. `sceAgcDriverGetResourceRegistrationMaxNameLength` writes 0xfc and returns 0, as [prosper](https://github.com/mattias800/prosper) does from a title's registration configuration; the `uint32_t*` parameter matches the SDK wrapper `sce::Agc::ResourceRegistration::getMaxNameLength(unsigned int*)`. sharpemu writes 256 and KytyPS5 returns `RESOURCE_REGISTRATION_UNAVAILABLE`, so the value is unverified; a null output throws +- [Shader registration](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Shaders/ShaderRegistry.cpp) (libSceAgcDriver) - a shader header is accepted at any address: PPSA26344 passes headers 4 and 1 bytes past an 8-byte boundary; whether the console refuses an alignment is unknown - [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown +- [sceRtcGetDosTime, sceRtcSetDosTime](../../core/libs/prx/libSceRtc/Export.cpp) (libSceRtc) - bit layout from shadPS4 (PS4, the standard FAT date/time). Years outside 1980-2107 throw in `GetDosTime`, since no reference says what the library returns for them. `SetDosTime` decodes the year as the unsigned 7-bit FAT field (1980-2107), where shadPS4 sign-extends the 16-bit date to years before 1980. `SetDosTime` does not validate the fields, as in shadPS4 +- [sce::Json::InitParameter2](../../core/libs/prx/libSceJson2/Export.cpp) (libSceJson2) - layout inferred, not known: assumed to start with the allocator, its user data and the file buffer size, in that order, and the whole struct is assumed to fit in 40 bytes; `Initializer::initialize(InitParameter2)` ignores them as `initialize(InitParameter)` does +- [sceAjmBatchJobSetResampleParameters, sceAjmBatchJobGetResampleInfo](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm) - the ratio is taken as decoded samples per output sample and num_samples as the decoded samples the resampler holds, both inferred from FMOD (Hades II); the console's interpolation is unknown, a 4-point Catmull-Rom is used. The flags argument is ignored, as in shadPS4 and Kyty ### Functional -- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out +- [sceNetSendmsg and sceNetRecvmsg](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) copy the scatter/gather list through one host buffer and accept only the flags `sceNetSend`/`sceNetRecv` accept. Sending control data throws; a receive never returns control data (`msg_controllen` 0), since no socket option that queues it is supported +- [Additional content](../../core/libs/prx/libSceAppContent/Export.cpp) (libSceAppContent) is not installed or mounted: `sceAppContentAddcontMount` answers `NOT_FOUND` for every entitlement label and `sceAppContentAddcontUnmount` for every mount point, as no add-content mount is ever handed out; `sceAppContentGetAddcontInfo` answers `DRM_NO_ENTITLEMENT` for every label and `sceAppContentGetAddcontInfoList` reports 0 entries +- [sceKernelInstallExceptionHandler](../../core/libs/prx/libkernel/System/src/Exception.cpp) records the handler but nothing delivers to it: a host fault still ends the process, and `sceKernelRaiseException` is not implemented. PPSA12544's PS5Util module (Unity) installs a SIGUSR1 handler that reads `uc_mcontext.mc_rsp` (offset 0xf8 of the context) and raises SIGUSR1 on other threads to stop them for garbage collection, so delivery must run the handler on the target thread with a FreeBSD `ucontext` - [libSceUlt queues](../../core/libs/prx/libSceUlt/Export.cpp) use preallocated host storage instead of the supplied guest work area; its existing size formula and the 512-byte object layouts remain unverified. Waiting-queue pool thread limits and waiter priority ordering are not implemented. Finalization wakes blocked queue callers with `ULT_ERROR_STATE`. +- [Thread cancellation](../../core/libs/prx/libkernel/Pthread/src/Thread.cpp) (libkernel) is not modelled: `scePthreadCancel` throws, so no cancel request is ever pending and `scePthreadTestcancel` and `pthread_testcancel` return without acting; the cancel state and type set by `scePthreadSetcancelstate` and `scePthreadSetcanceltype` are stored and not used - [MIMG `tfe` and `lwe`](../../core/shader/recompiler/RdnaDecoder/src/RdnaImageOpDecoder.cpp) never write the status VGPR after the data. On hardware it is written only when a texel fetch hits an unmapped page of a partially resident texture, or, for `lwe` on sample/gather, when the LOD is below the T# `MIN_LOD_WARN`; texture memory is always resident here, and `lwe` on sample, gather and `image_get_lod` throws. +- [Unnormalized S# coordinates](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FORCE_UNNORMALIZED` uses a Vulkan `unnormalizedCoordinates` sampler, so the S# LOD range, LOD bias and mip filter are not applied, which is exact only on a one-level view. +- [Unnormalized S#s](../../core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp) are accepted only when direct and used only by explicit-LOD `image_sample` without offset, comparison, derivatives or `_a`, on a single-level, single-layer 1D or 2D view of mip 0, even when the image is also sampled through a normalized S#. +- An [unnormalized S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) throws with unequal minification and magnification filters, anisotropy, `TRUNC_COORD`, `MC_COORD_TRUNC` or X/Y clamping other than last-texel or border. - [Image atomics](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) on 32_SINT and 32_FLOAT surfaces act on the raw dword through an R32_UINT storage view; their float results are assumed to match those measured on 32_UINT surfaces. Other formats throw. +- [`ds_wrxchg2_rtn_b32`, `ds_wrxchg2st64_rtn_b32`, `ds_wrxchg2_rtn_b64` and `ds_wrxchg2st64_rtn_b64`](../../core/shader/recompiler/Translation/src/MemoryInstructions/SharedMemoryInstructions.cpp) are translated as two separate 32-bit or 64-bit atomic swaps per invocation. RDNA2 hardware (measured on gfx1035) performs every lane's first exchange before any lane's second one, so when lanes of the same wave or of different waves overlap addresses, the result can differ from hardware. `offset0 == offset1` throws, although on hardware it is two exchanges in order. +- [64-bit image atomics](../../core/shader/recompiler/Translation/src/MemoryInstructions/ImageInstructions.cpp) (`image_atomic_*` with a two-dword data mask) need `VK_EXT_shader_image_atomic_int64` and act on 32_32 UINT, SINT and FLOAT surfaces through an R64_UINT storage view. 64-bit `image_atomic_inc`, `image_atomic_dec` and the float atomics throw, as does any other access in the same shader to an image it updates with 64-bit atomics. +- [8_SRGB and 8_8_SRGB loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) on a device that cannot sample R8_SRGB or R8G8_SRGB return the sRGB EOTF rounded to nearest even at bf16 precision, as adriadam10 measured on RDNA2 (gfx1035) in #866; the PS5's gfx1013 is not measured. +- [8_SRGB and 8_8_SRGB textures](../../core/libs/prx/libSceAgcDriver/Graphics/src/TextureFormat.cpp) (libSceAgcDriver) that the device samples as R8_SRGB or R8G8_SRGB use its own decode, which need not match gfx1035: NVIDIA's R8_SRGB is one bf16 step off in 29 of 256 codes. +- [Samples and gathers](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8_SRGB and 8_8_SRGB textures throw on a device that cannot sample R8_SRGB or R8G8_SRGB. +- [Bindless image table](../../core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp) entries of 8_SRGB and 8_8_SRGB textures read zeros, like converted formats, on a device that cannot sample R8_SRGB or R8G8_SRGB. - The shader recompiler [ignores `s_setreg_b32`](../../core/shader/recompiler/Translation/src/ScalarInstructions.cpp): writes to MODE (float rounding and denormal controls) and the other hardware registers have no effect, and `s_getreg_b32` reads of the MODE round fields report round to nearest even even after `s_setreg_b32` wrote another mode. The initial round mode from the shader's program registers is not read either. +- On a host subgroup narrower than 32 lanes, [`threadBit`](../../core/shader/recompiler/Translation/src/TranslationContext/ExecMask.cpp) reads an EXEC written as a value (`s_mov_b32`/`s_mov_b64`) at bit (subgroup invocation id) of EXEC_LO, so lanes at or above the subgroup size read a lower lane's bit. +- [`v_readlane_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) and fetch-inactive `v_permlane16_b32`/`v_permlanex16_b32` read undefined data from a lane that the host subgroup does not hold or whose host invocation is inactive; the console reads the lane's VGPR. +- Without fetch-inactive, [`v_permlane16_b32` and `v_permlanex16_b32`](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterMath.cpp) take a source lane that the host subgroup does not hold as inactive and keep the old value (0 with bound_ctrl); the console reads that lane's VGPR. - [Shader recompilation](../../core/shader/recompiler/Recompiler.cpp) currently occurs right before it was transferred to Vulkan with caching, but should be moved to the [relinker](../../core/relinker/main.cpp) stage. For this purpose, [shader/recompiler](../../core/shader/recompiler) was written completely independently from [libs/prx](../../core/libs/prx). - [v_fma_f64](../../core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp) is fused exactly only for normal inputs: with a subnormal input it is a separate multiply and add, which rounds twice. +- [SINT storage images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) of 8- and 16-bit components saturate each stored 32-bit value to the component range, as adriadam10 measured for image stores on RDNA2 (gfx1035) in #944 (448 rows match); the PS5's gfx1013 is not measured. +- [`StoreTexel`](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) writes channel 1 of a two-channel format from the data slot whose DST_SEL is Y, for SINT and UINT alike. With DST_SEL X,Y,Z,W, gfx1035 writes it from the W slot, as adriadam10 measured in #944. - [COND_EXEC](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp) reads its condition after the earlier packets of its queue complete. Without PFP_SYNC_ME the console's graphics PFP reads it ahead of the ME, so a title that races the read sees a later value here. - A [flip inside a COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/CommandValidation.cpp) throws: the flip is reserved with the video output when the buffer is submitted, and how the console withdraws a skipped one is unknown. - A [COND_EXEC range](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) must hold whole packets and end inside its command buffer. A chained INDIRECT_BUFFER or a REWIND inside a range throws, as what the CP does with the skipped dwords then is unknown. +- A [COND_INDIRECT_BUFFER](../../core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp) (`sceAgcCbBranch`) runs only with the always-pass compare function; a comparison or a predicated packet throws. Modes 1 and 2 run the first buffer, assumed to be the if-then and if-then-else modes of AMD's PM4 packet. [sceAgcCbBranch](../../core/libs/prx/libSceAgc/Cb/src/Branch.cpp) does not check the compare address of an always-pass branch or the address of an empty buffer, as PPSA14632 passes an uninitialised compare address and a null else buffer of 0 dwords. - [v_mullit_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) follows the hardware measured with the MODE register at IEEE=1 and denormals kept (a signaling NaN src0 is quieted, a positive denormal src2 counts as positive): the recompiler does not read MODE, so other IEEE/denormal settings are not modelled. - [v_fma_legacy_f32](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (VOP3 0x140) is assumed to be fused, which has not been measured on hardware. LLVM decodes this encoding as `v_mad_legacy_f32` for gfx1013, and `v_mac_legacy_f32` (VOP2 0x06) was measured to round its product before the add, so 0x140 may do the same. - The executable file that [relinker](../../core/relinker/elfpatcher/src/windows/WindowsPeWriter.cpp) generates opens the console when launched, which is inconvenient for playability. -- [libSceJpegEnc](../../core/libs/prx/libSceJpegEnc/Export.cpp) encodes 4:2:2 sampling requests with 4:2:0 chroma subsampling, and grayscale requests as a 3-component JPEG with neutral chroma instead of a single-component one: the [JPEG encoder](../../core/Decoder/Jpeg/src/Jpeg.cpp) (stb) only produces 3-component 4:2:0 and 4:4:4 images. - [PA_CL_CLIP_CNTL](../../core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp) (libSceAgcDriver) - `DX_LINEAR_ATTR_CLIP_ENA` (bit 24) clear is drawn like bit 24 set, with Vulkan's clipping of `NoPerspective` outputs; what a clear bit changes on the console is undocumented. +- [Per-stage descriptor count](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) (libSceAgcDriver) - a pixel shader's colour attachments are not counted against `maxPerStageResources`, so its buffers and images can fit the limit while the pipeline exceeds it - [v_rcp, v_rsq, v_sqrt, v_log, v_exp, v_sin and v_cos](../../core/shader/recompiler/Translation/src/FloatInstructions.cpp) (f32 and f16) use the driver's approximation for finite results, not the console's: the low bits can differ. Only NaN, invalid, zero and infinite results and the exact sin/cos zeros are chosen on the bits. v_sin_f32/v_cos_f32 with inputs beyond ±256 cycles were not compared with the console. - `--to-intel` does not lower RDPRU/MCOMMIT; the [matcher](../../core/relinker/codegen/src/x86/Amd64OnlyInstructionMatcher.cpp) fails the relink instead. SHA-1 and SHA-256 instructions with a RIP-relative operand fail the relink. - The length-changing path of the [instruction rewriter](../../core/relinker/codegen/src/x86/X64InstructionRewriter.cpp) is not used by the [converter](../../core/relinker/codegen/src/Amd64OnlyConverter.cpp): it does not adjust VEX/0F38/0F3A RIP-relative operands, data-to-code references (relocations, FDEs, jump tables) or segment sizes, so every substitution keeps the instruction length. @@ -153,17 +246,24 @@ Throughout the project, every function at every stage either **does exactly what - [DCC display buffers](../../core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp) are presented only from uniform uncompressed or fast-clear keys; other keys, and register-clear keys over a pending image, throw. Keys are read as one byte per 256 bytes (the driver's model), not in the console's layout. - `--to-intel` guest module trampolines are covered only by a synthetic relinker test; no game title has been verified with them on Linux or Windows. - `--to-intel` replaces the approximate VEX.128 register forms of VRSQRTPS and VRCPPS with [correctly rounded](../../core/relinker/codegen/src/x86/ReciprocalLowering.cpp) 1/sqrt(x) and 1/x, not with the AMD approximation: its tables are not public. Intel's approximation of 1/sqrt(1.0) is 0x3F7FF000, and two Newton-Raphson steps on it settle on 0.99999994 instead of 1.0; a renormalised identity quaternion then turns into a NaN axis in PPSA21564, which the console does not show. Refined results now reach the exact value; unrefined ones can still differ from the console in the low bits. Each site costs an out-of-line stub (two jumps, a spill below the red zone, SQRTPS/DIVPS). The 256-bit, scalar, legacy and memory forms keep the native instruction, as does a 4-byte site with no movable instruction after it for the jump (5 sites in PPSA21564). -- [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates only the 101-key (US) arrangement and throws for the 106-key (Japanese) one; Ctrl and Alt do not change the character. +- [sceKeyboardGetKey2Char](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) (libSceKeyboard) translates the 101-key (US) arrangement and the alphanumeric layer of the 106-key (Japanese) one, which follows the JIS layout of the HID usages: the Yen (0x89) and Ro (0x87) keys return `\` (0x5C) unshifted, as JIS X 0201 places the yen sign there; whether the console returns U+00A5 instead is unverified. With the Kana LED (bit 4, the HID position) set the 106-key arrangement throws, as the kana the console returns are unknown. Ctrl and Alt do not change the character. - [sceAudioOut2Set3DLatency](../../core/libs/prx/libSceAudioOut/src/AudioOut2System.cpp) and [sceAudioOut2MasteringInit](../../core/libs/prx/libSceAudioOut/src/AudioOut2Mastering.cpp) (libSceAudioOut) have no effect: there is no 3D rendering or mastering stage. - [sceImeKeyboardGetInfo](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) returns `0x80bc0023` (no resource id) for every id while a keyboard is open, since the IME keyboard never reports a connected device. [sceImeKeyboardSetMode](../../core/libs/prx/libSceIme/Export.cpp) validates the mode bits but the mode has no effect, as no IME keyboard events are delivered. - [AudioOut2 contexts](../../core/libs/prx/libSceAudioOut/src/AudioOut2Context.cpp) (libSceAudioOut) read a grain's port buffers at the next push, 100 us after it, and play the mix behind a 40 ms cushion that the queue level leaves out. Both follow PPSA12544's FMOD output, measured by recording its four 256-frame buffers at every push: it fills a buffer after pushing it, reuses it two pushes later, and its mixer thread can still be writing when the next push follows within microseconds. Reading at the push, or two pushes later, breaks 27% or 59% of grain boundaries. When the hardware reads the buffers is not known. - [libSceAudiodec](../../core/libs/prx/libSceAudiodec/Export.cpp) throws for the 24-bit PCM word size (`iBwPcm` 0): its sample layout is unknown. ATRAC9 decoding is covered only by configuration and error tests, as no ATRAC9 encoder is available for a fixture. - [sceAjmDecMp3ParseFrame](../../core/libs/prx/libSceAjm.native/src/Ajm.cpp) (libSceAjm.native) throws when the original file length is requested (`parse_ofl` non-zero) for a layer other than III, for a CRC-protected frame, or for a VBRI header outside MPEG-1 stereo: shadPS4 looks for the tag after the CRC and after the side information, while LAME and FFmpeg place it without the CRC and VBRI always at byte 36. - The [output modifier](../../core/shader/recompiler/Translation/src/TranslationContext/OperandAccess.cpp) (`mul:2`, `mul:4`, `div:2`) follows the hardware with the IEEE mode off and f32 denormals flushed, the LLVM graphics default; the shader's MODE register is not read, and the hardware ignores the modifier in the other modes. On f16 results it is ignored, as on the hardware while f16 denormals are enabled; with them flushed the hardware applies it. -- [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented; half-border addressing, the border color table, gradients, LOD bias, offsets, LOD clamps, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. Bilinear weights are full float, not the hardware's fixed-point subtexel weights -- [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified. +- [Comparison sampling of a color texture](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (`image_sample_c`/`image_sample_c_lz` on a texture that is not R32 float or R16 unorm) is emulated in the shader, since Vulkan compares only depth formats: the red channel is fetched, compared and, for bilinear filtering, blended (compare, then filter). Only 2D and 2D array views, float/unorm/snorm formats, wrap, clamp-to-edge and clamp-to-border addressing (border red 0 for the black borders, 1 for opaque white) and point or bilinear filtering at one level are implemented, with texel offsets added to the texel indices; LOD biases and clamps are accepted, as they cannot select another level of a single-level sample. Half-border addressing, the border color table, gradients, gathers, other dimensions, anisotropy and mip filtering throw. Under bilinear filtering, taps past a clamp-to-border edge compare the border value and blend with the texel taps, so results near the edge depend on the filter weights. AMD documentation does not say how a color format's reference is handled: it is clamped to [0, 1] for unorm and [-1, 1] for snorm, as Vulkan does for unorm depth, and not clamped for float. Bilinear weights are full float, not the hardware's fixed-point subtexel weights +- [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations, so `scePlayerInvitationDialogGetResult` reports `USER_CANCELED`; its parameter ABI remains unverified. The 40-byte result layout (error code, result, 32 reserved bytes) is taken from [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/Dialog/PlayerInvitationDialog.cs) and the error codes from the other common dialogs, not confirmed on a title. +- [Converted unorm images](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) (10_11_11_UNORM, read and written through an R32_UINT view): samples, gathers, LOD queries, 16-bit data and a DST_SEL of the missing W channel throw. +- [Converted unorm image loads](../../core/shader/recompiler/SpirvBackend/src/SpirvImageEmitter.cpp) divide by 2^n - 1 with the buffer format path's `OpFDiv`, which Vulkan allows to be up to 2.5 ULP off, so some drivers are not bit-exact. - [64-bit LDS atomics](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) (`ds_*_u64`, `ds_*_b64`, `ds_*_f64`) read and write the two dwords under a workgroup lock stored after the guest LDS, as LDS is a 32-bit array without 64-bit atomics. They are atomic with respect to each other only: 32-bit accesses to the same dwords issued concurrently are not ordered by the lock. 64-bit GDS atomics throw. `ds_min/max(_rtn)_f64` and `ds_cmpst(_rtn)_f64` follow the NaN and signed-zero rules measured for the f32 forms; the f64 forms were not measured on hardware. +- [Min/max S# reduction](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver) - `FILTER_MODE` 1 and 2 use Vulkan's min and max reduction (the same `SQ_IMG_FILTER_MODE` values RADV programs). Measured on RDNA2 (gfx1035), not on the console: only texels of non-zero weight are reduced, and the weight is taken after rounding to the 8-bit subtexel grid (a sample within 1/512 texel of a centre reduces that texel alone); a driver with other `subTexelPrecisionBits` reduces a different set there. Gathers ignore the mode. +- [Min/max S#](../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestSamplerResource.cpp) (libSceAgcDriver, shader recompiler) - throws with anisotropy, a linear mip filter, depth or color-texture compare, or bilinear filtering of an image the recompiler point-samples (sint, converted, depth-bits). +- [Raw `buffer_load_dwordx2`](../../core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvMemoryInstructions.cpp) is one 64-bit access on the hardware (with or without glc), so it never sees half of a `buffer_atomic_swap_x2`. Only a glc load that is 8-byte aligned and in bounds, in a program that has the 64-bit storage view (it uses 64-bit buffer atomics), is read with one 64-bit atomic load; every other dwordx2 load is two dword loads and can tear against a concurrent 64-bit write: loads without glc, and loads in a program without 64-bit buffer atomics while another shader writes the pair with `swap_x2`. - [wcsrtombs_s](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) follows the console's `libc.prx`, but a runtime-constraint violation only returns its error: the default handler there writes the message and a newline to `stderr` before returning, which is not done here. Conversion uses the C locale of `wcrtomb` (`MB_CUR_MAX` 1), so the path where a multibyte character does not fit the remaining space is not reached +- [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [Font writing lines](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontCreateWritingLine`, `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics` and `sceFontWritingLineClear` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libFont.cpp): runs are placed left to right, each at the line's advance so far, with no spacing or adjustment. Only horizontal and left-to-right lines, writing attribute 0 and non-null run metrics are modelled; other values, the metrics of an empty line and unknown or null handles throw, since no reference gives their result or error code. `sceFontWritingLineGetOrderingSpace` still throws (KytyPS5's values are placeholders). Lines are allocated on the host, not in the font memory +- [sceImeKeyboardGetResourceId](../../core/libs/prx/libSceIme/Export.cpp) (libSceIme) reports that no USB keyboard is connected (`0x80bc0004`, as [shadPS4](https://github.com/shadps4-emu/shadPS4) does) because IME keyboard events are not delivered to the handler, although [libSceKeyboard](../../core/libs/prx/libSceKeyboard/src/keyboard_impl.cpp) exposes the host keyboard diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 999714177..785c57379 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -89,6 +89,10 @@ Windows PowerShell: Games that open the console's system font sets (`sceFontOpenFontSet`) need font files in an `anyps5-fonts/` directory beside the output executable; set `ANYPS5_SYSTEM_FONTS` to use another directory. Files dumped from the console are used under their own names (`SST-Roman.otf`, `SST-Bold.otf`, `SSTJpPro-Regular.otf`, ...). Without them, these openly licensed substitutes are used when present: `NotoSans-{Light,Regular,Medium,Bold}.ttf` and `NotoSans-{LightItalic,Italic,MediumItalic,BoldItalic}.ttf` (Latin and Vietnamese), `NotoSansMono-{Light,Regular,Medium,Bold}.ttf` (typewriter), `NotoSansThai-{Light,Regular,Medium,Bold}.ttf` (Thai) and `NotoSansCJK-{Light,Regular,Medium,Bold}.ttc` (Japanese and Chinese). Without either, opening a system font set fails and the game shows no text in those fonts. +### GPU selection + +The game runs on the first Vulkan 1.1 device with graphics and compute queues and swapchain presentation, preferring a discrete GPU over an integrated one. Set `ANYPS5_GPU` to a part of a device name, compared without regard to case, to run on another device; the names are printed at start-up in the `Physical device candidate` lines. When no usable device contains the text, the start fails and the error lists the device names. + ## Exit codes `0`: conversion succeeded. `1`: invalid arguments. `2`: conversion failed; the error is printed to stderr. With `--autorun`, successful conversion returns the launched application's exit code. diff --git a/tools/check_conventions.py b/tools/check_conventions.py new file mode 100644 index 000000000..933dec172 --- /dev/null +++ b/tools/check_conventions.py @@ -0,0 +1,276 @@ +import argparse +import json +import os +import re +import subprocess +from collections import defaultdict +from pathlib import PurePosixPath + +RULES = { + "comment": ("error", "docs/dev/CONVENTIONS.md", "Comments are only for technical debt, so they need a change to docs/dev/TechnicalDebt.md in the same pull request; #endif and namespace ends are always allowed"), + "silent-stub": ("error", "CONTRIBUTING.md#code", "Unimplemented exports call NotImplemented_nid_no_patch(__func__); silent stubs are listed in docs/dev/TechnicalDebt.md#silent-stubs, by their file or by name with a path in their library"), + "duplicate-export": ("error", "CONTRIBUTING.md#code", "Already exported by another library: remove the copy, or use APS5_DUMMY_FUN if nothing is left"), + "system-dependency": ("error", "CONTRIBUTING.md#code", "Third-party code is a submodule under 3rdparty/ built from source, not found on the system"), + "title-specific": ("error", "CONTRIBUTING.md#code", "Implement the general behaviour, not what one title needs; title-specific code belongs in its sce_module"), + "extension": ("error", "CONTRIBUTING.md#code", "Avoid non-standard extensions where standard C++ is enough"), + "notes-file": ("error", "CONTRIBUTING.md#branches-and-pull-requests", "Notes, investigation and agent files go in the pull request, not in the repository"), + "binary": ("error", "docs/dev/CONVENTIONS.md", "Only UTF-8 text files in the repository; images go in the gist comments"), + "doc-link": ("error", "CONTRIBUTING.md#documentation", "Relative link to a file that does not exist"), + "commit-subject": ("error", "docs/dev/CONVENTIONS.md", "Commit subject is not Conventional Commits"), + "pr-title": ("error", "docs/dev/CONVENTIONS.md", "Pull request title is not Conventional Commits"), + "pr-template": ("error", ".github/pull_request_template.md", "Fill in the pull request template"), +} + +CPP = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx", ".inl", ".ipp"} +IMAGES = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".svg", ".webp", ".ico", ".tga", ".dds"} +NOTES = {".md", ".markdown", ".rst", ".log", ".patch", ".diff"} +AGENT_FILES = {"AGENTS.md", "CLAUDE.md", "GEMINI.md", ".cursorrules", "copilot-instructions.md"} + +CONVENTIONAL = re.compile(r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([^)]+\))?!?: \S") +STRINGS = re.compile(r'"(?:\\.|[^"\\])*"|\'(?:\\.|[^\'\\])*\'') +ALLOWED_COMMENT = re.compile(r"^\s*(#\s*endif\b|}\s*;?\s*//\s*(end\s+)?namespace\b)") +EXPORT = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\(") +STUB_BODY = re.compile(r"^(?:\(void\)\s*\w+\s*;|static_cast\(\s*\w+\s*\)\s*;)*" + r"(?:return\s*\(?\s*(?:-?(?:0x[0-9a-fA-F]+|\d+)[uUlL]*|nullptr|true|false|\w*_OK)?\s*\)?\s*;)?$") +SYSTEM_DEPENDENCY = re.compile(r"\b(find_package\s*\((?!\s*(Python3|Threads|Git)\b)|pkg_check_modules|pkg_search_module|find_library)\b") +TITLE = re.compile(r"\b(PPSA|CUSA)\d{5}\b") +EXTENSION = re.compile(r"__attribute__|__declspec|#\s*pragma\s+(?!once\b)") +LINK = re.compile(r"\]\(([^)\s]+)") +SECTION = re.compile(r"^###\s*(.+?)\s*$", re.M) + + +def git(*args, text=True): + output = subprocess.run(["git", *args], capture_output=True, check=True).stdout + return output.decode("utf-8", errors="replace") if text else output + + +class Check: + def __init__(self, base, head): + self.head = head + self.base = git("merge-base", base, head).strip() + self.findings = [] + self.added = defaultdict(list) + self.status = {} + self.numstat = {} + self.tree = set(git("ls-tree", "-r", "--name-only", head).splitlines()) + self.dirs = {str(parent) for path in self.tree for parent in PurePosixPath(path).parents} + for line in git("diff", "--name-status", "--no-renames", self.base, head).splitlines(): + status, path = line.split("\t", 1) + self.status[path] = status + for line in git("diff", "--numstat", "--no-renames", self.base, head).splitlines(): + added, deleted, path = line.split("\t", 2) + self.numstat[path] = (added, deleted) + path = None + for line in git("diff", "-U0", "--no-color", "--no-renames", "--no-prefix", self.base, head).splitlines(): + if line.startswith("+++ "): + path = None if line == "+++ /dev/null" else line[4:] + elif line.startswith("@@"): + number = int(re.search(r"\+(\d+)", line).group(1)) + elif line.startswith("+") and path: + self.added[path].append((number, line[1:])) + number += 1 + + def report(self, rule, path, line, detail=""): + self.findings.append((rule, path, line, detail)) + + def files(self, predicate): + return [(path, lines) for path, lines in self.added.items() if not path.startswith("3rdparty/") and predicate(PurePosixPath(path))] + + def show(self, path): + return git("show", f"{self.head}:{path}") + + def code(self): + debt = [(text, links("docs/dev/TechnicalDebt.md", text)) for text in self.show("docs/dev/TechnicalDebt.md").splitlines()] if "docs/dev/TechnicalDebt.md" in self.tree else [] + exports = {} + for path, lines in self.files(lambda p: p.suffix in CPP): + core = path.startswith("core/") and "tests" not in PurePosixPath(path).parts + source = None + for number, text in lines: + bare = STRINGS.sub('""', text) + if ("//" in bare or "/*" in bare) and not ALLOWED_COMMENT.match(bare): + self.report("comment", path, number) + if EXTENSION.search(bare): + self.report("extension", path, number, EXTENSION.search(bare).group(0)) + if core and TITLE.search(text): + self.report("title-specific", path, number, TITLE.search(text).group(0)) + match = EXPORT.search(bare) + if match and path.startswith("core/libs/prx/") and not match.group(1).endswith("_nid_no_patch"): + source = source or self.show(path).splitlines() + body = function_body(source, number - 1) + if body is None: + continue + exports[match.group(1)] = (path, number) + if match.group(1).startswith("sce") and STUB_BODY.match(body) and not listed(debt, match.group(1), path): + self.report("silent-stub", path, number, match.group(1)) + self.duplicates(exports) + for path, lines in self.files(lambda p: p.suffix in (".py", ".cmake") or p.name == "CMakeLists.txt"): + for number, text in lines: + if "#" in STRINGS.sub('""', text) and not text.startswith("#!"): + self.report("comment", path, number) + if "docs/dev/TechnicalDebt.md" in self.status: + self.findings = [finding for finding in self.findings if finding[0] != "comment"] + + def duplicates(self, exports): + if not exports: + return + pattern = r"\bAPS5_VABI\s+(" + "|".join(exports) + r")\s*\(" + result = subprocess.run(["git", "grep", "-nE", pattern, self.head, "--", "core/libs/prx"], capture_output=True, text=True) + for line in result.stdout.splitlines(): + _, path, _, text = line.split(":", 3) + name = EXPORT.search(text).group(1) + own, number = exports[name] + if library(path) != library(own) and not text.rstrip().endswith(";"): + self.report("duplicate-export", own, number, f"{name} is also in {library(path)}") + + def build(self): + for path, lines in self.files(lambda p: p.name == "CMakeLists.txt" or p.suffix == ".cmake"): + for number, text in lines: + if SYSTEM_DEPENDENCY.search(text.split("#", 1)[0]): + self.report("system-dependency", path, number, text.strip()) + for number, text in self.added.get(".gitmodules", []): + match = re.match(r"\s*path\s*=\s*(\S+)", text) + if match and not match.group(1).startswith("3rdparty/"): + self.report("system-dependency", ".gitmodules", number, match.group(1)) + + def repository(self): + for path, (added, deleted) in self.numstat.items(): + if path.startswith("3rdparty/"): + continue + suffix = PurePosixPath(path).suffix.lower() + if self.status.get(path) != "D" and (added == "-" or suffix in IMAGES or not utf8(git("show", f"{self.head}:{path}", text=False))): + self.report("binary", path, 0) + if self.status.get(path) == "A" and allowed_notes(path) is False: + self.report("notes-file", path, 0) + + def docs(self): + for path, lines in self.files(lambda p: p.suffix == ".md"): + for number, text in lines: + for target, resolved in links(path, text): + if resolved not in self.tree and resolved not in self.dirs: + self.report("doc-link", path, number, target) + + def commits(self): + for line in git("log", "--no-merges", "--format=%h %s", f"{self.base}..{self.head}").splitlines(): + sha, subject = line.split(" ", 1) + if not CONVENTIONAL.match(subject): + self.report("commit-subject", "", 0, f"{sha} {subject}") + + def pull_request(self, title, body): + if not CONVENTIONAL.match(title): + self.report("pr-title", "", 0, title) + body = re.sub(r"", "", body or "", flags=re.S) + sections = dict(zip(SECTION.findall(body), SECTION.split(body)[2::2])) + for name in ("What", "Tested", "Checklist"): + if not sections.get(name, "").strip(): + self.report("pr-template", "", 0, f"section '{name}' is missing or empty") + checklist = sections.get("Checklist", "") + for item in re.findall(r"^\s*-\s*\[ \]\s*(.+)$", checklist, re.M): + self.report("pr-template", "", 0, f"unchecked: {item.strip()}") + if checklist and not re.search(r"AI-assisted:\s*(yes|no)\b", checklist, re.I): + self.report("pr-template", "", 0, "answer AI-assisted with yes or no") + if checklist and not re.search(r"Depends on:\s*(#\d+|none)\b", checklist, re.I): + self.report("pr-template", "", 0, "fill Depends on with #N or none") + + +def utf8(data): + try: + data.decode("utf-8") + except UnicodeDecodeError: + return False + return True + + +def links(path, text): + result = [] + for target in LINK.findall(text): + if re.match(r"[a-z]+:|#", target): + continue + target = target.split("#", 1)[0].split("?", 1)[0] + result.append((target, os.path.normpath(target.lstrip("/") if target.startswith("/") else str(PurePosixPath(path).parent / target)))) + return result + + +def listed(debt, name, path): + own = PurePosixPath(path).parts[:4] + for text, targets in debt: + for _, resolved in targets: + if resolved == path or (PurePosixPath(resolved).parts[:4] == own and re.search(rf"\b{name}\b", text)): + return True + return False + + +def library(path): + return PurePosixPath(path).parts[3] + + +def allowed_notes(path): + p = PurePosixPath(path) + if p.name in AGENT_FILES: + return False + if p.suffix.lower() not in NOTES: + return None + return path in ("README.md", "CONTRIBUTING.md") or p.parts[0] == ".github" or p.parts[:2] in (("docs", "dev"), ("docs", "user")) + + +def function_body(lines, start): + text = "\n".join(lines[start:start + 400]) + text = re.sub(r"//.*|/\*.*?\*/", "", STRINGS.sub('""', text), flags=re.S) + brace, semicolon = text.find("{"), text.find(";") + if brace < 0 or 0 <= semicolon < brace: + return None + depth = 0 + for index in range(brace, len(text)): + depth += {"{": 1, "}": -1}.get(text[index], 0) + if depth == 0: + return re.sub(r"\s+", " ", text[brace + 1:index]).replace(" ", "") + return None + + +def output(check, repo): + errors = 0 + rows = [] + actions = os.environ.get("GITHUB_ACTIONS") == "true" + for rule, path, line, detail in sorted(check.findings, key=lambda f: (not f[1], f[1], f[2], f[0])): + level, doc, message = RULES[rule] + errors += level == "error" + text = f"{message}" + (f": {detail}" if detail else "") + if actions: + location = f" file={path},line={max(line, 1)}," if path else " " + print(f"::{level}{location}title={rule}::" + text.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")) + else: + print(f"{path or '(pull request)'}{f':{line}' if line else ''}: {level}: [{rule}] {text}") + where = f"`{path}:{line}`" if line else f"`{path}`" if path else "" + cell = text.replace("|", "\\|") + rows.append(f"| {level} | [{rule}](https://github.com/{repo}/blob/main/{doc}) | {where} | {cell} |") + summary = os.environ.get("GITHUB_STEP_SUMMARY") + if summary: + with open(summary, "a") as out: + if rows: + out.write("| Level | Rule | Where | What |\n|---|---|---|---|\n" + "\n".join(rows) + "\n") + else: + out.write("All conventions checks passed.\n") + print(f"{len(rows)} findings, {errors} errors; GitHub shows at most 10 error annotations, the job summary lists all" + if rows else "All conventions checks passed.") + return 1 if errors else 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="check a branch against the contributing rules; errors fail the check") + parser.add_argument("--base", default="origin/main", help="branch the pull request targets (default: origin/main)") + parser.add_argument("--head", default="HEAD") + parser.add_argument("--title", help="pull request title to check") + parser.add_argument("--body", help="file with the pull request description to check") + parser.add_argument("--event", default=os.environ.get("GITHUB_EVENT_PATH"), help="GitHub event JSON (set in CI)") + args = parser.parse_args() + check = Check(args.base, args.head) + check.code() + check.build() + check.repository() + check.docs() + check.commits() + pr = json.load(open(args.event)).get("pull_request") if args.event else None + if pr: + check.pull_request(pr["title"], pr["body"]) + elif args.title is not None or args.body: + check.pull_request(args.title or "", open(args.body).read() if args.body else "") + raise SystemExit(output(check, os.environ.get("GITHUB_REPOSITORY", "boykopovar/AnyPS5"))) diff --git a/tools/hw-oracle/hw_oracle.py b/tools/hw-oracle/hw_oracle.py new file mode 100644 index 000000000..eee171f50 --- /dev/null +++ b/tools/hw-oracle/hw_oracle.py @@ -0,0 +1,106 @@ +import argparse +import os +import shutil +import struct +import subprocess +import sys +import tempfile +from functools import cache +from pathlib import Path + +HERE = Path(__file__).resolve().parent +CACHE = Path(os.environ.get("HW_ORACLE_CACHE") or Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "anyps5-hw-oracle") +ROWS_PER_DISPATCH = 1024 + + +def rocm_root(): + for root in (os.environ.get("ROCM_PATH"), "/opt/rocm"): + if root and (Path(root) / "include" / "hsa" / "hsa.h").exists(): + return Path(root) + return None + + +def tool(name): + found = shutil.which(name) + if found: + return found + root = rocm_root() + if root and (root / "llvm" / "bin" / name).exists(): + return str(root / "llvm" / "bin" / name) + sys.exit(f"{name} not found: install LLVM with the AMDGPU target or set ROCM_PATH") + + +def oracle(): + binary = CACHE / "oracle" + source = HERE / "oracle.c" + if binary.exists() and binary.stat().st_mtime >= source.stat().st_mtime: + return binary + CACHE.mkdir(parents=True, exist_ok=True) + command = [os.environ.get("CC", "cc"), "-O1", str(source), "-o", str(binary)] + root = rocm_root() + if root: + lib = root / "lib" + command += [f"-I{root / 'include'}", f"-L{lib}", f"-Wl,-rpath,{lib}"] + subprocess.run(command + ["-lhsa-runtime64"], check=True) + return binary + + +@cache +def target(): + return os.environ.get("HW_ORACLE_TARGET") or subprocess.run([oracle(), "--target"], capture_output=True, text=True, check=True).stdout.strip() + + +def assemble(body, work, wave64, ieee, denorm32, denorm16): + text = (HERE / "template.s").read_text() + for key, value in (("@TARGET@", target()), ("@WAVE32@", "0" if wave64 else "1"), ("@WAVESIZE@", "64" if wave64 else "32"), + ("@DENORM@", str(denorm32)), ("@DENORM16@", str(denorm16)), ("@IEEE@", str(ieee)), ("@BODY@", body)): + text = text.replace(key, value) + (work / "k.s").write_text(text) + subprocess.run([tool("clang"), "-x", "assembler", "-target", "amdgcn-amd-amdhsa", f"-mcpu={target()}", "-c", str(work / "k.s"), "-o", str(work / "k.o")], check=True) + subprocess.run([tool("ld.lld"), "-shared", str(work / "k.o"), "-o", str(work / "k.co")], check=True) + return work / "k.co" + + +def run(body, rows, extra=b"", wave64=False, ieee=1, denorm32=3, denorm16=3, coarse=False): + rows = [tuple(r) for r in rows] + wave = 64 if wave64 else 32 + padded = rows + [(0, 0, 0, 0)] * (-len(rows) % wave) + env = dict(os.environ) + env.pop("COARSE", None) + if coarse: + env["COARSE"] = "1" + out = [] + with tempfile.TemporaryDirectory() as tmp: + work = Path(tmp) + code = assemble(body, work, wave64, ieee, denorm32, denorm16) + for start in range(0, len(padded), ROWS_PER_DISPATCH): + chunk = padded[start:start + ROWS_PER_DISPATCH] + (work / "in.bin").write_bytes(b"".join(struct.pack("<4I", *r) for r in chunk) + extra) + subprocess.run([oracle(), str(code), str(len(chunk)), str(work / "in.bin"), str(work / "out.bin"), "16"], check=True, env=env) + data = (work / "out.bin").read_bytes() + out += [struct.unpack_from("<16I", data, lane * 64) for lane in range(len(chunk))] + return out[:len(rows)] + + +def main(): + parser = argparse.ArgumentParser(description="Run an RDNA kernel body on the local AMD GPU, one input row per lane.") + parser.add_argument("body", type=Path, help="assembly inserted into template.s") + parser.add_argument("rows", type=Path, help="one row per line: 4 u32 values (decimal or 0x hex)") + parser.add_argument("--wave64", action="store_true") + parser.add_argument("--ieee", type=int, choices=(0, 1), default=1) + parser.add_argument("--denorm32", type=int, choices=range(4), default=3) + parser.add_argument("--denorm16", type=int, choices=range(4), default=3) + parser.add_argument("--coarse", action="store_true", help="input and output in coarse-grained GPU memory") + parser.add_argument("--extra", type=Path, help="bytes appended after the rows") + parser.add_argument("--outs", type=int, choices=range(1, 17), default=16, metavar="N", help="print v10..v(10+N-1)") + args = parser.parse_args() + rows = [tuple(int(v, 0) for v in line.split()) for line in args.rows.read_text().splitlines() if line.strip()] + if any(len(r) != 4 for r in rows): + sys.exit("every row needs 4 values") + extra = args.extra.read_bytes() if args.extra else b"" + for result in run(args.body.read_text(), rows, extra, args.wave64, args.ieee, args.denorm32, args.denorm16, args.coarse): + print(" ".join(f"{v:08x}" for v in result[:args.outs])) + + +if __name__ == "__main__": + main() diff --git a/tools/hw-oracle/oracle.c b/tools/hw-oracle/oracle.c new file mode 100644 index 000000000..cb240017e --- /dev/null +++ b/tools/hw-oracle/oracle.c @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include + +#define CHECK(x) do { hsa_status_t s_ = (x); if (s_ != HSA_STATUS_SUCCESS && s_ != HSA_STATUS_INFO_BREAK) { const char* m_; hsa_status_string(s_, &m_); fprintf(stderr, "%s: %s\n", #x, m_); exit(1); } } while (0) + +static hsa_agent_t gpu; +static int haveGpu; +static hsa_amd_memory_pool_t kernargPool, finePool; +static int haveKernarg, haveFine; +static hsa_amd_memory_pool_t coarsePool; +static int haveCoarse; + +static hsa_status_t findCoarse(hsa_amd_memory_pool_t pool, void* data) { + hsa_amd_segment_t segment; + hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_SEGMENT, &segment); + if (segment != HSA_AMD_SEGMENT_GLOBAL) return HSA_STATUS_SUCCESS; + uint32_t flags; + hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_GLOBAL_FLAGS, &flags); + if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_COARSE_GRAINED) && !haveCoarse) { coarsePool = pool; haveCoarse = 1; } + return HSA_STATUS_SUCCESS; +} + +static hsa_status_t findGpu(hsa_agent_t agent, void* data) { + hsa_device_type_t type; + hsa_agent_get_info(agent, HSA_AGENT_INFO_DEVICE, &type); + if (type == HSA_DEVICE_TYPE_GPU) { gpu = agent; haveGpu = 1; return HSA_STATUS_INFO_BREAK; } + return HSA_STATUS_SUCCESS; +} + +static hsa_status_t findPools(hsa_amd_memory_pool_t pool, void* data) { + hsa_amd_segment_t segment; + hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_SEGMENT, &segment); + if (segment != HSA_AMD_SEGMENT_GLOBAL) return HSA_STATUS_SUCCESS; + uint32_t flags; + hsa_amd_memory_pool_get_info(pool, HSA_AMD_MEMORY_POOL_INFO_GLOBAL_FLAGS, &flags); + if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_KERNARG_INIT) && !haveKernarg) { kernargPool = pool; haveKernarg = 1; } + if ((flags & HSA_AMD_MEMORY_POOL_GLOBAL_FLAG_FINE_GRAINED) && !haveFine) { finePool = pool; haveFine = 1; } + return HSA_STATUS_SUCCESS; +} + +static hsa_status_t findCpu(hsa_agent_t agent, void* data) { + hsa_device_type_t type; + hsa_agent_get_info(agent, HSA_AGENT_INFO_DEVICE, &type); + if (type == HSA_DEVICE_TYPE_CPU) { hsa_amd_agent_iterate_memory_pools(agent, findPools, NULL); } + return HSA_STATUS_SUCCESS; +} + +static void* readFile(const char* path, size_t* size) { + FILE* f = fopen(path, "rb"); + if (!f) { perror(path); exit(1); } + fseek(f, 0, SEEK_END); *size = (size_t)ftell(f); fseek(f, 0, SEEK_SET); + void* data = malloc(*size); + if (fread(data, 1, *size, f) != *size) { perror(path); exit(1); } + fclose(f); + return data; +} + +int main(int argc, char** argv) { + if (argc == 2 && strcmp(argv[1], "--target") == 0) { + CHECK(hsa_init()); + CHECK(hsa_iterate_agents(findGpu, NULL)); + if (!haveGpu) { fprintf(stderr, "no GPU agent\n"); return 1; } + char name[64] = {0}; + CHECK(hsa_agent_get_info(gpu, HSA_AGENT_INFO_NAME, name)); + printf("%s\n", name); + return 0; + } + if (argc != 6) { fprintf(stderr, "usage: oracle --target | oracle code.o threads in.bin out.bin out_dwords\n"); return 2; } + const uint32_t threads = (uint32_t)atoi(argv[2]); + const uint32_t outDwords = (uint32_t)atoi(argv[5]); + CHECK(hsa_init()); + CHECK(hsa_iterate_agents(findGpu, NULL)); + if (!haveGpu) { fprintf(stderr, "no GPU agent\n"); return 1; } + CHECK(hsa_iterate_agents(findCpu, NULL)); + if (!haveKernarg || !haveFine) { fprintf(stderr, "no system memory pools\n"); return 1; } + + size_t codeSize; void* code = readFile(argv[1], &codeSize); + hsa_code_object_reader_t reader; + CHECK(hsa_code_object_reader_create_from_memory(code, codeSize, &reader)); + hsa_executable_t exe; + CHECK(hsa_executable_create_alt(HSA_PROFILE_FULL, HSA_DEFAULT_FLOAT_ROUNDING_MODE_DEFAULT, NULL, &exe)); + CHECK(hsa_executable_load_agent_code_object(exe, gpu, reader, NULL, NULL)); + CHECK(hsa_executable_freeze(exe, NULL)); + hsa_executable_symbol_t sym; + CHECK(hsa_executable_get_symbol_by_name(exe, "probe.kd", &gpu, &sym)); + uint64_t kernelObject; uint32_t kernargSize, groupSize, privateSize; + hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_OBJECT, &kernelObject); + hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_KERNARG_SEGMENT_SIZE, &kernargSize); + hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_GROUP_SEGMENT_SIZE, &groupSize); + hsa_executable_symbol_get_info(sym, HSA_EXECUTABLE_SYMBOL_INFO_KERNEL_PRIVATE_SEGMENT_SIZE, &privateSize); + + size_t inSize; void* inHost = readFile(argv[3], &inSize); + void *in, *out, *args; + const size_t outSize = (size_t)threads * outDwords * 4u; + const int coarse = getenv("COARSE") != NULL; + if (coarse) { + CHECK(hsa_amd_agent_iterate_memory_pools(gpu, findCoarse, NULL)); + if (!haveCoarse) { fprintf(stderr, "no coarse-grained GPU pool\n"); return 1; } + } + void *inStage = NULL, *outStage = NULL; + CHECK(hsa_amd_memory_pool_allocate(coarse ? coarsePool : finePool, inSize, 0, &in)); + CHECK(hsa_amd_memory_pool_allocate(coarse ? coarsePool : finePool, outSize, 0, &out)); + if (coarse) { + CHECK(hsa_amd_memory_pool_allocate(finePool, inSize, 0, &inStage)); + CHECK(hsa_amd_memory_pool_allocate(finePool, outSize, 0, &outStage)); + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, inStage)); + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, outStage)); + } + CHECK(hsa_amd_memory_pool_allocate(kernargPool, 16, 0, &args)); + if (!coarse) { + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, in)); + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, out)); + } + CHECK(hsa_amd_agents_allow_access(1, &gpu, NULL, args)); + if (coarse) { + memcpy(inStage, inHost, inSize); + memset(outStage, 0xee, outSize); + CHECK(hsa_memory_copy(in, inStage, inSize)); + CHECK(hsa_memory_copy(out, outStage, outSize)); + } else { + memcpy(in, inHost, inSize); + memset(out, 0xee, outSize); + } + ((void**)args)[0] = in; + ((void**)args)[1] = out; + + hsa_queue_t* queue; + CHECK(hsa_queue_create(gpu, 64, HSA_QUEUE_TYPE_SINGLE, NULL, NULL, UINT32_MAX, UINT32_MAX, &queue)); + hsa_signal_t done; + CHECK(hsa_signal_create(1, 0, NULL, &done)); + const uint64_t index = hsa_queue_add_write_index_relaxed(queue, 1); + hsa_kernel_dispatch_packet_t* packet = (hsa_kernel_dispatch_packet_t*)queue->base_address + (index & (queue->size - 1)); + memset((uint8_t*)packet + 4, 0, sizeof(*packet) - 4); + packet->setup = 1u << HSA_KERNEL_DISPATCH_PACKET_SETUP_DIMENSIONS; + packet->workgroup_size_x = (uint16_t)(threads < 1024 ? threads : 1024); + packet->workgroup_size_y = 1; + packet->workgroup_size_z = 1; + packet->grid_size_x = threads; + packet->grid_size_y = 1; + packet->grid_size_z = 1; + packet->kernel_object = kernelObject; + packet->kernarg_address = args; + packet->private_segment_size = privateSize; + packet->group_segment_size = groupSize; + packet->completion_signal = done; + const uint16_t header = (HSA_PACKET_TYPE_KERNEL_DISPATCH << HSA_PACKET_HEADER_TYPE) | + (HSA_FENCE_SCOPE_SYSTEM << HSA_PACKET_HEADER_ACQUIRE_FENCE_SCOPE) | (HSA_FENCE_SCOPE_SYSTEM << HSA_PACKET_HEADER_RELEASE_FENCE_SCOPE); + __atomic_store_n((uint32_t*)packet, header | ((uint32_t)packet->setup << 16), __ATOMIC_RELEASE); + hsa_signal_store_screlease(queue->doorbell_signal, (hsa_signal_value_t)index); + if (hsa_signal_wait_scacquire(done, HSA_SIGNAL_CONDITION_LT, 1, 5000000000ull, HSA_WAIT_STATE_BLOCKED) != 0) { + fprintf(stderr, "kernel timed out\n"); + return 1; + } + if (coarse) { + CHECK(hsa_memory_copy(outStage, out, outSize)); + out = outStage; + } + FILE* f = fopen(argv[4], "wb"); + if (!f || fwrite(out, 1, outSize, f) != outSize) { perror(argv[4]); return 1; } + fclose(f); + return 0; +} diff --git a/tools/hw-oracle/template.s b/tools/hw-oracle/template.s new file mode 100644 index 000000000..ad5426247 --- /dev/null +++ b/tools/hw-oracle/template.s @@ -0,0 +1,70 @@ +.amdgcn_target "amdgcn-amd-amdhsa--@TARGET@" +.text +.globl probe +.p2align 8 +.type probe,@function +probe: + s_load_dwordx4 s[4:7], s[0:1], 0x0 + v_lshlrev_b32 v1, 4, v0 + v_lshlrev_b32 v2, 6, v0 + s_waitcnt lgkmcnt(0) + global_load_dwordx4 v[4:7], v1, s[4:5] + s_waitcnt vmcnt(0) + v_mov_b32 v10, 0 + v_mov_b32 v11, 0 + v_mov_b32 v12, 0 + v_mov_b32 v13, 0 + v_mov_b32 v14, 0 + v_mov_b32 v15, 0 + v_mov_b32 v16, 0 + v_mov_b32 v17, 0 + v_mov_b32 v18, 0 + v_mov_b32 v19, 0 + v_mov_b32 v20, 0 + v_mov_b32 v21, 0 + v_mov_b32 v22, 0 + v_mov_b32 v23, 0 + v_mov_b32 v24, 0 + v_mov_b32 v25, 0 +@BODY@ + global_store_dwordx4 v2, v[10:13], s[6:7] + global_store_dwordx4 v2, v[14:17], s[6:7] offset:16 + global_store_dwordx4 v2, v[18:21], s[6:7] offset:32 + global_store_dwordx4 v2, v[22:25], s[6:7] offset:48 + s_endpgm +.Lend: +.size probe, .Lend-probe + +.rodata +.p2align 6 +.amdhsa_kernel probe + .amdhsa_user_sgpr_kernarg_segment_ptr 1 + .amdhsa_kernarg_size 16 + .amdhsa_group_segment_fixed_size 4096 + .amdhsa_next_free_vgpr 128 + .amdhsa_next_free_sgpr 48 + .amdhsa_wavefront_size32 @WAVE32@ + .amdhsa_float_denorm_mode_32 @DENORM@ + .amdhsa_float_denorm_mode_16_64 @DENORM16@ + .amdhsa_ieee_mode @IEEE@ +.end_amdhsa_kernel + +.amdgpu_metadata +--- +amdhsa.version: [ 1, 2 ] +amdhsa.kernels: + - .name: probe + .symbol: probe.kd + .kernarg_segment_size: 16 + .group_segment_fixed_size: 4096 + .private_segment_fixed_size: 0 + .kernarg_segment_align: 8 + .wavefront_size: @WAVESIZE@ + .sgpr_count: 48 + .vgpr_count: 64 + .max_flat_workgroup_size: 1024 + .args: + - { .size: 8, .offset: 0, .value_kind: global_buffer, .address_space: global } + - { .size: 8, .offset: 8, .value_kind: global_buffer, .address_space: global } +... +.end_amdgpu_metadata diff --git a/tools/nid_names.py b/tools/nid_names.py index 9697adbcd..2eb43a804 100644 --- a/tools/nid_names.py +++ b/tools/nid_names.py @@ -19,7 +19,7 @@ CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-" SUFFIX = bytes([0x51, 0x8D, 0x64, 0xA6, 0x35, 0xDE, 0xD8, 0xC1, 0xE6, 0xB0, 0x39, 0xB1, 0xC3, 0xE5, 0x52, 0x30]) ALIAS = re.compile(r'APS5_EXPORT\("([A-Za-z0-9+\-]{11})",\s*(\w*[Uu]nknown\w*)\)') -DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?\{") +DEFINITION = re.compile(r"\bAPS5_VABI\s+(\w+)\s*\([^;{]*\)\s*(?:noexcept\s*)?(?:try\s*)?\{") NID_POSTFIX = "_nid_postfix" diff --git a/tools/package_release.py b/tools/package_release.py index 25296672b..53add7365 100644 --- a/tools/package_release.py +++ b/tools/package_release.py @@ -11,7 +11,6 @@ def package(platform, build, output, version): raise ValueError(f"Invalid release tag for asset filenames: {version}") libraries = sorted((build / "core/libs/libs").glob("*.prx")) expected = {f"{directory.name}.prx" for directory in Path("core/libs/prx").iterdir() if directory.is_dir()} - expected.add("libcohtml.Prospero.prx") missing = expected - {library.name for library in libraries} if missing: raise RuntimeError(f"Missing patched libraries: {', '.join(sorted(missing))}") diff --git a/tools/progress.py b/tools/progress.py index 81ceeea05..fcb383408 100644 --- a/tools/progress.py +++ b/tools/progress.py @@ -26,7 +26,7 @@ OPCODE_ALIASES = { "VMadMixhiF16": "V_FMA_MIXHI_F16", } OPCODE_VARIANTS = { - "SEndpgm": ("S_ENDPGM_SAVED", "S_ENDPGM_ORDERED_PS_DONE"), + "SEndpgm": ("S_ENDPGM_SAVED", "S_ENDPGM_ORDERED_PS_DONE", "S_SETKILL"), "VFmaF32": ("V_FMA_MIX_F32",), "SAddI32": ("S_ADDK_I32",), "SCmpEqI32": ("S_CMPK_EQ_I32",), @@ -53,6 +53,10 @@ OPCODE_VARIANTS = { "VMacLegacyF32": ("V_FMAC_LEGACY_F32",), "VMadLegacyF32": ("V_FMA_LEGACY_F32",), "ImageSample": ("IMAGE_SAMPLE_L", "IMAGE_SAMPLE_B", "IMAGE_SAMPLE_C_LZ", "IMAGE_SAMPLE_L_O", "IMAGE_SAMPLE_D_CL_O"), + "ImageLoadPck": ("IMAGE_LOAD_PCK2", "IMAGE_LOAD_PCK4"), + "ImageLoadMipPck": ("IMAGE_LOAD_MIP_PCK2", "IMAGE_LOAD_MIP_PCK4"), + "ImageStorePck": ("IMAGE_STORE_PCK2", "IMAGE_STORE_PCK4"), + "ImageStoreMipPck": ("IMAGE_STORE_MIP_PCK2", "IMAGE_STORE_MIP_PCK4"), } REPORT_ROWS = 100 PANEL_WIDTH, GAP, MAP_HEIGHT, HEADER = 495, 10, 280, 30