diff --git a/core/libs/prx/libSceAgc/CMakeLists.txt b/core/libs/prx/libSceAgc/CMakeLists.txt index a253010d1..4beb1e427 100644 --- a/core/libs/prx/libSceAgc/CMakeLists.txt +++ b/core/libs/prx/libSceAgc/CMakeLists.txt @@ -83,6 +83,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_packet_info_tests EXCLUDE_FROM_ALL tests/PacketInfo.cpp) +target_include_directories(agc_packet_info_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_packet_info_tests PRIVATE libSceAgc libc) +configure_windows_unwind(agc_packet_info_tests) +if(BUILD_TESTING) + set_target_properties(agc_packet_info_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_packet_info_tests COMMAND agc_packet_info_tests) + if(WIN32) + set_tests_properties(agc_packet_info_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_register_command_tests EXCLUDE_FROM_ALL tests/RegisterCommands.cpp Command/src/Packet.cpp @@ -102,6 +115,19 @@ if(BUILD_TESTING) 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) +configure_windows_unwind(agc_payload_range_tests) +if(BUILD_TESTING) + set_target_properties(agc_payload_range_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_payload_range_tests COMMAND agc_payload_range_tests) + if(WIN32) + set_tests_properties(agc_payload_range_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 @@ -220,3 +246,23 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +add_test_executable(agc_cond_exec_patch_tests EXCLUDE_FROM_ALL + tests/CondExecPatch.cpp + Command/src/Packet.cpp + Command/src/Memory.cpp + Command/src/Control.cpp + DcbFlow/src/Sync.cpp + Patch/src/CondExec.cpp +) +target_include_directories(agc_cond_exec_patch_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(agc_cond_exec_patch_tests PRIVATE libc) +configure_windows_unwind(agc_cond_exec_patch_tests) +if(BUILD_TESTING) + set_target_properties(agc_cond_exec_patch_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME agc_cond_exec_patch_tests COMMAND agc_cond_exec_patch_tests) + if(WIN32) + set_tests_properties(agc_cond_exec_patch_tests PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp b/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp index 535ebefb4..85a76dced 100644 --- a/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp +++ b/core/libs/prx/libSceAgc/Misc/include/PacketInfo.hpp @@ -1,4 +1,11 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_PACKETINFO_HPP #define CORE_LIBS_PRX_LIBSCEAGC_MISC_INCLUDE_PACKETINFO_HPP +#include + +struct SceAgcMemoryRange { + void* base; + uint64_t size; +}; + #endif diff --git a/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp b/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp index 12f74258a..7b7dff0f4 100644 --- a/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp +++ b/core/libs/prx/libSceAgc/Misc/src/PacketInfo.cpp @@ -9,9 +9,12 @@ extern "C" { uint32_t APS5_VABI sceAgcGetPacketSize(uint32_t* packet) { - (void)packet; - NotImplemented_nid_no_patch(__func__); - return 0; + Agc::Command::CheckAddress(reinterpret_cast(packet), alignof(uint32_t), __func__); + Agc::Command::Require((packet[0] & 0xc0000000u) == 0xc0000000u, __func__, "source pointer is not a PM4 packet"); + if ((packet[0] & 0x3fffff00u) == 0x3fff1000u) { + return 1; + } + return ((packet[0] >> 16u) & 0x3fffu) + 2u; } APS5_EXPORT("V++UgBtQhn0", sceAgcGetDataPacketPayloadAddressUnk); @@ -26,6 +29,23 @@ int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(uint32_t** addr, uint32_t* cm return 0; } +int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, uint32_t* cmd, int type) { + Agc::Command::CheckAddress(reinterpret_cast(range), alignof(SceAgcMemoryRange), __func__); + Agc::Command::CheckAddress(reinterpret_cast(cmd), alignof(uint32_t), __func__); + const uint64_t count_bytes = (cmd[0] >> 14u) & 0xfffcu; + if (type != 0) { + range->base = cmd + 2; + range->size = count_bytes; + } else if ((~cmd[0] & 0x3fff0000u) == 0) { + range->base = nullptr; + range->size = 0; + } else { + range->base = cmd + 1; + range->size = count_bytes + sizeof(uint32_t); + } + return 0; +} + int APS5_VABI sceAgcDcbGetSystemSoftwareVersion(uint32_t* destination, const uint32_t* packet) { Agc::Command::CheckAddress(reinterpret_cast(destination), alignof(uint32_t), __func__); Agc::Command::CheckAddress(reinterpret_cast(packet), alignof(uint32_t), __func__); diff --git a/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp b/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp index 42b45d366..bae2206ad 100644 --- a/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp +++ b/core/libs/prx/libSceAgc/Patch/src/CondExec.cpp @@ -10,17 +10,23 @@ extern "C" { int APS5_VABI sceAgcCondExecPatchSetCommandAddress(uint32_t* cmd, const volatile uint32_t* command) { - (void)cmd; - (void)command; - NotImplemented_nid_no_patch(__func__); - return 0; + Agc::Command::ValidatePacket(cmd, 0x22u, 5, __func__); + const auto address = reinterpret_cast(command); + Agc::Command::CheckAddress(address, 4, __func__); + 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) { - (void)cmd; - (void)buffer; - NotImplemented_nid_no_patch(__func__); - return 0; + Agc::Command::ValidatePacket(cmd, 0x22u, 5, __func__); + const auto end = reinterpret_cast(buffer); + const auto packetEnd = reinterpret_cast(cmd + 5); + Agc::Command::CheckAddress(end, 4, __func__); + const auto numDwords = (end - packetEnd) / sizeof(std::uint32_t); + Agc::Command::Require(numDwords <= 0x3fffu, __func__, "conditional execution range is too large"); + cmd[4] = (cmd[4] & ~0x3fffu) | static_cast(numDwords); + return 0; } int APS5_VABI sceAgcAsyncCondExecPatchSetCommandAddress(std::uint32_t* cmd, const volatile std::uint32_t* command) { diff --git a/core/libs/prx/libSceAgc/Unimplemented.cpp b/core/libs/prx/libSceAgc/Unimplemented.cpp index dea38a6a4..2e2b0532f 100644 --- a/core/libs/prx/libSceAgc/Unimplemented.cpp +++ b/core/libs/prx/libSceAgc/Unimplemented.cpp @@ -55,11 +55,6 @@ int APS5_VABI sceAgcDebugRaiseException() { return 0; } -int APS5_VABI sceAgcGetDataPacketPayloadRange() { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI sceAgcGetDefaultCxStateFlat() { NotImplemented_nid_no_patch(__func__); return 0; @@ -137,4 +132,10 @@ int APS5_VABI sceAgcAsyncWriteDataPatchSetCachePolicy() { return 0; } +APS5_EXPORT("vieBRwlh1Lw", sceAgcUnknown_vieBRwlh1Lw); +int APS5_VABI sceAgcUnknown_vieBRwlh1Lw(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp b/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp new file mode 100644 index 000000000..f338beea7 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/CondExecPatch.cpp @@ -0,0 +1,107 @@ +#include "prx/libSceAgc/Command/include/Packet.hpp" + +#include +#include +#include +#include +#include +#include + +extern "C" { +std::uint32_t* APS5_VABI sceAgcDcbCondExec(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*); +} + +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 std::uint32_t* 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}; + std::uint32_t condition = 1; + std::uint32_t* packet = nullptr; + + Storage() { + words.fill(Sentinel); + packet = sceAgcDcbCondExec(&buffer, &condition, 3); + check(packet == words.data() && packet[0] == 0xc0032200u && packet[4] == 3, "unexpected COND_EXEC packet"); + } +}; + +std::uintptr_t endAfter(const std::uint32_t* packet, std::uintptr_t numDwords) { + return reinterpret_cast(packet + 5) + numDwords * sizeof(std::uint32_t); +} + +template +void expectUnchanged(TAction action) { + Storage storage; + const auto before = storage.words; + expectFailure([&] { action(storage.packet); }); + check(storage.words == before, "failed patch modified the packet"); +} + +void testSetEnd() { + for (const std::uintptr_t numDwords : {0u, 1u, 7u, 0x3fffu}) { + Storage storage; + const auto before = storage.words; + check(sceAgcCondExecPatchSetEnd(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))); }); +} + +void testSetCommandAddress() { + for (const std::uintptr_t address : {std::uintptr_t{0x0000123456789abcu}, std::uintptr_t{4u}, std::uintptr_t{0x0000fffffffffffcu}}) { + Storage storage; + 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(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)); }); +} + +} + +int main() { + try { + testSetEnd(); + testSetCommandAddress(); + std::puts("AGC COND_EXEC 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/libSceAgc/tests/PacketInfo.cpp b/core/libs/prx/libSceAgc/tests/PacketInfo.cpp new file mode 100644 index 000000000..5e95906b5 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/PacketInfo.cpp @@ -0,0 +1,66 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include + +extern "C" std::uint32_t APS5_VABI sceAgcGetPacketSize(std::uint32_t* packet); + +namespace { + +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& error) { + check(error.what()[0] != '\0', "empty exception message"); + return; + } + throw std::runtime_error("expected an exception"); +} + +void testPacketSize() { + const std::array, 7> sizes{{ + {0xc0047600u, 6}, {0xc0001000u, 2}, {0xfffe1000u, 0x4000}, {0xffff7600u, 0x4001}, + {0xffff1000u, 1}, {0xffff1001u, 1}, {0xffff10fcu, 1}}}; + for (const auto& [header, size] : sizes) { + std::array words{header, 0xffff1000u}; + check(sceAgcGetPacketSize(words.data()) == size, "packet size mismatch"); + check(words[0] == header && words[1] == 0xffff1000u, "packet size query modified the packet"); + } +} + +void testRejections() { + for (const auto header : {0x80000000u, 0x3fff1000u, 0x40001000u, 0x00047600u}) { + std::uint32_t word = header; + expectFailure([&] { sceAgcGetPacketSize(&word); }); + } + expectFailure([] { sceAgcGetPacketSize(nullptr); }); + std::array words{0x00100000u, 0x000000c0u}; + auto* misaligned = reinterpret_cast(reinterpret_cast(words.data()) + 1); + expectFailure([&] { sceAgcGetPacketSize(misaligned); }); +} + +} + +int main() { + try { + testPacketSize(); + testRejections(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC packet info tests passed"); + return 0; + } 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/libSceAgc/tests/PayloadRange.cpp b/core/libs/prx/libSceAgc/tests/PayloadRange.cpp new file mode 100644 index 000000000..97a386cc3 --- /dev/null +++ b/core/libs/prx/libSceAgc/tests/PayloadRange.cpp @@ -0,0 +1,80 @@ +#include "prx/libSceAgc/Misc/include/PacketInfo.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/Shutdown.hpp" + +#include +#include +#include +#include +#include + +extern "C" int APS5_VABI sceAgcGetDataPacketPayloadRange(SceAgcMemoryRange* range, std::uint32_t* cmd, int type); +extern "C" int APS5_VABI sceAgcGetDataPacketPayloadAddressUnk(std::uint32_t** addr, std::uint32_t* cmd, int type); + +namespace { + +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& error) { + check(error.what()[0] != '\0', "empty exception message"); + return; + } + throw std::runtime_error("expected an exception"); +} + +void testRanges() { + const std::array, 10> rows{{ + {0xc0047600u, 1, 2, 16}, {0xc0001000u, 1, 2, 0}, {0xffff7600u, 1, 2, 0xfffc}, {0xc0047600u, -1, 2, 16}, + {0xc0047600u, 7, 2, 16}, {0xc0021000u, 0, 1, 12}, {0xc0001000u, 0, 1, 4}, {0xfffe1000u, 0, 1, 0xfffc}, + {0xffff1000u, 0, -1, 0}, {0xffff7600u, 0, -1, 0}}}; + for (const auto& [header, type, offset, size] : rows) { + std::array words{header, 0x12345678u}; + SceAgcMemoryRange range{reinterpret_cast(std::uintptr_t{0x1000}), 0xdeadu}; + check(sceAgcGetDataPacketPayloadRange(&range, words.data(), type) == 0, "payload range failed"); + void* expected = offset < 0 ? nullptr : static_cast(words.data() + offset); + check(range.base == expected, "payload range base mismatch"); + check(range.size == size, "payload range size mismatch"); + check(words[0] == header && words[1] == 0x12345678u, "payload range query modified the packet"); + std::uint32_t* address = nullptr; + check(sceAgcGetDataPacketPayloadAddressUnk(&address, words.data(), type) == 0 && address == range.base, + "payload range base differs from the payload address"); + } +} + +void testRejections() { + std::array words{0xc0047600u, 0u}; + SceAgcMemoryRange range{}; + expectFailure([&] { sceAgcGetDataPacketPayloadRange(nullptr, words.data(), 1); }); + expectFailure([&] { sceAgcGetDataPacketPayloadRange(&range, nullptr, 1); }); + auto* misaligned = reinterpret_cast(reinterpret_cast(words.data()) + 1); + expectFailure([&] { sceAgcGetDataPacketPayloadRange(&range, misaligned, 0); }); + std::array storage{}; + auto* misalignedRange = reinterpret_cast(storage.data() + 4); + if (reinterpret_cast(misalignedRange) % alignof(SceAgcMemoryRange) == 0) { + misalignedRange = reinterpret_cast(storage.data() + 2); + } + expectFailure([&] { sceAgcGetDataPacketPayloadRange(misalignedRange, words.data(), 1); }); +} + +} + +int main() { + try { + testRanges(); + testRejections(); + LibcRunShutdown_nid_postfix(); + std::puts("AGC payload range tests passed"); + return 0; + } 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/CMakeLists.txt b/core/libs/prx/libSceAgcDriver/CMakeLists.txt index b373847f0..65091c14a 100644 --- a/core/libs/prx/libSceAgcDriver/CMakeLists.txt +++ b/core/libs/prx/libSceAgcDriver/CMakeLists.txt @@ -303,6 +303,20 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(agc_display_buffer_dcc_tests EXCLUDE_FROM_ALL tests/DisplayBufferDcc.cpp) +target_include_directories(agc_display_buffer_dcc_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/core/shader/recompiler ${CMAKE_SOURCE_DIR}/3rdparty/Vulkan-Headers/include) +target_link_libraries(agc_display_buffer_dcc_tests PRIVATE libSceAgcDriver libc) +set_target_properties(agc_display_buffer_dcc_tests PROPERTIES CXX_EXTENSIONS OFF) +configure_windows_unwind(agc_display_buffer_dcc_tests) +if(BUILD_TESTING) + set_target_properties(agc_display_buffer_dcc_tests PROPERTIES EXCLUDE_FROM_ALL OFF) + add_test(NAME agc_display_buffer_dcc COMMAND agc_display_buffer_dcc_tests) + if(WIN32) + set_tests_properties(agc_display_buffer_dcc PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(agc_validate_program_tests EXCLUDE_FROM_ALL tests/ValidateProgram.cpp) target_link_libraries(agc_validate_program_tests PRIVATE shader_recompiler_agc_driver libc) set_target_properties(agc_validate_program_tests PROPERTIES CXX_EXTENSIONS OFF) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp index b7f70c3d8..5c3ef004f 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DISPLAYBUFFER_HPP #define CORE_LIBS_PRX_LIBSCEAGCDRIVER_EXECUTION_INCLUDE_DISPLAYBUFFER_HPP +#include #include #include #include @@ -8,6 +9,10 @@ namespace AgcDriver { +namespace Graphics { +enum class DccKeys; +} + struct DisplayBuffer { std::uint64_t address; std::uint64_t pixelFormat; @@ -15,11 +20,14 @@ struct DisplayBuffer { std::uint32_t height; std::uint32_t tilingMode = 0; std::uint32_t pitchInPixel = 0; + std::uint64_t dccAddress = 0; + std::uint64_t dccClearColor = 0; }; std::size_t DisplayBufferSize(const DisplayBuffer& buffer); std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::span source); std::vector ReadDisplayBuffer(const DisplayBuffer& buffer); +std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp index c413e357e..31f780e61 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/Pm4.hpp @@ -88,6 +88,8 @@ bool AccessesMemory(std::uint32_t header); // synchronization): such a packet needs a pipeline barrier, not a device drain. bool UsesGpuCacheBarrier(std::span packet); bool IsTagMarker(std::span packet); +inline bool Predicated(std::uint32_t header) { return !FillerPacket(header) && (header & 1u) != 0; } +bool PredicationPasses(const QueueState& queue); bool WaitSatisfied(std::span packet); // WaitSatisfied for a polling loop: the caller has validated the address once, so the value is read // directly instead of through the checked guest memory path. diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp index d56076311..35e8ed497 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp @@ -17,6 +17,7 @@ public: void EnsureSourceImage(std::uint32_t width, std::uint32_t height); void RecordUpload(VkCommandBuffer commands, VkBuffer uploadBuffer); + void RecordClear(VkCommandBuffer commands, const VkClearColorValue& color); void RecordImage(VkCommandBuffer commands, VkImage image); // Fills the source image from another image of the same extent (any blittable format, layout // unchanged), leaving the source image ready to blit or read back. diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp index 6d18f6228..19493fc5e 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/QueueState.hpp @@ -167,6 +167,12 @@ inline Registers InitialContextRegisters() { return result; } +struct Predication { + std::uint64_t address = 0; + std::uint32_t operation = 0; + bool executeWhenSet = false; +}; + struct QueueState { Registers shader; Registers context = InitialContextRegisters(); @@ -180,6 +186,7 @@ struct QueueState { std::uint32_t indexType = 0; std::uint32_t instanceCount = 1; std::vector markers; + Predication predication; void ClearContext() { context = InitialContextRegisters(); diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp index dc519ff5b..0064178b9 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp @@ -307,7 +307,7 @@ private: // The tail of a dispatch's device call from the open batch's command buffer to the completion // registration: keeps, the template data refresh, barriers, bind, push, dispatch, marks. void recordDispatch(RecordedDispatch& record); - bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false); + bool present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display = nullptr, const std::shared_ptr& resident = nullptr, VkFilter residentFilter = VK_FILTER_LINEAR, bool dumpFrame = false, bool residentConvert = false, const VkClearColorValue* uniform = nullptr); struct State; std::unique_ptr state; std::uint64_t serial; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp index 837b13f02..459b6e228 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/DisplayBuffer.cpp @@ -1,6 +1,9 @@ #include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Execution/include/GuestMemory.hpp" #include "prx/libSceAgcDriver/Execution/include/PerformanceTimer.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include #include #include #include @@ -28,6 +31,25 @@ std::uint32_t tileOffset(std::uint32_t x, std::uint32_t y) { return ((y << 4u) & 0x0070u) ^ ((y << 5u) & 0x0f00u) ^ ((y << 9u) & 0x1000u) ^ ((y << 8u) & 0x4000u) ^ ((x << 2u) & 0x000cu) ^ ((x << 5u) & 0x0380u) ^ ((x << 4u) & 0x0400u) ^ ((x << 6u) & 0x0800u) ^ ((x << 9u) & 0xa000u); } +void decodeTexel(const std::byte* texel, std::byte* pixel, bool rgba, bool tenBit) { + if (tenBit) { + std::uint32_t word = 0; + std::memcpy(&word, texel, 4); + const auto red = static_cast((word >> 2u) & 0xffu); + const auto green = static_cast((word >> 12u) & 0xffu); + const auto blue = static_cast((word >> 22u) & 0xffu); + pixel[0] = rgba ? blue : red; + pixel[1] = green; + pixel[2] = rgba ? red : blue; + pixel[3] = std::byte{0xff}; + return; + } + pixel[0] = texel[rgba ? 2 : 0]; + pixel[1] = texel[1]; + pixel[2] = texel[rgba ? 0 : 2]; + pixel[3] = texel[3]; +} + } std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { @@ -40,6 +62,8 @@ std::size_t DisplayBufferSize(const DisplayBuffer& buffer) { require(buffer.address != 0 && (buffer.address & 65535u) == 0, "VideoOut: display buffer requires 64 KiB alignment"); require(buffer.tilingMode <= 1, "VideoOut: unsupported display tiling mode"); require(buffer.tilingMode != 0 || buffer.pitchInPixel == 0, "VideoOut: tiled display pitch is unsupported"); + require(buffer.dccAddress == 0 || buffer.tilingMode == 0, "VideoOut: a linear display buffer cannot carry DCC metadata"); + require(buffer.dccAddress != 0 || buffer.dccClearColor == 0, "VideoOut: a DCC clear color needs DCC metadata"); const auto pitch = buffer.pitchInPixel == 0 ? buffer.width : buffer.pitchInPixel; require(pitch >= buffer.width && pitch <= 16384, "VideoOut: invalid linear display pitch"); const auto size = buffer.tilingMode == 1 ? static_cast(pitch) * buffer.height * 4u @@ -64,28 +88,35 @@ std::vector DecodeDisplayBuffer(const DisplayBuffer& buffer, std::spa const auto tiled = buffer.tilingMode == 1 ? (static_cast(y) * pitch + x) * 4u : (static_cast(y / 128u) * blocksPerRow + x / 128u) * 65536u + tileOffset(x, y); const auto linear = (static_cast(y) * buffer.width + x) * 4; - if (tenBit) { - std::uint32_t word = 0; - std::memcpy(&word, source.data() + tiled, 4); - const auto red = static_cast((word >> 2u) & 0xffu); - const auto green = static_cast((word >> 12u) & 0xffu); - const auto blue = static_cast((word >> 22u) & 0xffu); - pixels[linear] = rgba ? blue : red; - pixels[linear + 1] = green; - pixels[linear + 2] = rgba ? red : blue; - pixels[linear + 3] = std::byte{0xff}; - continue; - } - pixels[linear] = source[tiled + (rgba ? 2 : 0)]; - pixels[linear + 1] = source[tiled + 1]; - pixels[linear + 2] = source[tiled + (rgba ? 0 : 2)]; - pixels[linear + 3] = source[tiled + 3]; + decodeTexel(source.data() + tiled, pixels.data() + linear, rgba, tenBit); } } timing.Mark("detile_convert"); return pixels; } +std::array DisplayBufferClearPixel(const DisplayBuffer& buffer, Graphics::DccKeys keys) { + static_cast(DisplayBufferSize(buffer)); + require(buffer.dccAddress != 0, "VideoOut: a display buffer without DCC metadata has no fast-clear value"); + std::array texel{}; + if (keys == Graphics::DccKeys::ClearRegister) { + if ((buffer.dccClearColor >> 32u) != 0) { + char message[160]; + std::snprintf(message, sizeof(message), "VideoOut: DCC register clear color 0x%016llx of display buffer 0x%llx does not fit its 32-bit texel", static_cast(buffer.dccClearColor), static_cast(buffer.address)); + throw std::runtime_error(message); + } + const auto word = static_cast(buffer.dccClearColor); + std::memcpy(texel.data(), &word, texel.size()); + } else if (!Graphics::IsDccClear(keys) || !Graphics::FillDccClear(DisplayTexelFormat(buffer.pixelFormat), keys, true, texel)) { + char message[256]; + std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx (format 0x%016llx) reads %s DCC keys at 0x%llx: only uncompressed keys or a uniform clear code that the display format encodes can be presented", static_cast(buffer.address), static_cast(buffer.pixelFormat), Graphics::DccKeysName(keys), static_cast(buffer.dccAddress)); + throw std::runtime_error(message); + } + std::array pixel{}; + decodeTexel(texel.data(), pixel.data(), baseFormat(buffer.pixelFormat) == PixelFormatR8G8B8A8, (buffer.pixelFormat & PixelFormatUnormBit) != 0); + return pixel; +} + std::vector ReadDisplayBuffer(const DisplayBuffer& buffer) { const auto size = DisplayBufferSize(buffer); static const bool traceGpu = std::getenv("APS5_TRACE_GPU") != nullptr; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp index 64ec8646f..cdc931174 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawLookup.cpp @@ -62,6 +62,7 @@ void Driver::lookupDraw(const Submission& submission, const std::shared_ptr(pixel.conservativeZExport)); for (const auto value : pixel.targetOutputMode) mix(value); for (const auto value : pixel.targetExportMapping) mix(value); std::lock_guard cacheLock(drawCacheMutex); diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp index 27d567cd6..5181d61f5 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawRegisterKey.cpp @@ -81,7 +81,7 @@ bool Driver::sameDecode(const DrawDecode& a, const DrawDecode& b) { if (std::memcmp(&s.viewport, &t.viewport, sizeof(VkViewport)) != 0 || std::memcmp(&s.scissor, &t.scissor, sizeof(VkRect2D)) != 0) return false; const auto& p = a.pixel; const auto& q = b.pixel; - if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; + if (p.interpolatorCount != q.interpolatorCount || p.interpolatorSettings != q.interpolatorSettings || p.wave32 != q.wave32 || p.inputAddr != q.inputAddr || p.hasPerspectiveCenterVgpr != q.hasPerspectiveCenterVgpr || p.perspectiveCentroid != q.perspectiveCentroid || p.posX != q.posX || p.posY != q.posY || p.posZ != q.posZ || p.posW != q.posW || p.frontFace != q.frontFace || p.ancillary != q.ancillary || p.sampleShading != q.sampleShading || p.noPerspective != q.noPerspective || p.linearCentroid != q.linearCentroid || p.pixelKillEnable != q.pixelKillEnable || p.depthExportEnable != q.depthExportEnable || p.sampleMaskExportEnable != q.sampleMaskExportEnable || p.earlyZ != q.earlyZ || p.executeOnNoop != q.executeOnNoop || p.conservativeZExport != q.conservativeZExport || p.targetOutputMode != q.targetOutputMode || p.targetExportMapping != q.targetExportMapping) return false; if (a.roles != b.roles || a.programs.size() != b.programs.size()) return false; for (std::size_t i = 0; i < a.programs.size(); ++i) { const auto& x = a.programs[i]; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp index 06dcb02cb..3c5dcd148 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketExecution.cpp @@ -117,6 +117,17 @@ void Driver::execute(const Submission& submission) { GuestMemory::SetCurrentPacket(header == FlipPacketHeader ? 0xffffu : opcode, submission.queue); CaptureTrace::Log("packet submission=%llu queue=%x offset=%zu header=%08x words=%zu", static_cast(submission.serial), submission.queue, cursor, header, packet.size()); + if (Pm4::Predicated(header) && queue.predication.operation != 0) { + recordQueuedLabelsBeforeRead(submission.queue); + if (!Pm4::PredicationPasses(queue)) { + cursor = opcode == 0x3f ? submission.conditionalEnds.at(cursor) : nextCursor; + continue; + } + } + if (opcode == 0x3f) { + cursor = nextCursor; + continue; + } const auto flushStart = profilePackets ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; flushBetweenPackets(submission.queue, header, opcode == 0x49 || opcode == 0x37); 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 edd9b5b70..cdc6d68b8 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Driver/Queues/Submission.cpp @@ -50,6 +50,19 @@ bool Driver::copySegment(Submission& submission, const std::uint32_t* guest, std const bool chain = (guest[cursor + 3] & (1u << 20u)) != 0; require(!chain || guarded.empty(), "a chained INDIRECT_BUFFER inside a conditional execution range is not implemented"); GuestMemory::CheckRange(target, targetWords * sizeof(std::uint32_t), alignof(std::uint32_t)); + if (Pm4::Predicated(header)) { + require(!chain, "predicated command buffer chains are not implemented"); + const auto start = submission.commands.size(); + submission.commands.insert(submission.commands.end(), guest + cursor, guest + cursor + count); + require(!copySegment(submission, target, targetWords, budget), "a REWIND inside a predicated command buffer is not implemented"); + for (auto inner = start + count; inner < submission.commands.size(); inner += Pm4::PacketWords(submission.commands[inner])) { + const auto innerHeader = submission.commands[inner]; + require(innerHeader != FlipPacketHeader && innerHeader != RenderingWaitPacketHeader, "flips and rendering waits in predicated command buffers are not implemented"); + } + submission.conditionalEnds.emplace(start, submission.commands.size()); + cursor += count; + continue; + } if (copySegment(submission, target, targetWords, budget)) { require(guarded.empty(), "a REWIND inside a conditional execution range is not implemented"); return true; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp index a07acee1a..26f818d92 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/Pm4.cpp @@ -166,7 +166,7 @@ std::string_view UnsupportedReason(std::uint32_t header) { } } switch (opcode) { - case 0x11: case 0x12: case 0x13: case 0x15: case 0x16: case 0x22: case 0x26: case 0x27: + 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 0x58: case 0x63: case 0x64: case 0x69: case 0x76: case 0x79: case 0x7a: case 0x81: case 0x83: case 0x9f: return {}; @@ -175,7 +175,6 @@ std::string_view UnsupportedReason(std::uint32_t header) { return {}; case 0x3a: case 0x8d: return "graphics draw, shader stages and guest render-target materialization are not implemented"; - case 0x20: return "GPU query predication is not implemented"; case 0x33: case 0x3f: return "nested command buffers, branching and nested flip reservation are not implemented"; case 0x3c: case 0x93: return {}; case 0x39: @@ -213,7 +212,7 @@ void Validate(std::span packet, std::uint32_t queue) { const auto size = [&](std::size_t count) { require(packet.size() == count, "invalid packet size"); }; const auto graphics = [&] { require(queue == 0, "graphics packet in compute queue"); }; const auto reason = UnsupportedReason(header); - if (!reason.empty()) throw std::runtime_error(std::string(reason)); + if (!reason.empty() && !(opcode == 0x3f && Predicated(header))) throw std::runtime_error(std::string(reason)); if (opcode == 0x10) { require((header & 3u) == 0, "unsupported NOP header flags"); switch ((header >> 2u) & 0x3fu) { @@ -239,11 +238,11 @@ void Validate(std::span packet, std::uint32_t queue) { return; } // Header bit 1 selects the compute shader type and bit 2 (register writes) resets the filter CAM; - // neither changes what the packet writes. Predication (bit 0) is not implemented. + // neither changes what the packet writes. const auto flags = header & 0xffu; const bool registerWrite = opcode == 0x69 || opcode == 0x76 || opcode == 0x79 || opcode == 0x7a; auto allowedFlags = opcode == 0x11 ? 2u : registerWrite ? 6u : opcode == 0x3c || opcode == 0x93 ? 2u : 0u; - if (IsTagMarker(packet)) allowedFlags |= 1u; + if (opcode != 0x20 && opcode != 0x22) allowedFlags |= 1u; if ((flags & ~allowedFlags) != 0) throw std::runtime_error("PM4 header flags 0x" + ToHex(flags) + " are not implemented"); switch (opcode) { case 0x11: @@ -252,6 +251,17 @@ void Validate(std::span packet, std::uint32_t queue) { if ((header & 2u) == 0) graphics(); break; case 0x12: graphics(); size(2); require((packet[1] & ~0xfu) == 0, "unsupported CLEAR_STATE payload bits"); break; + case 0x20: { + graphics(); + size(4); + const auto operation = (packet[1] >> 16u) & 7u; + require(operation != 1 && operation != 2, "GPU query predication is not implemented"); + require(operation == 0 || operation == 3 || operation == 4, "invalid predication operation"); + require((packet[1] & ~0x71100u) == 0, "unsupported SET_PREDICATION bits"); + if (operation != 0) require(address(packet[2], packet[3]) != 0 && (packet[2] & 0xfu) == 0, "unaligned predication address"); + break; + } + case 0x3f: size(4); break; case 0x13: case 0x2f: graphics(); size(2); break; case 0x26: graphics(); size(3); break; case 0x27: @@ -476,6 +486,16 @@ std::size_t ConditionalWords(std::span packet) { return packet[4] & ConditionalWordsMask; } +bool PredicationPasses(const QueueState& queue) { + const auto& predication = queue.predication; + if (predication.operation == 0) return true; + const std::size_t bytes = predication.operation == 3 ? 8 : 4; + std::uint64_t value = 0; + const GuestMemory::ReadSiteScope site(GuestMemory::ReadSite::Wait); + GuestMemory::Read(predication.address, std::as_writable_bytes(std::span(&value, 1)).first(bytes), bytes); + return (value != 0) == predication.executeWhenSet; +} + std::uint32_t ReadCondition(std::span packet) { require(packet.size() == 5 && ((packet[0] >> 8u) & 0xffu) == 0x22u, "expected COND_EXEC packet"); std::uint32_t value = 0; @@ -725,6 +745,12 @@ void Execute(std::span packet, QueueState& queue) { APS5_LOG_OUT_DEBUG("CLEAR_STATE targetMaskBefore=0x%x shaderMaskBefore=0x%x", queue.context.contains(0x8e) ? queue.context.at(0x8e) : 0u, queue.context.contains(0x8f) ? queue.context.at(0x8f) : 0u); queue.ClearContext(); return; case 0x13: queue.indexBufferSize = packet[1]; return; + case 0x20: { + const auto operation = (packet[1] >> 16u) & 7u; + queue.predication = {operation == 0 ? 0 : address(packet[2], packet[3]), operation, (packet[1] & 0x100u) != 0}; + return; + } + case 0x3f: throw std::runtime_error(std::string(UnsupportedReason(packet[0]))); case 0x26: queue.indexBase = address(packet[1], packet[2]); return; case 0x2a: queue.indexType = packet[1]; return; case 0x2f: queue.instanceCount = packet[1]; return; diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp index ecea37cbe..354b2f0cb 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/PresentationScaler.cpp @@ -78,6 +78,26 @@ void PresentationScaler::RecordUpload(VkCommandBuffer commands, VkBuffer uploadB pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); } +void PresentationScaler::RecordClear(VkCommandBuffer commands, const VkClearColorValue& color) { + Graphics::Require(sourceImage != VK_NULL_HANDLE, "presentation source image has not been created"); + const auto pipelineBarrier = context.Function("vkCmdPipelineBarrier"); + VkImageMemoryBarrier barrier{VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER}; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.image = sourceImage; + barrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); + context.Function("vkCmdClearColorImage")(commands, sourceImage, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &color, 1, &barrier.subresourceRange); + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; + pipelineBarrier(commands, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); +} + void PresentationScaler::RecordBlitInto(VkCommandBuffer commands, VkImage image, VkImageLayout layout, VkFilter filter) { RecordBlitInto(commands, image, layout, filter, sourceWidth, sourceHeight); } diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp index b040bbb41..8eca21737 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp @@ -5,6 +5,7 @@ #include "prx/libSceAgcDriver/Execution/include/BdaFeatures.hpp" #include "prx/libSceAgcDriver/Execution/include/PresentationScaler.hpp" #include "prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" #include "prx/libSceAgcDriver/Execution/include/DisplayFormat.hpp" #include "prx/libSceAgcDriver/Graphics/include/TextureDetiler.hpp" #include "prx/libSceAgcDriver/Graphics/include/GpuColorTransfer.hpp" @@ -1822,6 +1823,41 @@ std::shared_ptr PresentableResident(const Graphics::Co return resident; } +Graphics::DccKeys DisplayKeys(const DisplayBuffer& buffer, std::size_t bytes) { + const auto keys = Graphics::CurrentDccKeys(buffer.dccAddress, bytes); + if (keys != Graphics::DccKeys::Uncompressed && !Graphics::IsDccClear(keys)) { + char message[256]; + std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads %s DCC keys at 0x%llx: presenting DCC metadata that is not uniformly uncompressed or fast-cleared is not implemented", static_cast(buffer.address), Graphics::DccKeysName(keys), static_cast(buffer.dccAddress)); + throw std::runtime_error(message); + } + return keys; +} + +bool ResidentServesDisplay(const Graphics::StorageTexture& resident, const DisplayBuffer& buffer, std::size_t bytes) { + const auto keys = DisplayKeys(buffer, bytes); + if (keys == Graphics::DccKeys::ClearRegister) { + char message[320]; + std::snprintf(message, sizeof(message), "VideoOut: display buffer 0x%llx reads register-clear DCC keys at 0x%llx over the pending image 0x%llx (DCC 0x%llx, filled keys %s): whether its results precede the clear is not modeled", static_cast(buffer.address), static_cast(buffer.dccAddress), static_cast(resident.Descriptor().baseAddress), static_cast(resident.Descriptor().dccAddress), Graphics::DccKeysName(resident.FilledKeys())); + throw std::runtime_error(message); + } + if (Graphics::IsDccClear(resident.FilledKeys()) && resident.FilledKeys() == keys) return false; + return Graphics::StorageImageServesKeys(resident, buffer.dccAddress); +} + +bool ResidentKeysMoved(const Graphics::StorageTexture& resident) { + const auto& own = resident.Descriptor(); + if (own.dccAddress == 0) return false; + GuestMemory::CollectWritesUncached(own.dccAddress, Graphics::DccKeyBytes(resident.GuestBytes())); + return Graphics::ProvedClearKeys(own, resident.GuestBytes(), resident.KeyProof()) != resident.UploadedKeys(); +} + +std::optional> CompressedClearPixel(const DisplayBuffer& buffer, std::size_t bytes) { + GuestMemory::FlushGpuWrites(buffer.address, bytes); + const auto keys = DisplayKeys(buffer, bytes); + if (keys == Graphics::DccKeys::Uncompressed) return std::nullopt; + return DisplayBufferClearPixel(buffer, keys); +} + // Debug aid: APS5_NO_RESIDENT_PRESENT=1 always presents through guest memory. bool NoResidentPresent() { static const bool no = std::getenv("APS5_NO_RESIDENT_PRESENT") != nullptr; @@ -1871,6 +1907,10 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { if (!NoResidentPresent()) { bool pending = false; resident = PresentableResident(graphicsContext(), buffer, filter, pending, convert); + if (resident != nullptr && buffer.dccAddress != 0 && !ResidentServesDisplay(*resident, buffer, bytes)) { + resident.reset(); + convert = false; + } if (!pending) { ++notPending; } else if (resident == nullptr) { @@ -1882,18 +1922,20 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { // walk: the collect memo is per worker packet, so on this thread a memoized answer could // miss a CPU write that landed after a worker's walk of the same range. GuestMemory::CollectWritesUncached(buffer.address, bytes); - if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->Generation())) { + if (!GuestMemory::UnchangedSince(buffer.address, bytes, resident->Generation()) || (buffer.dccAddress != 0 && ResidentKeysMoved(*resident))) { resident->Refresh(); ++refreshedPresents; } ++residentPresents; } } + const auto cleared = buffer.dccAddress != 0 && resident == nullptr ? CompressedClearPixel(buffer, bytes) : std::nullopt; auto& dumps = Dumps(); bool dumpFrame = false; if (dumps.dumped < dumps.limit && ++dumps.presents % static_cast(dumps.every) == 0) { if (dumps.cpu) { - const auto full = ReadDisplayBuffer(buffer); + auto full = cleared ? std::vector(static_cast(buffer.width) * buffer.height * cleared->size()) : ReadDisplayBuffer(buffer); + for (std::size_t offset = 0; cleared && offset < full.size(); offset += cleared->size()) std::memcpy(full.data() + offset, cleared->data(), cleared->size()); WriteFrameBmp(dumps.dumped++, buffer.width, buffer.height, full, DumpScale()); } else { state->nextDumpIndex = dumps.dumped++; @@ -1906,7 +1948,12 @@ bool VulkanDevice::PresentDisplayBuffer(const DisplayBuffer& buffer) { std::fprintf(stderr, "[flip] %llu presents from the resident image (%llu refreshed first), through guest memory: %llu not pending, %llu unsuitable; %llu GPU frame dumps\n", static_cast(residentPresents), static_cast(refreshedPresents), static_cast(notPending), static_cast(unsuitable), static_cast(gpuDumps)); } CaptureTrace::Log("present dump=%d address=%llx width=%u height=%u resident=%d generation=%llu", dumpFrame ? state->nextDumpIndex : -1, static_cast(buffer.address), buffer.width, buffer.height, resident != nullptr, static_cast(resident ? resident->Generation() : 0)); - if (!present(buffer.width, buffer.height, true, {}, &buffer, resident, filter, dumpFrame, convert)) { + VkClearColorValue uniform{}; + if (cleared) { + const auto channel = [&](std::size_t index) { return static_cast(std::to_integer((*cleared)[index])) / 255.0f; }; + uniform = {{channel(2), channel(1), channel(0), channel(3)}}; + } + if (!present(buffer.width, buffer.height, true, {}, cleared ? nullptr : &buffer, resident, filter, dumpFrame, convert, cleared ? &uniform : nullptr)) { // A dropped frame (swapchain out of date) keeps the dump numbering contiguous. if (dumpFrame) --dumps.dumped; return false; @@ -1951,7 +1998,7 @@ bool VulkanDevice::AcquireImage() { return true; } -bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert) { +bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaque, std::span pixels, const DisplayBuffer* display, const std::shared_ptr& resident, VkFilter residentFilter, bool dumpFrame, bool residentConvert, const VkClearColorValue* uniform) { PerformanceTimer timing("Vulkan.Present"); APS5_LOG_OUT_DEBUG("present begin width=%u height=%u opaque=%u pixels=%zu", width, height, static_cast(opaque), pixels.size()); require(state->swapchain != VK_NULL_HANDLE, "device has no swapchain"); @@ -1965,8 +2012,9 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu const auto index = state->acquiredIndex; require(index < state->images.size() && index < state->rendered.size(), "acquired image index is out of range"); auto& rendered = state->rendered[index]; - const bool clearOnly = pixels.empty() && display == nullptr; + const bool clearOnly = pixels.empty() && display == nullptr && uniform == nullptr; require(!residentConvert || (resident != nullptr && display != nullptr), "a converted resident presentation needs its image and display buffer"); + require(uniform == nullptr || (pixels.empty() && display == nullptr && resident == nullptr), "a uniform presentation has no other source"); const bool direct = resident != nullptr && !residentConvert; // The scaler's source image, the color transfer's staging and the upload buffer are single // objects an in-flight blit through them may still read: the paths using or re-creating them @@ -2024,7 +2072,7 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu auto pipelineBarrier = state->DeviceFunction("vkCmdPipelineBarrier"); pipelineBarrier(commands, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &barrier); Graphics::Recorder::CountBarriers(CommandClass::PresentBlit); - if (pixels.empty() && display == nullptr) { + if (clearOnly) { APS5_LOG_OUT_DEBUG("Recording swapchain clear opaque=%u image=%p", static_cast(opaque), reinterpret_cast(barrier.image)); VkClearColorValue clear{}; clear.float32[3] = opaque ? 1.0f : 0.0f; @@ -2056,7 +2104,8 @@ bool VulkanDevice::present(std::uint32_t width, std::uint32_t height, bool opaqu if (display != nullptr) { state->colorTransfer->Detile(commands, DisplayRedLow(display->pixelFormat), DisplayTenBit(display->pixelFormat)); } - state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); + if (uniform != nullptr) state->scaler->RecordClear(commands, *uniform); + else state->scaler->RecordUpload(commands, display != nullptr ? state->colorTransfer->LinearBuffer() : state->uploadBuffer); } VkClearColorValue letterbox{}; letterbox.float32[3] = 1.0f; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp index 3efc2b3b2..f997d452d 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp @@ -21,6 +21,7 @@ struct Context; enum class DccKeys { Uncompressed, Clear0000, Clear0001, Clear1110, Clear1111, ClearRegister, Mixed, Unreadable }; const char* DccKeysName(DccKeys keys); +std::size_t DccKeyBytes(std::uint64_t surfaceBytes); template bool KeysServeSurface(std::uint64_t followedDcc, DccKeys uploaded, DccKeys filled, std::uint64_t namedDcc, ReadFollowed&& readFollowed, ReadNamed&& readNamed) { diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp index 43d168e2e..fb98b5ada 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DccMetadata.cpp @@ -374,6 +374,10 @@ const char* DccKeysName(DccKeys keys) { return "?"; } +std::size_t DccKeyBytes(std::uint64_t surfaceBytes) { + return static_cast(surfaceBytes / KeyBytes); +} + namespace { // ReadDccKeys, saying in `memoized` whether the answer came from the pending-store memo rather diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp index 44ee5932f..9b814bf83 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp @@ -118,8 +118,9 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con const auto shaderControl = read(context, dbShaderControl, RegisterBank::Context); // Bits 9 and 11 are EXEC_ON_HIER_FAIL and ALPHA_TO_MASK_DISABLE on GFX10; neither changes what the // recompiled pixel shader computes. - if (((shaderControl >> 13u) & 0x3u) != 0) { - throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT is unsupported"); + const auto conservativeZExport = (shaderControl >> 13u) & 0x3u; + if (conservativeZExport == 3u) { + throw std::runtime_error("AGC graphics: DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT uses the reserved value 3"); } const auto colFormat = nullProgram ? readOr(context, spiShaderColFormat, RegisterBank::Context, 0u) : read(context, spiShaderColFormat, RegisterBank::Context); std::array targetOutputMode{}; @@ -152,6 +153,7 @@ ShaderRecompiler::ShaderPixelStageInfo DecodePixelStageInfo(const Registers& con .sampleMaskExportEnable = sampleMaskExportEnable, .earlyZ = zOrder == 1u && !pixelKillEnable && !depthExportEnable && !sampleMaskExportEnable, .executeOnNoop = ((shaderControl >> 10u) & 0x1u) != 0, + .conservativeZExport = static_cast(conservativeZExport), .targetOutputMode = targetOutputMode, .targetExportMapping = exportMappings }; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp index b489fce1b..67a61cc4e 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp @@ -368,7 +368,7 @@ Module Inspect(const CompiledShader& compiled, const State& state, const VkPhysi mode(spv::ExecutionModeVertexOrderCw, {}); Require(module.modes.size() == 3, "unsupported tessellation-evaluation execution mode"); } else if (fragment) { - for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests), "unsupported fragment execution mode"); + for (const auto& [name, operands] : module.modes) Require(operands.empty() && (name == spv::ExecutionModeOriginUpperLeft || name == spv::ExecutionModeEarlyFragmentTests || name == spv::ExecutionModeDepthLess || name == spv::ExecutionModeDepthGreater), "unsupported fragment execution mode"); } else Require(module.modes.empty(), "unsupported vertex execution mode"); std::set> descriptors; bool push = false; diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp index c8d156bf5..02a893d26 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/State.cpp @@ -64,7 +64,7 @@ constexpr std::uint32_t LayerExports = (1u << 18u) | (1u << 19u) | (1u << 21u) | constexpr std::uint32_t DepthControlMask = ~0x007007f0u; // EXEC_ON_HIER_FAIL / EXEC_ON_NOOP / EXEC_IF_OVERLAPPED (bits 9, 10, 17) only force the pixel shader // to run, which it always does here. -constexpr std::uint32_t ShaderControlMask = ~(0x00009870u | 0x00020600u); +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; diff --git a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp index d7fac7e84..aea72c426 100644 --- a/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/ControlFlow.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -208,6 +209,80 @@ void verifyProgram(const Program& program) { } } +const std::vector DwordTableGetpcFirst{ + 0xbe8c1f00u, 0x800cff0cu, 0x00000058u, 0x820d800du, 0x83928202u, 0xd4c40016u, 0x02000500u, 0x8f128212u, 0xf4000386u, 0x24000000u, + 0xbf8cc07fu, 0x7d820003u, 0x808c0e0cu, 0x828d800du, 0xbefd210cu, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, + 0xe0700000u, 0x80000100u, 0xbf810000u, 0x00000020u, 0x00000018u, 0x00000010u}; +const std::vector DwordTableIndexFirst{ + 0x816ac102u, 0x83ea826au, 0x7e020280u, 0x8f6a826au, 0xbe8e1f00u, 0x800ebc0eu, 0x820f800fu, 0xf4000487u, 0xd4000008u, 0x8aea167eu, + 0xbf8cc07fu, 0x808e120eu, 0x828f800fu, 0xbefd210eu, 0x7e020281u, 0xbf820003u, 0x7e020282u, 0xbf820001u, 0x7e020283u, 0xe0700000u, + 0x80000100u, 0xbf810000u, 0x00000018u, 0x00000010u, 0x00000008u}; +const std::vector LongBranchVcc{ + 0x7e020281u, 0xbeea1f00u, 0x806a906au, 0x826b806bu, 0xbefd216au, 0x7e020282u, 0xe0700000u, 0x80000100u, 0xbf810000u}; +const std::vector LongBranchBack{ + 0xbe880380u, 0x7e020280u, 0x4a020281u, 0x80089008u, 0xbf0ac008u, 0xbf840004u, 0xbe901f00u, 0x80909410u, 0x82918011u, 0xbefd2110u, + 0xe0700000u, 0x80000100u, 0xbf810000u}; + +std::vector patched(std::vector code, std::size_t word, std::initializer_list replacement) { + std::copy(replacement.begin(), replacement.end(), code.begin() + static_cast(word)); + return code; +} + +std::string refusal(std::span code) { + try { + static_cast(GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(code))); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +void verifyJumpTable(const std::string& name, std::span code, std::uint32_t loadPc, const std::vector& values, const std::vector& targets) { + const auto graph = GraphBuilder{}.Build(RdnaInstructionDecoder{}.Decode(code)); + require(graph.codeTableLoads.size() == 1u && graph.codeTableLoads.front().programCounter == loadPc && graph.codeTableLoads.front().values == values, name + ": wrong code table"); + std::vector lowered; + for (const auto& block : graph.blocks) { + if (block.terminator.indirectPcSgpr != InvalidControlFlowId) lowered.insert(lowered.end(), block.terminator.indirectTargetProgramCounters.begin(), block.terminator.indirectTargetProgramCounters.end()); + } + std::sort(lowered.begin(), lowered.end()); + require(lowered == targets, name + ": the jump does not reach exactly the table targets"); +} + +void verifyLongBranch(const std::string& name, std::span code, std::uint32_t branchPc, std::uint32_t target) { + const auto decoded = RdnaInstructionDecoder{}.Decode(code); + const auto graph = GraphBuilder{}.Build(decoded); + const auto source = std::find_if(graph.blocks.begin(), graph.blocks.end(), [&](const BasicBlock& block) { return block.instructionEnd != block.instructionBegin && decoded.instructions[block.instructionEnd - 1u].programCounter == branchPc; }); + require(source != graph.blocks.end() && source->terminator.kind == TerminatorKind::Branch && graph.FindBlock(source->terminator.trueBlock).startProgramCounter == target, name + ": the jump does not branch to " + std::to_string(target)); +} + +void verifyNullSwappc() { + const std::array jump{0xbefd210cu, 0xbf810000u}; + const auto swappc = RdnaInstructionDecoder{}.Decode(jump).instructions.front(); + require(swappc.op == RdnaOpcode::SSetpcB64 && swappc.opcodeId == 0x21u && swappc.destination.kind == RdnaOperandKind::Null && + swappc.source0.kind == RdnaOperandKind::ScalarRegister && swappc.source0.reg == 12u, "s_swappc_b64 null, s[12:13] does not decode as s_setpc_b64 s[12:13]"); + const std::array front{0x7e020281u, 0xbefd2106u, 0xbf810000u}; + require(DecodeRdnaFrontProgram(front).code.size() == 2u, "a front program ending in s_swappc_b64 null, s[6:7] does not end there"); + verifyJumpTable("dword jump table after its base", DwordTableGetpcFirst, 0x20u, {0x20u, 0x18u, 0x10u}, {0x3cu, 0x44u, 0x4cu}); + verifyJumpTable("dword jump table after its index", DwordTableIndexFirst, 0x1cu, {0x18u, 0x10u, 0x08u}, {0x38u, 0x40u, 0x48u}); + verifyLongBranch("long branch through vcc", LongBranchVcc, 0x10u, 0x18u); + verifyLongBranch("backward long branch", LongBranchBack, 0x24u, 0x08u); + const std::array call{0xbe8c1f00u, 0xbe8e210cu, 0xbf810000u}; + const std::vector, std::string_view>> refused{ + {"s_swappc_b64 with a return address", {call.begin(), call.end()}, "unsupported SOP1 opcode 33"}, + {"index rewritten after the bound", patched(DwordTableGetpcFirst, 5, {0xbe9203ffu, 0x00000040u}), "unsupported dynamic s_setpc_b64"}, + {"index scaled for two-dword entries", patched(DwordTableGetpcFirst, 7, {0x8f128312u}), "unsupported dynamic s_setpc_b64"}, + {"entry rewritten after the load", patched(DwordTableGetpcFirst, 11, {0xbe8e0380u}), "unsupported dynamic s_setpc_b64"}, + {"base rewritten after the load", patched(DwordTableGetpcFirst, 11, {0xbe8d0380u}), "unsupported dynamic s_setpc_b64"}, + {"borrow other than zero", patched(DwordTableGetpcFirst, 13, {0x828d810du}), "unsupported dynamic s_setpc_b64"}, + {"long branch with a register high half", patched(LongBranchVcc, 3, {0x826b046bu}), "unsupported dynamic s_setpc_b64"}, + {"branch into a long branch", patched(LongBranchVcc, 0, {0xbf850001u}), "does not start in its block"}, + }; + for (const auto& [name, code, message] : refused) { + const auto error = refusal(code); + require(error.find(message) != std::string::npos, std::string(name) + ": expected a refusal with '" + std::string(message) + "', got '" + error + "'"); + } +} + } int main(int argc, char** argv) { @@ -656,6 +731,92 @@ test: {0xbe880380u, 0x7e020280u, 0xbf06c002u, 0xbf840006u, 0x4a020081u, 0xe0700000u, 0x80000100u, 0xbf810000u, 0x4a020282u, 0x80089008u, 0xbf0ac008u, 0xbf85fffcu, 0xe0700000u, 0x80000100u, 0xbf810000u}, Split::None}, + {"dword jump table after its base", R"( + s_getpc_b64 s[12:13] +base: + s_add_u32 s12, s12, table - base + s_addc_u32 s13, s13, 0 + s_min_u32 s18, s2, 2 + v_cmp_gt_u32 s[22:23], v0, s2 + s_lshl_b32 s18, s18, 2 + s_load_dword s14, s[12:13], s18 + s_waitcnt lgkmcnt(0) + v_cmp_lt_u32 vcc, s3, v0 + s_sub_u32 s12, s12, s14 + s_subb_u32 s13, s13, 0 + s_swappc_b64 null, s[12:13] +case0: + v_mov_b32 v1, 1 + s_branch done +case1: + v_mov_b32 v1, 2 + s_branch done +case2: + v_mov_b32 v1, 3 +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm +table: + .long table - case0, table - case1, table - case2)", + DwordTableGetpcFirst, Split::None}, + {"dword jump table after its index", R"( + s_add_i32 vcc_lo, s2, -1 + s_min_u32 vcc_lo, vcc_lo, 2 + v_mov_b32 v1, 0 + s_lshl_b32 vcc_lo, vcc_lo, 2 + s_getpc_b64 s[14:15] +base: + s_add_u32 s14, s14, table - base - 8 + s_addc_u32 s15, s15, 0 + s_load_dword s18, s[14:15], vcc_lo offset:8 + s_andn2_b64 vcc, exec, s[22:23] + s_waitcnt lgkmcnt(0) + s_sub_u32 s14, s14, s18 + s_subb_u32 s15, s15, 0 + s_swappc_b64 null, s[14:15] +case0: + v_mov_b32 v1, 1 + s_branch done +case1: + v_mov_b32 v1, 2 + s_branch done +case2: + v_mov_b32 v1, 3 +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm +table: + .long table - 8 - case0, table - 8 - case1, table - 8 - case2)", + DwordTableIndexFirst, Split::None}, + {"long branch through vcc", R"( + v_mov_b32 v1, 1 + s_getpc_b64 vcc +base: + s_add_u32 vcc_lo, vcc_lo, target - base + s_addc_u32 vcc_hi, vcc_hi, 0 + s_swappc_b64 null, vcc + v_mov_b32 v1, 2 +target: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + LongBranchVcc, Split::None}, + {"backward long branch", R"( + s_mov_b32 s8, 0 + v_mov_b32 v1, 0 +loop: + v_add_nc_u32 v1, 1, v1 + s_add_u32 s8, s8, 16 + s_cmp_lt_u32 s8, 64 + s_cbranch_scc0 done + s_getpc_b64 s[16:17] +base: + s_sub_u32 s16, s16, base - loop + s_subb_u32 s17, s17, 0 + s_swappc_b64 null, s[16:17] +done: + buffer_store_dword v1, off, s[0:3], 0 + s_endpgm)", + LongBranchBack, Split::None}, }; int failures = 0; for (const auto& program : programs) { @@ -666,6 +827,12 @@ test: ++failures; } } + try { + verifyNullSwappc(); + } catch (const std::exception& error) { + std::fprintf(stderr, "%s\n", error.what()); + ++failures; + } if (failures != 0) return 1; std::puts("Control flow structurization tests passed"); return 0; diff --git a/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp new file mode 100644 index 000000000..b2173b7ec --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/DisplayBufferDcc.cpp @@ -0,0 +1,83 @@ +#include "prx/libSceAgcDriver/Execution/include/DisplayBuffer.hpp" +#include "prx/libSceAgcDriver/Graphics/include/DccMetadata.hpp" +#include +#include +#include +#include +#include + +using AgcDriver::DisplayBuffer; +using AgcDriver::Graphics::DccKeys; + +namespace { + +int failures = 0; + +void Expect(bool condition, const std::string& what) { + if (condition) return; + std::fprintf(stderr, "FAIL: %s\n", what.c_str()); + ++failures; +} + +template +std::string Rejection(TAction action) { + try { + action(); + } catch (const std::exception& error) { + return error.what(); + } + return {}; +} + +void ExpectPixels(const DisplayBuffer& buffer, DccKeys keys, std::array expected, const char* what) { + const auto pixel = AgcDriver::DisplayBufferClearPixel(buffer, keys); + for (std::size_t i = 0; i < pixel.size(); ++i) { + if (std::to_integer(pixel[i]) == expected[i]) continue; + Expect(false, std::string(what) + ": byte " + std::to_string(i) + " is " + std::to_string(std::to_integer(pixel[i])) + ", expected " + std::to_string(expected[i])); + return; + } +} + +} + +int main() { + constexpr std::uint64_t bgra = 0x8000000000000000ull; + constexpr std::uint64_t rgba = 0x8000000022000000ull; + constexpr std::uint64_t tenBit = 0x0100000000000000ull; + DisplayBuffer buffer{65536, bgra, 131, 3, 0, 0, 0x7f0000, 0x11223344}; + ExpectPixels(buffer, DccKeys::Clear0000, {0, 0, 0, 0}, "8-bit 0000"); + ExpectPixels(buffer, DccKeys::Clear0001, {0, 0, 0, 255}, "8-bit 0001"); + ExpectPixels(buffer, DccKeys::Clear1110, {255, 255, 255, 0}, "8-bit 1110"); + ExpectPixels(buffer, DccKeys::Clear1111, {255, 255, 255, 255}, "8-bit 1111"); + ExpectPixels(buffer, DccKeys::ClearRegister, {0x44, 0x33, 0x22, 0x11}, "B8G8R8A8 register clear"); + buffer.pixelFormat = rgba; + ExpectPixels(buffer, DccKeys::ClearRegister, {0x22, 0x33, 0x44, 0x11}, "R8G8B8A8 register clear"); + ExpectPixels(buffer, DccKeys::Clear0001, {0, 0, 0, 255}, "R8G8B8A8 0001"); + buffer.pixelFormat = rgba | tenBit; + ExpectPixels(buffer, DccKeys::Clear1110, {255, 255, 255, 255}, "A2B10G10R10 1110"); + ExpectPixels(buffer, DccKeys::Clear0001, {0, 0, 0, 255}, "A2B10G10R10 0001"); + buffer.dccClearColor = 0x3ff; + ExpectPixels(buffer, DccKeys::ClearRegister, {0, 0, 255, 255}, "A2B10G10R10 register clear"); + buffer.pixelFormat = bgra | tenBit; + ExpectPixels(buffer, DccKeys::ClearRegister, {255, 0, 0, 255}, "A2R10G10B10 register clear"); + for (const auto keys : {DccKeys::Uncompressed, DccKeys::Mixed, DccKeys::Unreadable}) { + const auto message = Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, keys); }); + Expect(message.find(AgcDriver::Graphics::DccKeysName(keys)) != std::string::npos && message.find("0x7f0000") != std::string::npos, std::string("keys without a clear value were presented as one: ") + AgcDriver::Graphics::DccKeysName(keys)); + } + buffer.dccClearColor = 0x100000000ull; + Expect(Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, DccKeys::ClearRegister); }).find("register clear color") != std::string::npos, "a register clear color wider than the texel was truncated"); + buffer.dccClearColor = 0; + buffer.dccAddress = 0; + Expect(!Rejection([&] { AgcDriver::DisplayBufferClearPixel(buffer, DccKeys::Clear0000); }).empty(), "a buffer without DCC metadata was presented as fast-cleared"); + buffer.dccClearColor = 1; + Expect(!Rejection([&] { AgcDriver::DisplayBufferSize(buffer); }).empty(), "a DCC clear color without metadata was accepted"); + buffer.dccClearColor = 0; + buffer.dccAddress = 0x7f0000; + buffer.tilingMode = 1; + Expect(!Rejection([&] { AgcDriver::DisplayBufferSize(buffer); }).empty(), "a linear display buffer with DCC metadata was accepted"); + buffer.tilingMode = 0; + Expect(AgcDriver::DisplayBufferSize(buffer) == 2 * 65536, "DCC metadata changed the display footprint"); + if (failures != 0) return 1; + std::puts("display buffer DCC tests passed"); + return 0; +} diff --git a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp index 75b240db0..f151c35e1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Graphics.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Graphics/include/Draw.hpp" #include "prx/libSceAgcDriver/Graphics/include/ShaderInputState.hpp" #include "ControlFlow/RequestSerializer.hpp" +#include "CacheKey.hpp" #include #include #include @@ -1283,6 +1284,7 @@ void misalignedShaderDataTests() { struct ModuleShape { bool fragment = false; + std::optional fragmentMode; bool push = false; std::uint32_t pushLength = 32; std::uint32_t pushStride = 4; @@ -1446,6 +1448,7 @@ std::vector makeModule(const ModuleShape& shape) { else emit(words, spv::OpEntryPoint, {shape.fragment ? spv::ExecutionModelFragment : spv::ExecutionModelVertex, main, 0x6e69616du, 0, output}); words[entryPointOffset] += static_cast(extraInterface.size()) << 16u; words.insert(words.end(), extraInterface.begin(), extraInterface.end()); + if (shape.fragmentMode) emit(words, spv::OpExecutionMode, {main, *shape.fragmentMode}); if (shape.fragment) emit(words, spv::OpExecutionMode, {main, spv::ExecutionModeOriginUpperLeft}); words.insert(words.end(), annotations.begin(), annotations.end()); words.insert(words.end(), declarations.begin(), declarations.end()); @@ -1563,6 +1566,81 @@ std::vector locatedInputs(std::span words) { return result; } +constexpr std::array DepthExportCode{0xf8001881u, 2u, 0xbf810000u}; + +AgcDriver::QueueState depthExportQueue(std::uint32_t shaderControl) { + auto queue = makeState(); + queue.context[0x1b3] = 0x402u; + queue.context[0x1b4] = 0x402u; + queue.context[0x203] = shaderControl; + return queue; +} + +ShaderRecompiler::RecompileRequest depthExportRequest(std::uint32_t shaderControl) { + const auto queue = depthExportQueue(shaderControl); + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Fragment, 0x30000u, DepthExportCode, 0, {}}; + request.context.waveSize = 64; + request.context.pixel = AgcDriver::Graphics::DecodePixelStageInfo(queue.context, IdentityExports); + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 64; + request.layout.pushConstantSizeBytes = 128; + request.useCache = false; + return request; +} + +std::set depthModes(const ShaderRecompiler::RecompileRequest& request) { + const auto result = ShaderRecompiler::Recompile(request); + const auto& words = result.spirv.Words(); + std::set modes; + for (std::size_t at = 5; at < words.size() && (words[at] >> 16u) != 0; at += words[at] >> 16u) { + if (static_cast(words[at] & 0xffffu) != spv::OpExecutionMode) continue; + const auto mode = words[at + 2]; + if (mode == spv::ExecutionModeDepthReplacing || mode == spv::ExecutionModeDepthGreater || mode == spv::ExecutionModeDepthLess || mode == spv::ExecutionModeDepthUnchanged) modes.insert(mode); + } + return modes; +} + +void conservativeZExportTests() { + const std::array>, 4> cases{{ + {0x0021u, {spv::ExecutionModeDepthReplacing}}, + {0x2021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthLess}}, + {0x4021u, {spv::ExecutionModeDepthReplacing, spv::ExecutionModeDepthGreater}}, + {0x2020u, {}} + }}; + const ShaderRecompiler::RequestSerializer serializer; + std::set> keys; + for (const auto& [shaderControl, expected] : cases) { + const auto request = depthExportRequest(shaderControl); + const auto replayed = serializer.Deserialize(serializer.Serialize(request)); + Require(depthModes(request) == expected && depthModes(replayed.request) == expected, "DB_SHADER_CONTROL " + std::to_string(shaderControl) + " declared the wrong depth execution modes"); + std::vector key; + ShaderRecompiler::RecompileCacheKey::Build(request, key); + keys.insert(key); + } + Require(keys.size() == cases.size(), "the recompile cache key ignores CONSERVATIVE_Z_EXPORT"); + for (const auto shaderControl : {0x2020u, 0x4020u}) { + const auto queue = depthExportQueue(shaderControl); + static_cast(AgcDriver::Graphics::DecodeState(queue)); + Require(AgcDriver::Graphics::DrawRejection(queue, false).empty(), "the precheck rejected DB_SHADER_CONTROL " + std::to_string(shaderControl)); + } + expectFailure([] { static_cast(depthExportRequest(0x6021u)); }, "CONSERVATIVE_Z_EXPORT"); + AgcDriver::Graphics::State state{}; + state.stages.path = AgcDriver::Graphics::ShaderPath::Vertex; + ShaderRecompiler::RecompileResult vertex; + vertex.spirv = makeModule({}); + ShaderRecompiler::RecompileResult pixel; + const std::array shaders{{{ShaderRecompiler::ShaderStage::Vertex, &vertex, 0}, {ShaderRecompiler::ShaderStage::Fragment, &pixel, 0}}}; + const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; + for (const auto mode : {spv::ExecutionModeDepthLess, spv::ExecutionModeDepthGreater}) { + pixel.spirv = makeModule({.fragment = true, .fragmentMode = mode}); + AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); + } + pixel.spirv = makeModule({.fragment = true, .fragmentMode = spv::ExecutionModeDepthUnchanged}); + expectFailure([&] { AgcDriver::Graphics::ValidateShaders(shaders, state, subgroup, false); }, "unsupported fragment execution mode"); +} + void pixelParameterSlotTests() { using AgcDriver::Graphics::CompiledShader; const VkPhysicalDeviceSubgroupProperties subgroup{VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SUBGROUP_PROPERTIES}; @@ -1922,6 +2000,7 @@ int main() { DepthClipTests(); DepthStencilTests(); DepthBoundsBiasTests(); + conservativeZExportTests(); DisabledColorTests(); CompactedExportTests(); metadataPassTests(); diff --git a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp index 2efbe1ab5..3af51bbd8 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Pm4.cpp @@ -180,7 +180,7 @@ void testAutoDraw() { } expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3}), 0); }, "packet size"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2, 0}), 0); }, "packet size"); - expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 1), 0); }, "header flags"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 2), 0); }, "header flags"); state.userConfig[0x24a] = std::numeric_limits::max(); check(AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {1, 2}), state).firstVertex == std::numeric_limits::max(), "last vertex rejected"); expectFailure([&] { AgcDriver::Pm4::ResolveDraw(makePacket(0x2d, {2, 2}), state); }, "vertex range overflow"); @@ -508,7 +508,7 @@ void testEventWrite() { for (const auto bit : {0x40u, 0x80u, 0x800u, 0x80000000u}) { expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410u | bit}), 0); }, "reserved bits"); } - expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 1), 0); }, "header flags"); + expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 2), 0); }, "header flags"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x410, 0, 0}), 0); }, "packet size"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x13a, 0, 0}), 0); }, "event type 58"); expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x46, {0x0d}), 0); }, "event type 13"); @@ -541,7 +541,7 @@ void testAcquireMem() { packet[index] = value; expectFailure([&] { AgcDriver::Pm4::Validate(packet, 0); }, reason); }; - invalidWord(0, captured[0] | 1u, "header flags"); + invalidWord(0, captured[0] | 2u, "header flags"); invalidWord(1, 4, "control flags"); invalidWord(1, 0x00800000, "control flags"); invalidWord(3, 1, "above 40 bits"); @@ -554,6 +554,50 @@ void testAcquireMem() { expectFailure([] { AgcDriver::Pm4::Validate(makePacket(0x58, {0, 0, 0, 0, 0, 0, 0, 0}), 0); }, "packet size"); } +void testPredication() { + alignas(16) std::uint64_t flag[4] = {0, 0, 0, 0}; + auto* flag32 = reinterpret_cast(&flag[2]); + const auto setPredication = [&](std::uint32_t operation, bool executeWhenSet, const void* address) { + return makePacket(0x20, {(operation << 16u) | (executeWhenSet ? 0x100u : 0u) | 0x1000u, low(address), high(address)}); + }; + AgcDriver::QueueState state; + check(AgcDriver::Pm4::PredicationPasses(state), "inactive predication must pass"); + execute(state, setPredication(3, true, &flag[0])); + check(state.predication.operation == 3 && state.predication.executeWhenSet && state.predication.address == reinterpret_cast(&flag[0]), "BOOL64 predication state mismatch"); + check(!AgcDriver::Pm4::PredicationPasses(state), "zero BOOL64 value must skip draw-visible packets"); + flag[0] = 1ull << 40u; + check(AgcDriver::Pm4::PredicationPasses(state), "upper BOOL64 bits must count"); + execute(state, setPredication(3, false, &flag[0])); + check(!AgcDriver::Pm4::PredicationPasses(state), "non-zero BOOL64 value must skip draw-not-visible packets"); + flag[0] = 0; + check(AgcDriver::Pm4::PredicationPasses(state), "zero BOOL64 value must run draw-not-visible packets"); + execute(state, setPredication(4, true, &flag[2])); + flag[2] = 0xffffffff00000000ull; + check(*flag32 == 0 && !AgcDriver::Pm4::PredicationPasses(state), "BOOL32 must read only 32 bits"); + *flag32 = 7; + check(AgcDriver::Pm4::PredicationPasses(state), "non-zero BOOL32 value must run draw-visible packets"); + execute(state, setPredication(0, false, nullptr)); + check(state.predication.operation == 0 && AgcDriver::Pm4::PredicationPasses(state), "clear must end predication"); + + expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(1, true, &flag[0]), 0); }, "query predication"); + expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(2, true, &flag[0]), 0); }, "query predication"); + expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(5, true, &flag[0]), 0); }, "invalid predication operation"); + expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, &flag[0]), 0x20); }, "compute queue"); + expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, nullptr), 0); }, "unaligned predication address"); + expectFailure([&] { AgcDriver::Pm4::Validate(setPredication(3, true, reinterpret_cast(&flag[0]) + 8), 0); }, "unaligned predication address"); + auto continued = setPredication(3, true, &flag[0]); + continued[1] |= 1u << 31u; + expectFailure([&] { AgcDriver::Pm4::Validate(continued, 0); }, "SET_PREDICATION bits"); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x20, {3u << 16u, low(&flag[0]), high(&flag[0])}, 1), 0); }, "header flags"); + + AgcDriver::Pm4::Validate(makePacket(0x2d, {3, 2}, 1), 0); + AgcDriver::Pm4::Validate(makePacket(0x46, {0x410}, 1), 0); + const auto call = makePacket(0x3f, {0x1000, 0, 0x0f200010}, 1); + AgcDriver::Pm4::Validate(call, 0); + expectFailure([&] { AgcDriver::Pm4::Validate(makePacket(0x3f, {0x1000, 0, 0x0f200010}), 0); }, "nested command buffers"); + expectFailure([&] { AgcDriver::Pm4::Execute(call, state); }, "nested command buffers"); +} + void testDriverSubmission() { std::array source{0x10, 73}; std::array destination{}; @@ -693,6 +737,49 @@ void testConditionalSubmission() { check(results[13] == 0, "a rejected conditional submission executed a guarded packet"); } +void testPredicatedSubmission() { + alignas(16) std::uint64_t flag[2] = {0, 0}; + alignas(16) std::array written{}; + alignas(16) std::array nestedWritten{}; + const auto nested = makePacket(0x37, {0x100, low(nestedWritten.data()), high(nestedWritten.data()), 3}); + const auto call = makePacket(0x3f, {low(nested.data()), high(nested.data()), 0x0f200000u | static_cast(nested.size())}, 1); + const auto submit = [](std::vector& commands) { + Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; + check(sceAgcDriverSubmitDcb(&packet) == 0, "predicated submission failed"); + AgcDriverWaitIdle_nid_postfix(); + }; + std::vector commands; + for (const auto& packet : { + makePacket(0x20, {0x31100, low(&flag[0]), high(&flag[0])}), + makePacket(0x37, {0x100, low(&written[0]), high(&written[0]), 7}, 1), + call, + makePacket(0x20, {0, 0, 0}), + makePacket(0x37, {0x100, low(&written[1]), high(&written[1]), 9}, 1) + }) commands.insert(commands.end(), packet.begin(), packet.end()); + submit(commands); + check(written[0] == 0, "packet with a false predicate was executed"); + check(nestedWritten[0] == 0, "command buffer with a false predicate was executed"); + check(written[1] == 9, "cleared predication skipped a packet"); + flag[0] = 1; + commands.clear(); + for (const auto& packet : { + makePacket(0x20, {0x31100, low(&flag[0]), high(&flag[0])}), + makePacket(0x37, {0x100, low(&written[2]), high(&written[2]), 5}, 1), + call, + makePacket(0x20, {0x31000, low(&flag[0]), high(&flag[0])}), + makePacket(0x37, {0x100, low(&written[3]), high(&written[3]), 6}, 1), + makePacket(0x20, {0, 0, 0}) + }) commands.insert(commands.end(), packet.begin(), packet.end()); + submit(commands); + check(written[2] == 5, "packet with a true predicate was skipped"); + check(nestedWritten[0] == 3, "command buffer with a true predicate was skipped"); + check(written[3] == 0, "draw-not-visible packet ran with a set value"); + auto chain = makePacket(0x3f, {low(nested.data()), high(nested.data()), 0x0f300000u | static_cast(nested.size())}, 1); + Packet chained{chain.data(), static_cast(chain.size()), 0, {}}; + expectFailure([&] { sceAgcDriverSubmitDcb(&chained); }, "predicated command buffer chains"); + AgcDriverWaitIdle_nid_postfix(); +} + void testAsyncMemoryFailure() { auto commands = makePacket(0x37, {0x100, 0x1000, 0, 1}); Packet packet{commands.data(), static_cast(commands.size()), 0, {}}; @@ -728,7 +815,9 @@ int main(int argc, char** argv) { testConditionReadSynchronization(); testEventWrite(); testAcquireMem(); + testPredication(); testDriverSubmission(); + testPredicatedSubmission(); testConditionalSubmission(); LibcRunShutdown_nid_postfix(); std::puts("PM4 catalog, registers, state, memory, conditional execution and submission tests passed"); diff --git a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp index e2b8d991f..28a8f081a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Submit.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Submit.cpp @@ -76,7 +76,7 @@ void testValidation() { expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); commands[0] = 0xc001105c; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); - commands[0] = 0xc0017601; + commands[0] = 0xc0017608; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); commands[0] = 0xc0017600; commands[1] = 0x10000; @@ -110,7 +110,7 @@ void testClearState() { words[0] = 0xc0011200; packet.dw_num = 3; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); - words[0] = 0xc0001201; + words[0] = 0xc0001202; packet.dw_num = 2; expectFailure([&] { sceAgcDriverSubmitDcb(&packet); }); words[0] = 0xc0001200; diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp index 7242e910c..f7d519ee1 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/Float16Dot.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include @@ -18,52 +17,94 @@ namespace { using AgcDriver::Graphics::Require; using ShaderRecompiler::ShaderStage; -constexpr std::uint32_t Threads = 64; +constexpr std::uint32_t Threads = 32; constexpr std::uint32_t Inputs = 4; -constexpr std::uint32_t Results = 8; +constexpr std::uint32_t Results = 16; alignas(256) std::array Input{}; alignas(256) std::array Output{}; -alignas(256) constexpr std::array DotCode{ - 0x34020084, 0x34060085, 0xe0301000, 0x80000401, 0xe0301004, 0x80000501, 0xe0301008, 0x80000601, - 0xbf8c3f70, 0xcc13400a, 0x1c1a0b04, 0xcc13480b, 0x141a0b04, 0xcc13420c, 0xbc1a0b04, 0xcc13c00d, - 0x1c1a0b04, 0xe0701000, 0x80010a03, 0xe0701004, 0x80010b03, 0xe0701008, 0x80010c03, 0xe070100c, - 0x80010d03, 0xbf810000, +alignas(256) constexpr std::array Code{ + 0x34020084, 0x34060086, 0xe0381000, 0x80000401, 0xbf8c3f70, 0x7e140306, 0x04140b04, 0xcc13400b, + 0x1c1a0b04, 0xcc13420c, 0xbc1a0b04, 0xcc13410d, 0x5c1a0b04, 0xcc13480e, 0x141a0b04, 0xcc13c00f, + 0x1c1a0b04, 0x7e200306, 0x04200af2, 0x7e220306, 0x04220aff, 0x40003c00, 0x7e240306, 0x04240afa, + 0xff00b104, 0x7e260306, 0xcc135013, 0x0c1a0b04, 0xe0781000, 0x80010a03, 0xe0781010, 0x80010e03, + 0xe0781020, 0x80011203, 0xe0781030, 0x80011603, 0xbf810000, }; -constexpr std::array, 8> Edges{{ - {0x3c003c00u, 0x3c003c00u, 0x00000000u}, {0x7c000000u, 0x3c000000u, 0x3f800000u}, {0x7c003c00u, 0x00003c00u, 0x3f800000u}, - {0xfc007c00u, 0x3c003c00u, 0x40000000u}, {0x80000000u, 0x00008000u, 0x80000000u}, {0x7bff7bffu, 0x7bff7bffu, 0xcf800000u}, - {0x38003800u, 0xb800b800u, 0x3e800000u}, {0x7e003c00u, 0x3c003c00u, 0x00000000u}, -}}; +constexpr std::uint32_t Rows[32][4] = { + {0x0001bc00u, 0x81bfacbau, 0xc541aa2fu, 0x00000000u}, + {0x01f61a79u, 0x3c000381u, 0xc1729fffu, 0x00000000u}, + {0x02c9be80u, 0xb252213du, 0x41880526u, 0x00000000u}, + {0x032f030cu, 0x0d88ca10u, 0x3f800000u, 0x00000000u}, + {0x04003848u, 0x2427b61cu, 0x3920d8c0u, 0x00000000u}, + {0x04008190u, 0x01c85a38u, 0xc1e5655cu, 0x00000000u}, + {0x00ba3faeu, 0x02bcc61fu, 0x439ff26au, 0x00000000u}, + {0x00bc0400u, 0xaed23800u, 0xaf78b10fu, 0x00000000u}, + {0x47a37c85u, 0x7ceb0000u, 0x80521c95u, 0x00000000u}, + {0x6bc64fd7u, 0x02f45f4du, 0xffc8ab86u, 0x00000000u}, + {0x034102d0u, 0xac427d00u, 0x7fa00000u, 0x00000000u}, + {0x00000000u, 0xf35dfc00u, 0x805c769au, 0x00000000u}, + {0x0000030cu, 0x8370c506u, 0x00000001u, 0x00000000u}, + {0x00002783u, 0x5f2c0896u, 0x7f800000u, 0x00000000u}, + {0x00010001u, 0xfc00004cu, 0xca63705au, 0x00000000u}, + {0x64ba817bu, 0x80000000u, 0x00000001u, 0x00000000u}, + {0x000083ffu, 0x7d00def6u, 0x95a99701u, 0x00000000u}, + {0x7c210c47u, 0x825c8000u, 0x45b66cc3u, 0x00000000u}, + {0x00003134u, 0x8000c266u, 0x8029d2f0u, 0x00000000u}, + {0x3b898000u, 0x0000e8b9u, 0x0034ccdcu, 0x00000000u}, + {0x00000000u, 0x04008000u, 0x5f834da7u, 0x00000000u}, + {0x00000001u, 0x366e51ddu, 0x7f7fffffu, 0x00000000u}, + {0x00000001u, 0xdc472c9du, 0x42750eeeu, 0x00000000u}, + {0x000000c8u, 0x83ff3c00u, 0x43380747u, 0x00000000u}, + {0x0000010cu, 0x7c00f56fu, 0x005df72bu, 0x00000000u}, + {0x00000377u, 0xdaabea88u, 0xf540cddbu, 0x00000000u}, + {0x000003cau, 0x5b9f7e00u, 0xcaba68d5u, 0x00000000u}, + {0x00000400u, 0x785b8ab8u, 0x7fa00000u, 0x00000000u}, + {0x0000051cu, 0x6f115c7fu, 0x0047beccu, 0x00000000u}, + {0x00000c55u, 0x3c00ec7fu, 0x263e4496u, 0x00000000u}, + {0x00000f28u, 0x9e2d0123u, 0x565f49ebu, 0x00000000u}, + {0x00001209u, 0x3fe3c10fu, 0x801b0c3eu, 0x00000000u} +}; -float Half(std::uint32_t bits) { - const std::uint32_t sign = (bits & 0x8000u) << 16u; - const std::uint32_t exponent = (bits >> 10u) & 0x1fu; - const std::uint32_t mantissa = bits & 0x3ffu; - if (exponent == 0x1fu) return std::bit_cast(sign | 0x7f800000u | (mantissa << 13u)); - if (exponent == 0u) return std::bit_cast(sign) + (sign != 0u ? -1.0f : 1.0f) * std::ldexp(static_cast(mantissa), -24); - return std::bit_cast(sign | ((exponent + 112u) << 23u) | (mantissa << 13u)); -} +constexpr std::uint32_t Expected[32][10] = { + {0xc541a901u, 0xc541a901u, 0x4541a901u, 0xc541ab5du, 0xc541aa2fu, 0xc541a901u, 0xc541ab5eu, 0xc541ab5eu, 0xc541aa30u, 0xc541aa2fu}, + {0xc1729fdfu, 0xc1729fdfu, 0x41729fdfu, 0xc172a01fu, 0xc172930du, 0xc1729fdfu, 0xc1729fc7u, 0xc1529fc7u, 0xc172a037u, 0xc172930du}, + {0x4187e315u, 0x4187e315u, 0xc187e315u, 0x41882737u, 0x418a9676u, 0x4187e315u, 0x41881a1au, 0x4184f11au, 0x41880521u, 0x418a9676u}, + {0x3f7fdb0fu, 0x3f7fdb0fu, 0xbf7fdb0fu, 0x3f801279u, 0x3f7fd966u, 0x3f7fdb0fu, 0xc1320000u, 0xc131fd3cu, 0x41a5a000u, 0x3f7fd966u}, + {0xbe511687u, 0xbe511687u, 0x3e511687u, 0x3e5166f4u, 0x3c105963u, 0xbe511687u, 0xbec36be5u, 0xbeb2cfe5u, 0x392b6e40u, 0x3c105963u}, + {0xc1e56f13u, 0xc1e56f13u, 0x41e56f13u, 0xc1e55ba5u, 0xc1e54c7cu, 0xc1e56f13u, 0x432a5354u, 0x432a5358u, 0x429ba4a9u, 0xc1e54c7cu}, + {0x439a1228u, 0x439a1228u, 0xc39a1228u, 0x43a5d2acu, 0x439ff26au, 0x439a1228u, 0x439ce2eau, 0x439ce2edu, 0x439ff25eu, 0x439ff26au}, + {0x37f5fb14u, 0x37f5fb14u, 0xb7f5fb14u, 0xb7f5fc0cu, 0xb5720f8bu, 0x37f5fb14u, 0x3f000000u, 0x3e92e000u, 0x3f75bfecu, 0xb5720f8bu}, + {0x7fd0a000u, 0x7fd0a000u, 0xffd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fd0a000u, 0x7fdd6000u, 0x7fdd6000u, 0x7fdd6000u, 0x7fd0a000u}, + {0xffc8ab86u, 0xffc8ab86u, 0x7fc8ab86u, 0xffc8ab86u, 0xffc8ab86u, 0xffc8ab86u, 0xffc8ab86u, 0xffc8ab86u, 0x7fd0a000u, 0xffc8ab86u}, + {0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0xff800000u, 0xff800000u, 0xff800000u, 0xffc00000u}, + {0xb974e480u, 0xb974e480u, 0x3974e480u, 0x3974e480u, 0xb1279400u, 0xb974e480u, 0xc0a0c000u, 0xc0a0c0dcu, 0xbe16f048u, 0xb1279400u}, + {0x7f800000u, 0x7f800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u, 0x7f800000u}, + {0xff800000u, 0xff800000u, 0x7f800000u, 0x7f800000u, 0xff800000u, 0xff800000u, 0xffc00000u, 0xff800000u, 0xff800000u, 0xff800000u}, + {0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u, 0x00000000u}, + {0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fc42000u, 0x7fe00000u}, + {0x7fc42000u, 0x7fc42000u, 0x7fc42000u, 0xffc42000u, 0x7fc42000u, 0x7fc42000u, 0x45b66cc3u, 0x45b66cc3u, 0x45b66cc3u, 0x7fc42000u}, + {0xbf052ae0u, 0xbf052ae0u, 0x3f052ae0u, 0x3f052ae0u, 0x80000000u, 0xbf052ae0u, 0xc04cc000u, 0xc04cc000u, 0x00000000u, 0x80000000u}, + {0x00000000u, 0x00000000u, 0x80000000u, 0x00000000u, 0xc50e5804u, 0x00000000u, 0xc5172000u, 0xc5172000u, 0xc3c494a0u, 0xc50e5804u}, + {0x5f834da7u, 0x5f834da7u, 0xdf834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u, 0x5f834da7u}, + {0x7f7fffffu, 0x7f7fffffu, 0xff7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu, 0x7f7fffffu}, + {0x42750eeeu, 0x42750eeeu, 0xc2750eeeu, 0x42750eeeu, 0x42750eeau, 0x42750eeeu, 0x427558beu, 0xc3f314e8u, 0x42750eeeu, 0x42750eeau}, + {0x43380748u, 0x43380748u, 0xc3380748u, 0x43380746u, 0x43380747u, 0x43380748u, 0x43390747u, 0x4339073fu, 0x43380747u, 0x43380747u}, + {0xffc00000u, 0xffc00000u, 0xffc00000u, 0xffc00000u, 0x7f800000u, 0xffc00000u, 0xffc00000u, 0x7f800000u, 0xffc00000u, 0x7f800000u}, + {0xf540cddbu, 0xf540cddbu, 0x7540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu, 0xf540cddbu}, + {0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0xffc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u, 0x7fc00000u}, + {0x7fe00000u, 0x7fe00000u, 0xffe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u, 0x7fe00000u}, + {0x3cb7c720u, 0x3cb7c720u, 0xbcb7c720u, 0xbcb7c720u, 0x3f106b70u, 0x3cb7c720u, 0x438fe000u, 0x46669f00u, 0x3d9bd158u, 0x3f106b70u}, + {0xbf9bd158u, 0xbf9bd158u, 0x3f9bd158u, 0x3f9bd158u, 0x398aa000u, 0xbf9bd158u, 0xc58fe000u, 0xc58fd000u, 0xbeb7c720u, 0x398aa000u}, + {0x565f49ebu, 0x565f49ebu, 0xd65f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu, 0x565f49ebu}, + {0xbaf43c38u, 0xbaf43c38u, 0x3af43c38u, 0x3af43c38u, 0x3abe63ecu, 0xbaf43c38u, 0xc021e000u, 0x3fb50000u, 0xba90cd60u, 0x3abe63ecu} +}; -std::uint32_t RandomHalf(std::uint64_t& state) { - state = state * 6364136223846793005ull + 1442695040888963407ull; - const auto bits = static_cast(state >> 33u); - return (bits & 0x8000u) | ((8u + (bits >> 16u) % 15u) << 10u) | (bits & 0x3ffu); -} +constexpr const char* Names[10] = {"v_dot2c", "v_dot2", "v_dot2 neg_lo src0/src2 neg_hi src1", "v_dot2 neg_lo src1 neg_hi src0", "v_dot2 op_sel src0", "v_dot2 clamp", "v_dot2c inline 1.0", "v_dot2c literal", "v_dot2c dpp quad_perm", "v_dot2 op_sel src1"}; -void FillInput() { - std::uint64_t state = 0x9e3779b97f4a7c15ull; - for (std::uint32_t tid = 0; tid < Threads; ++tid) { - auto* words = &Input[tid * Inputs]; - if (tid < Edges.size()) { - std::copy(Edges[tid].begin(), Edges[tid].end(), words); - continue; - } - words[0] = RandomHalf(state) | (RandomHalf(state) << 16u); - words[1] = RandomHalf(state) | (RandomHalf(state) << 16u); - words[2] = std::bit_cast(Half(RandomHalf(state)) * Half(RandomHalf(state))); - } +void Fill(std::uint32_t tid, std::uint32_t* words) { + for (std::uint32_t i = 0; i < 4; ++i) words[i] = Rows[tid][i]; } std::array BufferDescriptor(const void* data, std::uint32_t bytes) { @@ -77,6 +118,10 @@ std::string Hex(std::uint32_t 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("v_dot2_f32_f16: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(expected)); +} + auto Compile(AgcDriver::VulkanDevice& device, std::span code) { std::vector userData(8, 0u); const auto input = BufferDescriptor(Input.data(), static_cast(Input.size() * 4u)); @@ -84,7 +129,7 @@ auto Compile(AgcDriver::VulkanDevice& device, std::span cod 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}, 0, {false, false, false}, false, 1}; + 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}, @@ -95,14 +140,6 @@ auto Compile(AgcDriver::VulkanDevice& device, std::span cod return ShaderRecompiler::Recompile(request); } -void Run(AgcDriver::VulkanDevice& device) { - Output.fill(0xdeadbeefu); - const std::span code(DotCode); - const auto result = Compile(device, code); - device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); - device.WaitIdle(); -} - void CheckRefused(AgcDriver::VulkanDevice& device, std::uint32_t word0, const std::string& what) { alignas(256) const std::array code{word0, 0x1c1a0b04u, 0xbf810000u}; std::string refusal; @@ -114,30 +151,19 @@ void CheckRefused(AgcDriver::VulkanDevice& device, std::uint32_t word0, const st Require(refusal.find("v_dot2_f32_f16 accumulator op_sel, op_sel_hi and neg_hi are not implemented") != std::string::npos, what + " was not refused"); } -float Dot(float aLow, float aHigh, float bLow, float bHigh, float c) { - return std::fma(aHigh, bHigh, std::fma(aLow, bLow, c)); -} - -void Expect(std::uint32_t tid, std::uint32_t actual, float expected, const char* name) { - const std::uint32_t bits = std::bit_cast(expected); - const bool nan = std::isnan(expected) && std::isnan(std::bit_cast(actual)); - Require(nan || actual == bits, std::string("v_dot2_f32_f16: lane ") + std::to_string(tid) + " " + name + " is " + Hex(actual) + ", expected " + Hex(bits)); +void Run(AgcDriver::VulkanDevice& device) { + for (std::uint32_t tid = 0; tid < Threads; ++tid) Fill(tid, &Input[tid * Inputs]); + Output.fill(0xdeadbeefu); + const std::span code(Code); + const auto result = Compile(device, code); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); } void Check() { for (std::uint32_t tid = 0; tid < Threads; ++tid) { - const auto* in = &Input[tid * Inputs]; - const auto* out = &Output[tid * Results]; - const float aLow = Half(in[0] & 0xffffu); - const float aHigh = Half(in[0] >> 16u); - const float bLow = Half(in[1] & 0xffffu); - const float bHigh = Half(in[1] >> 16u); - const float c = std::bit_cast(in[2]); - const float plain = Dot(aLow, aHigh, bLow, bHigh, c); - Expect(tid, out[0], plain, "plain"); - Expect(tid, out[1], Dot(aHigh, aLow, bLow, bHigh, c), "op_sel swapping the halves of src0"); - Expect(tid, out[2], Dot(-aLow, aHigh, bLow, -bHigh, -c), "neg_lo src0, neg_hi src1 and neg src2"); - if (!std::isnan(plain)) Expect(tid, out[3], plain > 0.0f ? std::min(plain, 1.0f) : 0.0f, "clamp"); + const std::uint32_t* out = &Output[tid * Results]; + for (std::uint32_t i = 0; i < 10; ++i) Expect(tid, out[i], Expected[tid][i], Names[i]); } } @@ -147,7 +173,6 @@ int main() { try { const auto device = OpenVulkanTestDevice(); if (!device) return VulkanTestSkipped; - FillInput(); Run(*device); Check(); CheckRefused(*device, 0xcc13600au, "op_sel on the accumulator"); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp new file mode 100644 index 000000000..e0e441989 --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/GlobalVccBase.cpp @@ -0,0 +1,192 @@ +#include "prx/libSceAgcDriver/Execution/include/VulkanDevice.hpp" +#include "prx/libSceAgcDriver/Graphics/include/Draw.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 + +namespace { + +using AgcDriver::Graphics::Require; +using ShaderRecompiler::ShaderStage; + +constexpr std::uint32_t Threads = 64; +constexpr std::size_t BlockBytes = 65536; +constexpr std::uint32_t InputDwords = 768; +constexpr std::uint32_t Fill = 0xcdcdcdcdu; +constexpr std::uint32_t VectorBase = 512; +constexpr std::uint32_t DwordBase = 4; +constexpr std::uint32_t AddtidStoreBase = 256; +constexpr std::uint32_t AddtidBase = 384; +constexpr std::uint32_t AtomicBase = 448; +constexpr std::uint32_t OutputBase = 1024; +constexpr std::uint32_t OutputDwords = 8; + +alignas(256) constexpr std::array VccBaseCode{ + 0xbeea0400, 0x34020082, 0x34040084, 0x340c0085, 0x4a0c0cff, 0x00001000, 0x4ad404ff, 0x00000048, + 0xdc308010, 0x046a0001, 0xdcc98700, 0x0c6a0201, 0xdc3887b8, 0x006a006a, 0xbf8c3f70, 0xdc788000, + 0x006a0006, 0xdc708010, 0x006a0406, 0xdc708014, 0x006a0c06, 0xbf810000, +}; + +alignas(256) constexpr std::array VccAddtidCode{ + 0xbeea0400, 0x340c0085, 0x4a0c0cff, 0x00001000, 0xdc588600, 0x056a0000, 0xbf8c3f70, 0xdc708018, + 0x006a0506, 0xdc5c8400, 0x006a0500, 0xbf810000, +}; + +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, "global vcc base: 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::vector Initial() { + std::vector memory(BlockBytes / 4u, Fill); + for (std::uint32_t dword = 0; dword < InputDwords; ++dword) memory[dword] = 0x51000000u + dword * 0x00010203u; + return memory; +} + +std::vector ExpectedBase() { + const auto initial = Initial(); + auto memory = initial; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + auto* out = &memory[OutputBase + tid * OutputDwords]; + for (std::uint32_t component = 0; component < 4u; ++component) out[component] = initial[VectorBase + tid * 4u + component]; + out[4] = initial[DwordBase + tid]; + out[5] = initial[AtomicBase + tid]; + memory[AtomicBase + tid] = initial[AtomicBase + tid] + tid * 16u; + } + return memory; +} + +std::vector ExpectedAddtid(std::uint32_t waveSize) { + const auto initial = Initial(); + auto memory = initial; + for (std::uint32_t tid = 0; tid < Threads; ++tid) { + const std::uint32_t lane = tid % waveSize; + memory[OutputBase + tid * OutputDwords + 6u] = initial[AddtidBase + lane]; + memory[AddtidStoreBase + lane] = initial[AddtidBase + lane]; + } + return memory; +} + +ShaderRecompiler::RecompileResult Recompile(std::span code, std::uint32_t waveSize, std::span userData, const ShaderRecompiler::SpirvTarget& target) { + const std::array regions{{{reinterpret_cast(code.data()), std::as_bytes(code)}}}; + const ShaderRecompiler::ShaderComputeStageInfo compute{{Threads, 1, 1}, 0, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, regions}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, GuestBlock& guest, std::span code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target, const std::vector& expected, const std::string& name) { + auto memory = Initial(); + std::memcpy(guest.Data(), memory.data(), BlockBytes); + const auto address = static_cast(reinterpret_cast(guest.Data())); + std::vector userData(2, 0u); + userData[0] = static_cast(address); + userData[1] = static_cast(address >> 32u); + const auto result = Recompile(code, waveSize, userData, target); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); + std::memcpy(memory.data(), guest.Data(), BlockBytes); + for (std::size_t dword = 0; dword < memory.size(); ++dword) { + Require(memory[dword] == expected[dword], "global vcc base: " + name + " byte " + std::to_string(dword * 4u) + " is " + Hex(memory[dword]) + ", expected " + Hex(expected[dword])); + } +} + +void CheckRejected(const AgcDriver::VulkanDevice& device, std::span code, const std::string& reason) { + const std::vector userData(2, 0u); + std::string failure; + try { + static_cast(Recompile(code, 32, userData, device.Target())); + } catch (const std::exception& error) { + failure = error.what(); + } + Require(failure.find(reason) != std::string::npos, "global vcc base: expected '" + reason + "', got '" + failure + "'"); +} + +void CheckRejections(const AgcDriver::VulkanDevice& device) { + alignas(256) static constexpr std::array vccHiLoad{0xdc308000, 0x046b0001, 0xbf810000}; + alignas(256) static constexpr std::array m0Atomic{0xdcc88000, 0x007c0201, 0xbf810000}; + alignas(256) static constexpr std::array execStore{0xdc708000, 0x007e0201, 0xbf810000}; + alignas(256) static constexpr std::array vccHiAddtid{0xdc588000, 0x056b0000, 0xbf810000}; + CheckRejected(device, vccHiLoad, "FLAT scalar address register range overflow"); + CheckRejected(device, m0Atomic, "FLAT scalar address register range overflow"); + CheckRejected(device, execStore, "FLAT scalar address register range overflow"); + CheckRejected(device, vccHiAddtid, "global_load_dword_addtid supports only an SGPR pair as base address"); +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + CheckRejections(*device); + GuestBlock guest; + const auto base = ExpectedBase(); + Run(*device, guest, VccBaseCode, 32, device->Target(), base, "wave32"); + Run(*device, guest, VccBaseCode, 64, device->Target(), base, "wave64"); + Run(*device, guest, VccBaseCode, 64, device->ComputeTarget(32), base, "wave64 split"); + if (device->Target().subgroupSize < 32u) { + std::printf("addtid cases skipped, the device's subgroups are narrower than a wave (%u lanes)\n", device->Target().subgroupSize); + } else { + Run(*device, guest, VccAddtidCode, 32, device->Target(), ExpectedAddtid(32), "addtid wave32"); + Run(*device, guest, VccAddtidCode, 64, device->Target(), ExpectedAddtid(64), "addtid wave64"); + Run(*device, guest, VccAddtidCode, 64, device->ComputeTarget(32), ExpectedAddtid(64), "addtid wave64 split"); + } + std::puts("global vcc base 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/ScalarSwappc.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappc.cpp new file mode 100644 index 000000000..161d76cea --- /dev/null +++ b/core/libs/prx/libSceAgcDriver/tests/execution/ScalarSwappc.cpp @@ -0,0 +1,143 @@ +#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 MaxThreads = 64; +constexpr std::uint32_t Inputs = 4; +constexpr std::uint32_t Results = 4; +alignas(256) std::array Input{}; +alignas(256) std::array Output{}; + +alignas(256) constexpr std::array Wave32Code{ + 0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1402ffu, 0xdeadbeefu, 0x7e1602ffu, 0xdeadbeefu, + 0x7e1802ffu, 0xdeadbeefu, 0x7e1a02ffu, 0xdeadbeefu, 0xbe940380u, 0xd7600008u, 0x02002904u, 0xd760000au, + 0x02002905u, 0xbe8c1f00u, 0x800cff0cu, 0x0000014cu, 0x820d800du, 0xbe89030au, 0x83928308u, 0xd4c40016u, + 0x02001104u, 0x8f128212u, 0xf4000386u, 0x24000000u, 0xbf8cc07fu, 0x7d820a0au, 0x808c0e0cu, 0x828d800du, + 0xbefd210cu, 0x8009ff0au, 0x11110000u, 0xbf820008u, 0x8009ff0au, 0x22220000u, 0xbf820005u, 0x8009ff0au, + 0x33330000u, 0xbf820002u, 0x8009ff0au, 0x44440000u, 0xd761000au, 0x02002809u, 0x816ac108u, 0x83ea836au, + 0x8f6a826au, 0xbe8e1f00u, 0x800eff0eu, 0x000000d4u, 0x820f800fu, 0xf4000487u, 0xd4000008u, 0x8aea167eu, + 0xbf8cc07fu, 0x808e120eu, 0x828f800fu, 0xbefd210eu, 0x8009ff0au, 0x0a000000u, 0xbf820008u, 0x8009ff0au, + 0x0b000000u, 0xbf820005u, 0x8009ff0au, 0x0c000000u, 0xbf820002u, 0x8009ff0au, 0x0d000000u, 0xd761000bu, + 0x02002809u, 0x8009ff0au, 0x600d0000u, 0xbeea1f00u, 0x806a946au, 0x826b806bu, 0xbefd216au, 0xbe8903ffu, + 0x00000badu, 0xd761000cu, 0x02002809u, 0xbe890380u, 0xbe950383u, 0x80090a09u, 0x80958115u, 0xbf068015u, + 0xbf850004u, 0xbe901f00u, 0x80909410u, 0x82918011u, 0xbefd2110u, 0xd761000du, 0x02002809u, 0x80148114u, + 0xbf0aa014u, 0xbf85ffabu, 0xe0781000u, 0x80010a01u, 0xbf810000u, 0x00000110u, 0x00000104u, 0x000000f8u, + 0x000000ecu, 0x000000acu, 0x000000a0u, 0x00000094u, 0x00000088u, +}; + +alignas(256) constexpr std::array Wave64Code{ + 0x34020084u, 0xe0381000u, 0x80000401u, 0xbf8c3f70u, 0x7e1402ffu, 0xdeadbeefu, 0x7e1602ffu, 0xdeadbeefu, + 0x7e1802ffu, 0xdeadbeefu, 0x7e1a02ffu, 0xdeadbeefu, 0xbe940380u, 0xd7600008u, 0x02002904u, 0xd760000au, + 0x02002905u, 0xbe8c1f00u, 0x800cff0cu, 0x0000014cu, 0x820d800du, 0xbe89030au, 0x83928308u, 0xd4c40016u, + 0x02001104u, 0x8f128212u, 0xf4000386u, 0x24000000u, 0xbf8cc07fu, 0x7d820a0au, 0x808c0e0cu, 0x828d800du, + 0xbefd210cu, 0x8009ff0au, 0x11110000u, 0xbf820008u, 0x8009ff0au, 0x22220000u, 0xbf820005u, 0x8009ff0au, + 0x33330000u, 0xbf820002u, 0x8009ff0au, 0x44440000u, 0xd761000au, 0x02002809u, 0x816ac108u, 0x83ea836au, + 0x8f6a826au, 0xbe8e1f00u, 0x800eff0eu, 0x000000d4u, 0x820f800fu, 0xf4000487u, 0xd4000008u, 0x8aea167eu, + 0xbf8cc07fu, 0x808e120eu, 0x828f800fu, 0xbefd210eu, 0x8009ff0au, 0x0a000000u, 0xbf820008u, 0x8009ff0au, + 0x0b000000u, 0xbf820005u, 0x8009ff0au, 0x0c000000u, 0xbf820002u, 0x8009ff0au, 0x0d000000u, 0xd761000bu, + 0x02002809u, 0x8009ff0au, 0x600d0000u, 0xbeea1f00u, 0x806a946au, 0x826b806bu, 0xbefd216au, 0xbe8903ffu, + 0x00000badu, 0xd761000cu, 0x02002809u, 0xbe890380u, 0xbe950383u, 0x80090a09u, 0x80958115u, 0xbf068015u, + 0xbf850004u, 0xbe901f00u, 0x80909410u, 0x82918011u, 0xbefd2110u, 0xd761000du, 0x02002809u, 0x80148114u, + 0xbf0ac014u, 0xbf85ffabu, 0xe0781000u, 0x80010a01u, 0xbf810000u, 0x00000110u, 0x00000104u, 0x000000f8u, + 0x000000ecu, 0x000000acu, 0x000000a0u, 0x00000094u, 0x00000088u, +}; + +constexpr std::array Selectors{0u, 1u, 2u, 3u, 4u, 7u, 0x80000000u, 0xffffffffu}; +constexpr std::array GetpcFirstCases{0x11110000u, 0x22220000u, 0x33330000u, 0x44440000u}; +constexpr std::array IndexFirstCases{0x0a000000u, 0x0b000000u, 0x0c000000u, 0x0d000000u}; +constexpr const char* Names[Results] = {"dword_table_getpc_first", "dword_table_index_first", "long_branch_vcc", "long_branch_back"}; + +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; +} + +std::uint32_t Expected(std::uint32_t lane, std::uint32_t result) { + const auto selector = Input[lane * Inputs]; + const auto value = Input[lane * Inputs + 1u]; + switch (result) { + case 0: return value + GetpcFirstCases[std::min(selector, 3u)]; + case 1: return value + IndexFirstCases[std::min(selector - 1u, 3u)]; + case 2: return value + 0x600d0000u; + default: return value * 3u; + } +} + +ShaderRecompiler::RecompileResult Compile(std::span code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) { + 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{{waveSize, 1, 1}, 0u, {false, false, false}, false, 1}; + ShaderRecompiler::RecompileRequest request{ + {ShaderStage::Compute, reinterpret_cast(code.data()), code, 0, {}}, + {waveSize, 0, userData, compute, std::nullopt, std::nullopt, memory}, + target, + {0, 0, 0, 128} + }; + request.useCache = false; + return ShaderRecompiler::Recompile(request); +} + +void Run(AgcDriver::VulkanDevice& device, std::span code, std::uint32_t waveSize, const ShaderRecompiler::SpirvTarget& target) { + for (std::uint32_t tid = 0; tid < MaxThreads; ++tid) { + Input[tid * Inputs] = Selectors[tid % Selectors.size()]; + Input[tid * Inputs + 1u] = 0x9e3779b9u * (tid + 1u); + } + Output.fill(0xdeadbeefu); + const auto result = Compile(code, waveSize, target); + device.Dispatch(result, 1, 1, 1, {}, reinterpret_cast(code.data())); + device.WaitIdle(); +} + +void Check(std::uint32_t lanes) { + for (std::uint32_t tid = 0; tid < lanes; ++tid) { + for (std::uint32_t index = 0; index < Results; ++index) { + const auto actual = Output[tid * Results + index]; + const auto expected = Expected(tid, index); + Require(actual == expected, "swappc wave" + std::to_string(lanes) + ": lane " + std::to_string(tid) + " " + Names[index] + " is " + Hex(actual) + ", expected " + Hex(expected)); + } + } +} + +} + +int main() { + try { + const auto device = OpenVulkanTestDevice(); + if (!device) return VulkanTestSkipped; + Run(*device, Wave32Code, 32, device->ComputeTarget(32)); + Check(32); + Run(*device, Wave64Code, 64, device->Target()); + Check(64); + Run(*device, Wave64Code, 64, device->ComputeTarget(32)); + Check(64); + std::puts("swappc 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 7fe1d9502..62c0bc44e 100644 --- a/core/libs/prx/libSceAjm.native/Export.cpp +++ b/core/libs/prx/libSceAjm.native/Export.cpp @@ -14,13 +14,6 @@ static void AjmStub(const char* name) { extern "C" { -int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { - (void)context; - (void)batch; - AjmStub(__func__); - return 0; -} - 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; diff --git a/core/libs/prx/libSceAjm.native/src/Ajm.cpp b/core/libs/prx/libSceAjm.native/src/Ajm.cpp index 3b30d8d53..9b5542631 100644 --- a/core/libs/prx/libSceAjm.native/src/Ajm.cpp +++ b/core/libs/prx/libSceAjm.native/src/Ajm.cpp @@ -26,6 +26,7 @@ extern "C" { // memory follow the SDK layouts. namespace { +constexpr int SCE_AJM_ERROR_INVALID_CONTEXT = static_cast(0x80930002); constexpr int SCE_AJM_ERROR_INVALID_INSTANCE = static_cast(0x80930003); constexpr int SCE_AJM_ERROR_INVALID_BATCH = static_cast(0x80930004); constexpr int SCE_AJM_ERROR_INVALID_PARAMETER = static_cast(0x80930005); @@ -492,6 +493,134 @@ bool ParseMp3Frame(const std::uint8_t* data, std::size_t size, Mp3Frame& frame) return true; } +constexpr std::uint32_t MP3_OFL_NONE = 0; +constexpr std::uint32_t MP3_OFL_LAME = 1; +constexpr std::uint32_t MP3_OFL_VBRI = 2; +constexpr std::uint32_t MP3_OFL_FGH = 3; +constexpr std::uint32_t MP3_OFL_VBRI_AND_FGH = 4; + +std::uint32_t ReadBigEndian(const std::uint8_t* data, std::size_t bytes) { + std::uint32_t value = 0; + for (std::size_t i = 0; i < bytes; ++i) value = (value << 8u) | data[i]; + return value; +} + +int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame); + +void ParseMp3Ofl(const std::uint8_t* stream, std::uint32_t streamSize, std::uint32_t header, AjmDecMp3ParseFrame* frame) { + const std::size_t end = std::min(streamSize, frame->frame_size); + if (end <= 4) return; + const std::size_t available = end - 4; + std::size_t bit = 0; + const auto readBits = [&](std::uint32_t count) { + std::uint32_t value = 0; + for (std::uint32_t i = 0; i < count; ++i, ++bit) value = (value << 1u) | (bit / 8 < available ? (stream[4 + bit / 8] >> (7 - bit % 8)) & 1u : 0u); + return value; + }; + const bool mpeg1 = ((header >> 19u) & 3u) == 3u; + const bool mono = ((header >> 6u) & 3u) == 3u; + bit += mpeg1 ? (mono ? 18 : 20) : (mono ? 9 : 10); + std::uint32_t mainDataBits = 0; + for (std::uint32_t granule = 0; granule < (mpeg1 ? 2u : 1u); ++granule) { + for (std::uint32_t channel = 0; channel < frame->num_channels; ++channel) { + mainDataBits += readBits(12); + bit += mpeg1 ? 47 : 51; + } + } + bit += mainDataBits; + std::size_t at = 4 + std::min((bit + 7) / 8, available); + + if (at + 8 <= end && (std::memcmp(stream + at, "Xing", 4) == 0 || std::memcmp(stream + at, "Info", 4) == 0)) { + const std::uint8_t flags = stream[at + 7]; + std::size_t field = at + 8; + std::uint32_t frames = 0; + if (flags & 0x01u) { + if (field + 4 > end) return; + frames = ReadBigEndian(stream + field, 4); + frame->num_frames = frames; + field += 4; + } + if (flags & 0x02u) { + field += 4; + if (field > end) return; + } + if (flags & 0x04u) { + field += 100; + if (field > end) return; + } + if (flags & 0x08u) { + field += 4; + if (field > end) return; + } + if (field + 0x18 <= end && std::memcmp(stream + field, "LAME", 4) == 0) { + const std::uint32_t delay = (std::uint32_t{stream[field + 0x15]} << 4u) | (stream[field + 0x16] >> 4u); + const std::uint32_t padding = ((std::uint32_t{stream[field + 0x16]} & 0x0Fu) << 8u) | stream[field + 0x17]; + if ((flags & 0x01u) && frames > 0) frame->total_samples = frames * frame->samples_per_channel - (delay + padding); + frame->encoder_delay = frame->samples_per_channel + delay + 529; + frame->ofl_type = MP3_OFL_LAME; + } + } else if (at != 36 && end >= 40 && std::memcmp(stream + 36, "VBRI", 4) == 0) { + NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (VBRI header outside MPEG-1 stereo)"); + } else if (at + 26 <= end && std::memcmp(stream + at, "VBRI", 4) == 0) { + frame->encoder_delay = ReadBigEndian(stream + at + 6, 2); + frame->ofl_type = MP3_OFL_VBRI; + if (frame->frame_size <= streamSize) { + AjmDecMp3ParseFrame next{}; + if (ParseMp3Header(stream + frame->frame_size, streamSize - static_cast(frame->frame_size), 1, &next) == 0 && next.ofl_type == MP3_OFL_FGH) { + frame->encoder_delay += next.encoder_delay; + frame->total_samples = next.total_samples; + frame->ofl_type = MP3_OFL_VBRI_AND_FGH; + } + } + } else if (at + 10 <= end) { + while (at + 9 < end && stream[at] != 0xB4u) ++at; + if (at + 9 >= end) return; + std::uint8_t crc = 0xFF; + for (std::size_t i = 0; i < 9; ++i) { + for (int shift = 7; shift >= 0; --shift) { + const bool same = ((crc >> 7u) & 1u) == ((stream[at + i] >> shift) & 1u); + crc = static_cast(same ? (crc << 1u) ^ 0x45u : crc << 1u); + } + } + if (stream[at + 9] != crc) return; + frame->encoder_delay = ReadBigEndian(stream + at + 1, 2); + frame->total_samples = ReadBigEndian(stream + at + 3, 4); + frame->ofl_type = MP3_OFL_FGH; + } +} + +int ParseMp3Header(const std::uint8_t* stream, std::uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame) { + if (!stream || streamSize < 4 || !frame) return SCE_AJM_ERROR_INVALID_PARAMETER; + static constexpr std::uint32_t sampleRates[4][4] = {{11025, 12000, 8000, 0}, {0, 0, 0, 0}, {22050, 24000, 16000, 0}, {44100, 48000, 32000, 0}}; + static constexpr std::uint32_t kbps[4][16] = { + {0, 8, 16, 24, 32, 40, 48, 56, 64, 0, 0, 0, 0, 0, 0, 0}, + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}, + {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}, + }; + const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; + const std::uint32_t version = (header >> 19u) & 3u; + const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; + const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; + const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; + AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); + if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; + if (parseOfl && ((header >> 17u) & 3u) != 1u) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup outside layer III)"); + if (parseOfl && ((header >> 16u) & 1u) == 0) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup in a CRC-protected frame)"); + const bool mpeg1 = version == 3; + frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); + frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; + frame->samples_per_channel = mpeg1 ? 1152u : 576u; + frame->bitrate = bitrate; + frame->sample_rate = sampleRate; + frame->encoder_delay = 0; + frame->num_frames = 0; + frame->total_samples = 0; + frame->ofl_type = MP3_OFL_NONE; + if (parseOfl) ParseMp3Ofl(stream, streamSize, header, frame); + return 0; +} + void OpenMp3(Instance& instance) { static const bool quiet = (av_log_set_level(TraceEnabled() ? AV_LOG_WARNING : AV_LOG_QUIET), true); (void)quiet; @@ -870,33 +999,7 @@ int APS5_VABI sceAjmDecAt9ParseConfigData(const void* config_data, AjmDecAt9Conf } int APS5_VABI sceAjmDecMp3ParseFrame(const uint8_t* stream, uint32_t streamSize, int parseOfl, AjmDecMp3ParseFrame* frame) { - if (!stream || streamSize < 4 || !frame) return SCE_AJM_ERROR_INVALID_PARAMETER; - static constexpr std::uint32_t sampleRates[4][4] = {{11025, 12000, 8000, 0}, {0, 0, 0, 0}, {22050, 24000, 16000, 0}, {44100, 48000, 32000, 0}}; - static constexpr std::uint32_t kbps[4][16] = { - {0, 8, 16, 24, 32, 40, 48, 56, 64, 0, 0, 0, 0, 0, 0, 0}, - {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, - {0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0}, - {0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0}, - }; - const std::uint32_t header = (std::uint32_t{stream[0]} << 24u) | (std::uint32_t{stream[1]} << 16u) | (std::uint32_t{stream[2]} << 8u) | stream[3]; - const std::uint32_t version = (header >> 19u) & 3u; - const std::uint32_t sampleRate = sampleRates[version][(header >> 10u) & 3u]; - const std::uint32_t bitrate = kbps[version][(header >> 12u) & 15u] * 1000u; - const bool valid = (header >> 21u) == 0x7FFu && sampleRate != 0 && bitrate != 0; - AJM_TRACE("[ajm] parse mp3 frame %02x %02x %02x %02x (%u bytes, parse ofl %d) -> %s, %u Hz, %u bps\n", stream[0], stream[1], stream[2], stream[3], streamSize, parseOfl, valid ? "ok" : "invalid", sampleRate, bitrate); - if (!valid) return SCE_AJM_ERROR_INVALID_PARAMETER; - if (parseOfl) NotImplemented_nid_no_patch("sceAjmDecMp3ParseFrame (original file length lookup)"); - const bool mpeg1 = version == 3; - frame->frame_size = (mpeg1 ? 144u : 72u) * bitrate / sampleRate + ((header >> 9u) & 1u); - frame->num_channels = ((header >> 6u) & 3u) == 3u ? 1u : 2u; - frame->samples_per_channel = mpeg1 ? 1152u : 576u; - frame->bitrate = bitrate; - frame->sample_rate = sampleRate; - frame->encoder_delay = 0; - frame->num_frames = 0; - frame->total_samples = 0; - frame->ofl_type = 0; - return 0; + return ParseMp3Header(stream, streamSize, parseOfl, frame); } int APS5_VABI sceAjmBatchInitialize(void* buffer, size_t size, AjmBatchInfo* info) { @@ -1009,6 +1112,12 @@ int APS5_VABI sceAjmBatchWait(uint32_t context, uint32_t batch, uint32_t timeout return 0; } +int APS5_VABI sceAjmBatchCancel(uint32_t context, uint32_t batch) { + if (context == 0 || context >= g_nextContext.load(std::memory_order_relaxed)) return SCE_AJM_ERROR_INVALID_CONTEXT; + std::lock_guard lock(g_batchLock); + return g_batches.contains(batch) ? 0 : SCE_AJM_ERROR_INVALID_BATCH; +} + int APS5_VABI sceAjmBatchErrorDump(const AjmBatchInfo* info, AjmBatchError* error) { (void)info; if (error) std::memset(error, 0, sizeof(*error)); diff --git a/core/libs/prx/libSceAmpr/Export.cpp b/core/libs/prx/libSceAmpr/Export.cpp index 51d8032fd..bfb04045c 100644 --- a/core/libs/prx/libSceAmpr/Export.cpp +++ b/core/libs/prx/libSceAmpr/Export.cpp @@ -4,6 +4,7 @@ #include "prx/libc/include/General.hpp" #include "prx/libkernel/Apr/include/AprCommandBuffer.hpp" #include +#include static constexpr int SCE_AMPR_ERROR_BUFFER_FULL = 0x8002001C; static constexpr int SCE_KERNEL_ERROR_EINVAL = 0x80020016; @@ -35,6 +36,57 @@ static bool ValidWait(std::uint32_t compare, std::uint32_t flush) { return compare < 4u && flush < 2u; } +static bool ValidWaitOnCounter_04_00(std::uint32_t access, std::uint32_t compare, std::uint32_t maskOperation, std::uint32_t flush) { + return access < 8u && compare <= 6u && maskOperation < 2u && flush < 2u; +} + +static bool ValidWriteCounter_04_00(std::uint32_t counter, std::uint32_t access, std::uint32_t operation) { + return (counter & 0x80u) == 0u && access < 8u && operation < 5u; +} + +static constexpr std::uint64_t MeasureInvalid = static_cast(SCE_KERNEL_ERROR_EINVAL); + +static bool ValidReadLength(std::uint64_t length) { + return length != 0u && length <= 0x100000000ull; +} + +static bool ValidReadOffset(std::uint64_t offset) { + return offset < 0x10000000000ull; +} + +static bool ValidRead(const void* destination, std::uint64_t length, std::uint64_t offset) { + constexpr std::uint64_t userLimit = 0xF00000000000ull; + const auto address = reinterpret_cast(destination); + return ValidReadLength(length) && address <= userLimit && length <= userLimit - address && ValidReadOffset(offset); +} + +static int AppendRead(Apr::CommandBufferObject* buffer, Apr::Opcode opcode, std::uint32_t fileId, const void* destination, std::uint64_t size, std::uint64_t offset) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + if (opcode != Apr::Opcode::ReadFile && (buffer->recording & Apr::ScatterGatherValid) == 0u) return SCE_KERNEL_ERROR_EINVAL; + Apr::ReadFileCommand command{{opcode, sizeof(command)}, fileId, 0, reinterpret_cast(destination), size, offset}; + const int result = Append(buffer, &command, sizeof(command)); + if (result == 0) buffer->recording |= Apr::ScatterGatherValid; + return result; +} + +static std::uint64_t NopBytes(std::uint32_t dwords) { + return sizeof(Apr::CommandHeader) + std::uint64_t{dwords} * 4u; +} + +static int AppendNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { + if (!buffer) return SCE_KERNEL_ERROR_EINVAL; + const std::uint64_t bytes = NopBytes(dwords); + if (!buffer->base || bytes > buffer->size - buffer->offset) return SCE_AMPR_ERROR_BUFFER_FULL; + const Apr::CommandHeader header{Apr::Opcode::Nop, static_cast(bytes)}; + std::uint8_t* destination = buffer->base + buffer->offset; + std::memset(destination, 0, bytes); + std::memcpy(destination, &header, sizeof(header)); + if (data) std::memcpy(destination + sizeof(header), data, std::uint64_t{dwords} * 4u); + buffer->offset += static_cast(bytes); + ++buffer->numCommands; + return 0; +} + static std::uint64_t MarkerBytes(const char* text) { return sizeof(Apr::MarkerCommand) + ((std::strlen(text) + 8) & ~std::uint64_t{7}); } @@ -67,7 +119,7 @@ int APS5_VABI sceAmprCommandBufferWriteAddressOnCompletion(Apr::CommandBufferObj int APS5_VABI sceAmprCommandBufferWriteCounterOnCompletion(Apr::CommandBufferObject* buffer, std::uint8_t counter, std::uint32_t value) { if (!ValidCounter(counter)) return SCE_KERNEL_ERROR_EINVAL; - return AppendCommand(buffer, Apr::Opcode::WriteCounter, Apr::WriteCounterCommand{{}, counter, value}); + return AppendCommand(buffer, Apr::Opcode::WriteCounter, Apr::WriteCounterCommand{{}, counter, Apr::CounterAccess::Size4, Apr::CounterOperation::Store, 0, value}); } int APS5_VABI sceAmprCommandBufferWaitOnAddress(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t reference, std::uint8_t compare, std::uint8_t flush) { @@ -77,7 +129,7 @@ int APS5_VABI sceAmprCommandBufferWaitOnAddress(Apr::CommandBufferObject* buffer int APS5_VABI sceAmprCommandBufferWaitOnCounter(Apr::CommandBufferObject* buffer, std::uint8_t counter, std::uint32_t reference, std::uint8_t compare, std::uint8_t flush) { if (!ValidCounter(counter) || !ValidWait(compare, flush)) return SCE_KERNEL_ERROR_EINVAL; - return AppendCommand(buffer, Apr::Opcode::WaitOnCounter, Apr::WaitCommand{{}, 0, reference, ~0ull, counter, compare}); + return AppendCommand(buffer, Apr::Opcode::WaitOnCounter, Apr::WaitCommand{{}, 0, reference, ~0ull, counter, compare, Apr::CounterAccess::Size4, 0}); } int APS5_VABI sceAmprCommandBufferWriteKernelEventQueueOnCompletion(Apr::CommandBufferObject* buffer, std::uint64_t equeue, std::int32_t ident, std::uint64_t data) { @@ -138,33 +190,35 @@ int APS5_VABI sceAmprAprCommandBufferMapEnd() { int APS5_VABI sceAmprAprCommandBufferReadFile(Apr::CommandBufferObject* buffer, uint64_t* gatherState, uint64_t* scatterState, uint32_t fileId, void* destination, uint64_t size, uint64_t offset) { (void)gatherState; (void)scatterState; - Apr::ReadFileCommand command{}; - command.header = {Apr::Opcode::ReadFile, sizeof(command)}; - command.fileId = fileId; - command.destination = reinterpret_cast(destination); - command.size = size; - command.offset = offset; - return Append(buffer, &command, sizeof(command)); + if (!ValidRead(destination, size, offset)) return SCE_KERNEL_ERROR_EINVAL; + return AppendRead(buffer, Apr::Opcode::ReadFile, fileId, destination, size, offset); } -int APS5_VABI sceAmprAprCommandBufferReadFileGather() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprAprCommandBufferReadFileGather(Apr::CommandBufferObject* buffer, std::uint64_t* mapState, std::uint64_t* scatterGatherState, std::uint64_t size, std::uint64_t offset) { + (void)mapState; + (void)scatterGatherState; + if (!ValidReadLength(size) || !ValidReadOffset(offset)) return SCE_KERNEL_ERROR_EINVAL; + return AppendRead(buffer, Apr::Opcode::ReadFileGather, 0, nullptr, size, offset); } -int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter(Apr::CommandBufferObject* buffer, std::uint64_t* mapState, std::uint64_t* scatterGatherState, void* destination, std::uint64_t size, std::uint64_t offset) { + (void)mapState; + (void)scatterGatherState; + if (!ValidRead(destination, size, offset)) return SCE_KERNEL_ERROR_EINVAL; + return AppendRead(buffer, Apr::Opcode::ReadFileGatherScatter, 0, destination, size, offset); } -int APS5_VABI sceAmprAprCommandBufferReadFileScatter() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprAprCommandBufferReadFileScatter(Apr::CommandBufferObject* buffer, std::uint64_t* mapState, std::uint64_t* scatterGatherState, void* destination, std::uint64_t size) { + (void)mapState; + (void)scatterGatherState; + if (!ValidRead(destination, size, 0)) return SCE_KERNEL_ERROR_EINVAL; + return AppendRead(buffer, Apr::Opcode::ReadFileScatter, 0, destination, size, 0); } -int APS5_VABI sceAmprAprCommandBufferResetGatherScatterState() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprAprCommandBufferResetGatherScatterState(Apr::CommandBufferObject* buffer) { + const int result = AppendCommand(buffer, Apr::Opcode::ResetGatherScatterState, Apr::ResetGatherScatterStateCommand{}); + if (result == 0) buffer->recording &= static_cast(~Apr::ScatterGatherValid); + return result; } int APS5_VABI sceAmprCommandBufferClearBuffer(Apr::CommandBufferObject* buffer) { @@ -175,14 +229,32 @@ int APS5_VABI sceAmprCommandBufferClearBuffer(Apr::CommandBufferObject* buffer) return 0; } -int APS5_VABI sceAmprCommandBufferConstructMarker() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferConstructMarker(Apr::CommandBufferObject* buffer, std::uint32_t type, const char* text, const std::uint32_t* color) { + switch (type) { + case 1: return AppendMarker(buffer, Apr::Opcode::SetMarker, text); + case 2: return AppendMarker(buffer, Apr::Opcode::PushMarker, text); + case 3: return AppendCommand(buffer, Apr::Opcode::PopMarker, Apr::MarkerCommand{}); + case 5: return color ? AppendMarker(buffer, Apr::Opcode::SetMarker, text) : SCE_KERNEL_ERROR_EINVAL; + case 6: return color ? AppendMarker(buffer, Apr::Opcode::PushMarker, text) : SCE_KERNEL_ERROR_EINVAL; + default: return SCE_KERNEL_ERROR_EINVAL; + } } -int APS5_VABI sceAmprCommandBufferConstructNop() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject* buffer, std::int16_t type, const void* payload, std::uint32_t payloadBytes, const std::uint32_t* word) { + (void)type; + if (payloadBytes > (word ? 56u : 60u)) return SCE_KERNEL_ERROR_EINVAL; + if (payloadBytes != 0u && !payload) throw std::invalid_argument("sceAmprCommandBufferConstructNop: null payload"); + const std::uint32_t dwords = (payloadBytes + 3u) / 4u + (word ? 1u : 0u); + const std::uint32_t offset = buffer ? buffer->offset : 0u; + const int result = AppendNop(buffer, dwords, nullptr); + if (result != 0) return result; + std::uint8_t* data = buffer->base + offset + sizeof(Apr::CommandHeader); + if (word) { + std::memcpy(data, word, sizeof(*word)); + data += sizeof(*word); + } + if (payloadBytes != 0u) std::memcpy(data, payload, payloadBytes); + return 0; } int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject* buffer) { @@ -215,14 +287,15 @@ uint32_t APS5_VABI sceAmprCommandBufferGetType(const Apr::CommandBufferObject* b return static_cast(buffer->type); } -int APS5_VABI sceAmprCommandBufferNop() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject* buffer, std::uint32_t dwords) { + if (dwords == 0u || dwords > 16u) return SCE_KERNEL_ERROR_EINVAL; + return AppendNop(buffer, dwords, nullptr); } -int APS5_VABI sceAmprCommandBufferNopWithData() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject* buffer, std::uint32_t dwords, const std::uint32_t* data) { + if (dwords > 15u) return SCE_KERNEL_ERROR_EINVAL; + if (dwords != 0u && !data) throw std::invalid_argument("sceAmprCommandBufferNopWithData: null data"); + return AppendNop(buffer, dwords, data); } int APS5_VABI sceAmprCommandBufferPopMarker(Apr::CommandBufferObject* buffer) { @@ -261,29 +334,30 @@ int APS5_VABI sceAmprCommandBufferSetMarkerWithColor(Apr::CommandBufferObject* b return AppendMarker(buffer, Apr::Opcode::SetMarker, text); } -int APS5_VABI sceAmprCommandBufferWaitOnAddress_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWaitOnAddress_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t reference, std::uint8_t compare, std::uint8_t flush) { + if (!ValidWriteAddress(address) || compare > 6u || flush > 1u) return SCE_KERNEL_ERROR_EINVAL; + return AppendCommand(buffer, Apr::Opcode::WaitOnAddress, Apr::WaitCommand{{}, reinterpret_cast(address), reference, ~0ull, 0, compare}); } -int APS5_VABI sceAmprCommandBufferWaitOnCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWaitOnCounter_04_00(Apr::CommandBufferObject* buffer, std::uint8_t counter, std::uint8_t access, std::uint64_t reference, std::uint8_t compare, std::uint8_t maskOperation, std::uint64_t mask, std::uint8_t flush) { + if (!ValidWaitOnCounter_04_00(access, compare, maskOperation, flush)) return SCE_KERNEL_ERROR_EINVAL; + const std::uint64_t applied = maskOperation ? mask : ~0ull; + return AppendCommand(buffer, Apr::Opcode::WaitOnCounter, Apr::WaitCommand{{}, 0, reference, applied, counter, compare, static_cast(access), 0}); } -int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPair_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPair_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint8_t counter, std::uint64_t atStart) { + (void)atStart; + return sceAmprCommandBufferWriteAddressFromCounterPairOnCompletion(buffer, address, counter); } -int APS5_VABI sceAmprCommandBufferWriteAddressFromCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteAddressFromCounter_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint8_t counter, std::uint64_t atStart) { + (void)atStart; + return sceAmprCommandBufferWriteAddressFromCounterOnCompletion(buffer, address, counter); } -int APS5_VABI sceAmprCommandBufferWriteAddressFromTimeCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteAddressFromTimeCounter_04_00(Apr::CommandBufferObject* buffer, volatile std::uint64_t* address, std::uint64_t atStart) { + (void)atStart; + return sceAmprCommandBufferWriteAddressFromTimeCounterOnCompletion(buffer, address); } int APS5_VABI sceAmprCommandBufferWriteAddress_04_00(Apr::CommandBufferObject* buffer, uint64_t* address, uint64_t value, uint32_t flags) { @@ -295,14 +369,15 @@ int APS5_VABI sceAmprCommandBufferWriteAddress_04_00(Apr::CommandBufferObject* b return Append(buffer, &command, sizeof(command)); } -int APS5_VABI sceAmprCommandBufferWriteCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteCounter_04_00(Apr::CommandBufferObject* buffer, std::uint8_t counter, std::uint8_t access, std::uint64_t value, std::uint8_t operation, std::uint8_t atStart) { + (void)atStart; + if (!ValidWriteCounter_04_00(counter, access, operation)) return SCE_KERNEL_ERROR_EINVAL; + return AppendCommand(buffer, Apr::Opcode::WriteCounter, Apr::WriteCounterCommand{{}, counter, static_cast(access), static_cast(operation), 0, value}); } -int APS5_VABI sceAmprCommandBufferWriteKernelEventQueue_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceAmprCommandBufferWriteKernelEventQueue_04_00(Apr::CommandBufferObject* buffer, std::uint64_t equeue, std::int32_t ident, std::uint64_t data, std::uint64_t atStart) { + (void)atStart; + return sceAmprCommandBufferWriteKernelEventQueueOnCompletion(buffer, equeue, ident, data); } int APS5_VABI sceAmprMeasureCommandSizeMapBegin() { @@ -320,14 +395,14 @@ int APS5_VABI sceAmprMeasureCommandSizeMapEnd() { return 0; } -int APS5_VABI sceAmprMeasureCommandSizeNop() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNop(std::uint32_t dwords) { + if (dwords == 0u || dwords > 16u) return MeasureInvalid; + return NopBytes(dwords); } -int APS5_VABI sceAmprMeasureCommandSizeNopWithData() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNopWithData(std::uint32_t dwords) { + if (dwords == 0u || dwords > 16u) return MeasureInvalid; + return NopBytes(dwords - 1u); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizePopMarker() { @@ -347,24 +422,23 @@ uint32_t APS5_VABI sceAmprMeasureCommandSizeReadFile(void) { return sizeof(Apr::ReadFileCommand); } -int APS5_VABI sceAmprMeasureCommandSizeReadFileGather() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGather(std::uint64_t size, std::uint64_t offset) { + if (!ValidReadLength(size) || !ValidReadOffset(offset)) return MeasureInvalid; + return sizeof(Apr::ReadFileCommand); } -int APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter(void* destination, std::uint64_t size, std::uint64_t offset) { + if (!ValidRead(destination, size, offset)) return MeasureInvalid; + return sizeof(Apr::ReadFileCommand); } -int APS5_VABI sceAmprMeasureCommandSizeReadFileScatter() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileScatter(void* destination, std::uint64_t size) { + if (!ValidRead(destination, size, 0)) return MeasureInvalid; + return sizeof(Apr::ReadFileCommand); } -int APS5_VABI sceAmprMeasureCommandSizeResetGatherScatterState() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeResetGatherScatterState() { + return sizeof(Apr::ResetGatherScatterStateCommand); } std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarker(const char* text) { @@ -376,43 +450,43 @@ std::uint64_t APS5_VABI sceAmprMeasureCommandSizeSetMarkerWithColor(const char* return MeasureMarker(text); } -int APS5_VABI sceAmprMeasureCommandSizeWaitOnAddress_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnAddress_04_00(volatile std::uint64_t* address, std::uint64_t, std::uint8_t compare, std::uint8_t flush) { + if (!ValidWriteAddress(address) || compare > 6u || flush > 1u) return MeasureInvalid; + return sizeof(Apr::WaitCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00(std::uint8_t, std::uint8_t access, std::uint64_t, std::uint8_t compare, std::uint8_t maskOperation, std::uint64_t, std::uint8_t flush) { + if (!ValidWaitOnCounter_04_00(access, compare, maskOperation, flush)) return MeasureInvalid; + return sizeof(Apr::WaitCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(volatile std::uint64_t* address, std::uint8_t counter) { + if (!ValidWriteAddress(address) || (counter & 1u) != 0u) return MeasureInvalid; + return sizeof(Apr::WriteAddressFromCounterCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(volatile std::uint64_t* address, std::uint8_t counter) { + (void)counter; + if (!ValidWriteAddress(address)) return MeasureInvalid; + return sizeof(Apr::WriteAddressFromCounterCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00(volatile std::uint64_t* address) { + if (!ValidWriteAddress(address)) return MeasureInvalid; + return sizeof(Apr::WriteAddressFromCounterCommand); } uint32_t APS5_VABI sceAmprMeasureCommandSizeWriteAddress_04_00(void) { return sizeof(Apr::WriteAddressCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00(std::uint8_t counter, std::uint8_t access, std::uint64_t, std::uint8_t operation) { + if (!ValidWriteCounter_04_00(counter, access, operation)) return MeasureInvalid; + return sizeof(Apr::WriteCounterCommand); } -int APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00() { - NotImplemented_nid_no_patch(__func__); - return 0; +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00(std::uint64_t, std::int32_t, std::uint64_t) { + return sizeof(Apr::WriteKernelEventQueueCommand); } } diff --git a/core/libs/prx/libSceAudio3d/CMakeLists.txt b/core/libs/prx/libSceAudio3d/CMakeLists.txt index c324ba678..e93a55d05 100644 --- a/core/libs/prx/libSceAudio3d/CMakeLists.txt +++ b/core/libs/prx/libSceAudio3d/CMakeLists.txt @@ -18,4 +18,13 @@ if(BUILD_TESTING) set_tests_properties(guest_audio3d_parameters PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_audio3d_port_tests ${LIBS_INCLUDE_DIR}/tests/GuestAudio3dPort.cpp) + target_include_directories(guest_audio3d_port_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_audio3d_port_tests PRIVATE libSceAudio3d libc) + configure_windows_unwind(guest_audio3d_port_tests) + add_test(NAME guest_audio3d_port COMMAND guest_audio3d_port_tests) + if(WIN32) + set_tests_properties(guest_audio3d_port PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceAudio3d/Export.cpp b/core/libs/prx/libSceAudio3d/Export.cpp index d82269005..705f81119 100644 --- a/core/libs/prx/libSceAudio3d/Export.cpp +++ b/core/libs/prx/libSceAudio3d/Export.cpp @@ -1,9 +1,62 @@ +#include #include #include #include +#include +#include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int AUDIO3D_ERROR_INVALID_PORT = static_cast(0x80EA0002); +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_RATE_48000 = 0; +constexpr std::uint32_t AUDIO3D_SAMPLE_RATE = 48000; +constexpr std::uint32_t AUDIO3D_BUFFER_ADVANCE_AND_PUSH = 2; +constexpr std::uint32_t AUDIO3D_BLOCKING_ASYNC = 0; +constexpr std::uint32_t AUDIO3D_BLOCKING_SYNC = 1; +constexpr std::uint32_t AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL = 0x10001; +constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS = 0x10002; +constexpr std::uint32_t AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE = 0x10003; + +using Clock = std::chrono::steady_clock; + +struct Audio3dPort { + bool open = false; + std::uint32_t queue_depth = 0; + Clock::duration frame_duration{}; + std::uint32_t advanced = 0; + std::deque playing; +}; + +std::mutex g_mutex; +bool g_initialized = false; +Audio3dPort g_port; + +[[noreturn]] void Unsupported(const char* function, const std::string& what) { + throw std::runtime_error(std::string(function) + ": " + what); +} + +bool PortIsOpen(std::uint32_t port_id) { + return port_id == AUDIO3D_PORT_ID && g_port.open; +} + +std::uint32_t QueueLevel(Clock::time_point now) { + while (!g_port.playing.empty() && g_port.playing.front() <= now) g_port.playing.pop_front(); + return g_port.advanced + static_cast(g_port.playing.size()); +} + +} + extern "C" { void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* p) { @@ -14,53 +67,109 @@ void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* p) { } int APS5_VABI sceAudio3dInitialize(int64_t reserved) { - (void)reserved; - NotImplemented_nid_no_patch(__func__); - return 0; + if (reserved != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + std::lock_guard lock(g_mutex); + if (g_initialized) Unsupported(__func__, "repeated initialization"); + g_initialized = true; + return 0; } int APS5_VABI sceAudio3dPortAdvance(uint32_t port_id) { - (void)port_id; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (QueueLevel(Clock::now()) >= g_port.queue_depth) Unsupported(__func__, "queue is full"); + ++g_port.advanced; + return 0; } int APS5_VABI sceAudio3dPortClose(uint32_t port_id) { - (void)port_id; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + g_port = Audio3dPort{}; + return 0; } int APS5_VABI sceAudio3dPortGetQueueLevel(uint32_t port_id, uint32_t* queue_level, uint32_t* queue_available) { - (void)port_id; - (void)queue_level; - (void)queue_available; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (queue_level == nullptr && queue_available == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + const std::uint32_t level = QueueLevel(Clock::now()); + if (queue_level != nullptr) *queue_level = level; + if (queue_available != nullptr) *queue_available = g_port.queue_depth - level; + return 0; } int APS5_VABI sceAudio3dPortOpen(int user_id, const Audio3dOpenParameters* parameters, uint32_t* id) { - (void)user_id; - (void)parameters; - (void)id; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!g_initialized) return AUDIO3D_ERROR_NOT_READY; + if (user_id != AUDIO3D_USER_ID_SYSTEM || parameters == nullptr || id == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + std::uint32_t num_beds = 2; + switch (parameters->size_this) { + case 0x10: + case 0x18: + Unsupported(__func__, "parameter size " + std::to_string(parameters->size_this) + " selects an unsupported buffer mode"); + case 0x20: + break; + case 0x28: + num_beds = parameters->num_beds; + break; + default: + return AUDIO3D_ERROR_INVALID_PARAMETER; + } + if (parameters->rate != AUDIO3D_RATE_48000) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (parameters->granularity < 0x100 || (parameters->granularity & 0xFF) != 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (parameters->max_objects == 0 || parameters->queue_depth == 0) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (parameters->buffer_mode > AUDIO3D_BUFFER_ADVANCE_AND_PUSH) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (num_beds != 2 && num_beds != 3) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (parameters->buffer_mode != AUDIO3D_BUFFER_ADVANCE_AND_PUSH) { + Unsupported(__func__, "buffer mode " + std::to_string(parameters->buffer_mode)); + } + if (num_beds != 2) Unsupported(__func__, "3 beds"); + if (g_port.open) return AUDIO3D_ERROR_OUT_OF_RESOURCES; + g_port = Audio3dPort{}; + g_port.open = true; + g_port.queue_depth = parameters->queue_depth; + g_port.frame_duration = std::chrono::duration_cast( + std::chrono::nanoseconds(std::uint64_t{parameters->granularity} * 1000000000u / AUDIO3D_SAMPLE_RATE)); + *id = AUDIO3D_PORT_ID; + return 0; } int APS5_VABI sceAudio3dPortPush(uint32_t port_id, uint32_t blocking) { - (void)port_id; - (void)blocking; - NotImplemented_nid_no_patch(__func__); - return 0; + Clock::time_point wait_until; + { + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (blocking != AUDIO3D_BLOCKING_ASYNC && blocking != AUDIO3D_BLOCKING_SYNC) { + Unsupported(__func__, "blocking mode " + std::to_string(blocking)); + } + const Clock::time_point now = Clock::now(); + const std::uint32_t level = QueueLevel(now); + Clock::time_point end = g_port.playing.empty() ? now : g_port.playing.back(); + for (; g_port.advanced != 0; --g_port.advanced) { + end += g_port.frame_duration; + g_port.playing.push_back(end); + } + if (blocking == AUDIO3D_BLOCKING_ASYNC || level < g_port.queue_depth) return 0; + wait_until = g_port.playing[level - g_port.queue_depth]; + } + while (Clock::now() < wait_until) std::this_thread::sleep_until(wait_until); + return 0; } int APS5_VABI sceAudio3dPortSetAttribute(uint32_t port_id, uint32_t attribute_id, const void* attribute, size_t attribute_size) { - (void)port_id; - (void)attribute_id; - (void)attribute; - (void)attribute_size; - NotImplemented_nid_no_patch(__func__); - return 0; + std::lock_guard lock(g_mutex); + if (!PortIsOpen(port_id)) return AUDIO3D_ERROR_INVALID_PORT; + if (attribute == nullptr) return AUDIO3D_ERROR_INVALID_PARAMETER; + if (attribute_size != 4) Unsupported(__func__, "attribute size " + std::to_string(attribute_size)); + switch (attribute_id) { + case AUDIO3D_ATTRIBUTE_LATE_REVERB_LEVEL: + case AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_RADIUS: + case AUDIO3D_ATTRIBUTE_DOWNMIX_SPREAD_HEIGHT_AWARE: + return 0; + default: + Unsupported(__func__, "attribute " + std::to_string(attribute_id)); + } } } diff --git a/core/libs/prx/libSceContentExport/Export.cpp b/core/libs/prx/libSceContentExport/Export.cpp index 3887de0bc..1a7fde306 100644 --- a/core/libs/prx/libSceContentExport/Export.cpp +++ b/core/libs/prx/libSceContentExport/Export.cpp @@ -26,6 +26,11 @@ int APS5_VABI sceContentExportFinish(void) { return 0; } +int APS5_VABI sceContentExportFromData(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceContentExportFromFile(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -47,10 +52,4 @@ int APS5_VABI sceContentExportTerm(void) { return 0; } -APS5_EXPORT("AOWqIYsgVHs", sceContentExportUnknown_AOWqIYsgVHs); -int32_t APS5_VABI sceContentExportUnknown_AOWqIYsgVHs(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - } diff --git a/core/libs/prx/libSceFont/CMakeLists.txt b/core/libs/prx/libSceFont/CMakeLists.txt index 3ae1d162b..8866b00b4 100644 --- a/core/libs/prx/libSceFont/CMakeLists.txt +++ b/core/libs/prx/libSceFont/CMakeLists.txt @@ -12,8 +12,13 @@ if(BUILD_TESTING) target_include_directories(guest_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_font_tests PRIVATE libSceFont libSceFontFt libc) add_test(NAME guest_font COMMAND guest_font_tests) + add_test_executable(guest_system_font_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemFont.cpp) + target_include_directories(guest_system_font_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_font_tests PRIVATE libSceFont libSceFontFt libc) + configure_windows_unwind(guest_system_font_tests) + add_test(NAME guest_system_font COMMAND guest_system_font_tests) if(WIN32) - set_tests_properties(guest_font PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_font guest_system_font PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceFont/FontLibrary.cmake b/core/libs/prx/libSceFont/FontLibrary.cmake index 26e287f39..d8ff9e0d3 100644 --- a/core/libs/prx/libSceFont/FontLibrary.cmake +++ b/core/libs/prx/libSceFont/FontLibrary.cmake @@ -7,6 +7,7 @@ function(add_sce_font_library target) ${fontDir}/src/Render.cpp ${fontDir}/src/State.cpp ${fontDir}/src/Style.cpp + ${fontDir}/src/SystemFont.cpp ${fontDir}/src/Text.cpp ${fontDir}/src/Unimplemented.cpp ) diff --git a/core/libs/prx/libSceFont/include/FontDriver.hpp b/core/libs/prx/libSceFont/include/FontDriver.hpp index e58ae256e..889c367dd 100644 --- a/core/libs/prx/libSceFont/include/FontDriver.hpp +++ b/core/libs/prx/libSceFont/include/FontDriver.hpp @@ -23,6 +23,7 @@ constexpr std::uint16_t STYLE_FRAME_MAGIC = 0x0F09; constexpr std::uint32_t LOCK_BIT = 0x80000000u; constexpr std::uint32_t OPEN_BIT = 0x40000000u; constexpr std::uint32_t COUNT_MASK = 0x0FFFFFFFu; +constexpr std::uint64_t SYSFONT_FLAG_SYSTEM_SET = 0x100u; constexpr std::size_t HORIZONTAL_LAYOUT_SIZE = 0x30; constexpr std::size_t HORIZONTAL_LINE_ADVANCE = 0x00; diff --git a/core/libs/prx/libSceFont/include/FontInternal.hpp b/core/libs/prx/libSceFont/include/FontInternal.hpp index 426a21bee..b65648a0d 100644 --- a/core/libs/prx/libSceFont/include/FontInternal.hpp +++ b/core/libs/prx/libSceFont/include/FontInternal.hpp @@ -6,6 +6,9 @@ #include #include +#include +#include +#include #include #include @@ -21,7 +24,7 @@ constexpr std::uint8_t STYLE_FRAME_FLAG_WEIGHT = 0x04; constexpr float POINTS_PER_INCH = 72.0f; struct FontState { - std::vector faceData; + std::shared_ptr> faceData; FT_Face face = nullptr; float scaleW = 16.0f; float scaleH = 16.0f; @@ -48,6 +51,11 @@ struct GeneratedGlyph { bool outlineInitialized = false; }; +struct SystemFontFile { + std::filesystem::path path; + std::uint32_t subFontIndex = 0; +}; + struct RenderSurfaceSystemUse { FontStyleFrame* styleframe; float catchedScale; @@ -70,12 +78,18 @@ void** EntryObjectSlot(FontCtxEntry* entry, std::uint32_t modeLow); FontCtxEntry* AcquireFontCtxEntry(void* ctx, std::uint32_t index, std::uint32_t modeLow, FontObj** outObject, std::uint32_t* outLockWord); void ReleaseFontCtxEntryLock(FontCtxEntry* entry, std::uint32_t modeLow, std::uint32_t lockWord); FontObj* FindSubFont(FontObj* head, std::uint32_t subFontIndex); +void* FontContext(const FontLibNative* library, const FontHandleNative* font); void ReleaseFontObjectsForHandle(FontHandleNative* font); FontState* TryGetState(FontHandle handle); FontState& ResetState(FontHandle handle); void RemoveState(FontHandle handle); -void LoadStateFace(FontState& state, std::vector bytes, std::uint32_t subFontIndex); +void LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex); + +bool IsSystemFontSet(std::uint32_t fontSetType); +std::vector SystemFontCandidates(std::uint32_t fontSetType); +std::filesystem::path SystemFontDirectory(); +std::optional FindSystemFontFile(std::uint32_t fontSetType); void TrackGeneratedGlyph(FontGlyph glyph); bool ForgetGeneratedGlyph(FontGlyph glyph); diff --git a/core/libs/prx/libSceFont/src/Layout.cpp b/core/libs/prx/libSceFont/src/Layout.cpp index 7b76c46d0..68d242d53 100644 --- a/core/libs/prx/libSceFont/src/Layout.cpp +++ b/core/libs/prx/libSceFont/src/Layout.cpp @@ -212,7 +212,7 @@ int Font::ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* styl auto* font = GetNativeFont(handle); if (!font) return fail(); auto* library = static_cast(font->library); - if (!library || !library->external_fonts_ctx) return fail(); + if (!library || !FontContext(library, font)) return fail(); const SysDriver* driver = library->sys_driver; if (!driver || !driver->set_char_with_dpi || !driver->set_char_default_dpi || !driver->compute_layout || !style) return fail(); const std::uint32_t modeLow = font->flags & 0x0Fu; @@ -226,7 +226,7 @@ int Font::ComputeHorizontalLayout(FontHandle handle, const StyleStateBlock* styl if (static_cast(fontId) >= 0) { FontObj* head = nullptr; std::uint32_t lockWord = 0; - auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + auto* entry = AcquireFontCtxEntry(FontContext(library, font), fontId, modeLow, &head, &lockWord); if (!entry) return fail(); FontObj* match = FindSubFont(head, font->open_info.sub_font_index); int rc = SCE_FONT_ERROR_FATAL; @@ -288,7 +288,7 @@ int Font::ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, }; auto* font = GetNativeFont(handle); auto* library = font ? static_cast(font->library) : nullptr; - if (!library || !library->external_fonts_ctx || !library->sys_driver) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); + if (!library || !FontContext(library, font) || !library->sys_driver) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); const SysDriver* driver = library->sys_driver; const std::uint32_t modeLow = font->flags & 0x0Fu; const std::uint32_t fontId = font->open_info.ctx_entry_index; @@ -303,7 +303,7 @@ int Font::ComputeVerticalLayout(FontHandle handle, const StyleStateBlock* style, if (static_cast(fontId) >= 0) { FontObj* head = nullptr; std::uint32_t lockWord = 0; - auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + auto* entry = AcquireFontCtxEntry(FontContext(library, font), fontId, modeLow, &head, &lockWord); FontObj* obj = FindSubFont(head, font->open_info.sub_font_index); if (!entry || !obj) { if (entry) ReleaseFontCtxEntryLock(entry, modeLow, lockWord); @@ -454,7 +454,7 @@ int Font::RenderCharGlyphImageCore(FontHandle handle, std::uint32_t code, FontRe const StyleStateBlock* style = ResolveSurfaceStyleState(&font->cached_style.state, surface, surfaceStyle); FontObj* obj = nullptr; std::uint32_t lockWord = 0; - auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, font->open_info.ctx_entry_index, modeLow, &obj, &lockWord); + auto* entry = AcquireFontCtxEntry(FontContext(library, font), font->open_info.ctx_entry_index, modeLow, &obj, &lockWord); obj = FindSubFont(obj, font->open_info.sub_font_index); if (!entry || !obj) { if (entry) ReleaseFontCtxEntryLock(entry, modeLow, lockWord); diff --git a/core/libs/prx/libSceFont/src/Open.cpp b/core/libs/prx/libSceFont/src/Open.cpp index b92f7ebf9..3ed2eaa05 100644 --- a/core/libs/prx/libSceFont/src/Open.cpp +++ b/core/libs/prx/libSceFont/src/Open.cpp @@ -1,13 +1,15 @@ // SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later -#include -#include #include #include #include #include #include +#include +#include +#include +#include #include #include #include @@ -18,28 +20,11 @@ namespace { using namespace Font; -constexpr std::array SYSTEM_FONT_SET_TYPES = { - 0x18070043u, 0x18070044u, 0x18070045u, 0x18070047u, 0x18070053u, 0x18070054u, - 0x18070055u, 0x18070057u, 0x180700C3u, 0x180700C4u, 0x180700C5u, 0x180700C7u, - 0x18070444u, 0x18070447u, 0x18070454u, 0x18070457u, 0x180704C4u, 0x180704C7u, - 0x18071053u, 0x18071054u, 0x18071055u, 0x18071057u, 0x18071454u, 0x18071457u, - 0x18072444u, 0x18072447u, 0x180724C4u, 0x180724C7u, 0x18073454u, 0x18073457u, - 0x180734D4u, 0x180734D7u, 0x18078044u, 0x180780C4u, 0x18079054u, 0x1807A044u, - 0x1807A0C4u, 0x1807A444u, 0x1807A4C4u, 0x1807AC44u, 0x1807ACC4u, 0x1807B054u, - 0x1807B0D4u, 0x1807B454u, 0x1807B4D4u, 0x1807BC54u, 0x1807BCD4u, 0x18080444u, - 0x18080447u, 0x18080454u, 0x18080457u, 0x180804C4u, 0x180804C7u, 0x18081454u, - 0x18081457u, 0x18082444u, 0x18082447u, 0x180824C4u, 0x180824C7u, 0x18083454u, - 0x18083457u, 0x180834D4u, 0x180834D7u, 0x1808A444u, 0x1808A4C4u, 0x1808B454u, - 0x1808B4D4u, 0x180C8044u, 0x180C80C4u, 0x180C9054u, 0x180CA044u, 0x180CA0C4u, - 0x180CAC44u, 0x180CACC4u, 0x180CB054u, 0x180CB0D4u, 0x180CBC54u, 0x180CBCD4u, - 0x18170043u, 0x18170044u, 0x18170045u, 0x18170047u, 0x18170444u, 0x18170447u, - 0x18370044u, 0x18370047u, 0x18370444u, 0x18370447u, -}; - constexpr std::uint16_t HANDLE_OWNED = 0x10; struct OpenRequest { FontLibNative* library; + void* context; std::uint32_t modeLow; std::uint32_t driverMode; const void* source; @@ -111,7 +96,7 @@ void SetOpenFlags(FontLibNative* library, std::uint32_t modeLow) { if ((flags & secondBit) != 0) library->sysfont_flags |= 2; } -std::uint32_t ClaimExternalEntry(FontCtxHeader* header, const OpenRequest& request) { +std::uint32_t ClaimEntry(FontCtxHeader* header, const OpenRequest& request) { AcquireWordLock(header->lock_word); auto* entries = static_cast(header->base); std::uint32_t entryIndex = 0xFFFFFFFFu; @@ -140,7 +125,7 @@ std::uint32_t ClaimExternalEntry(FontCtxHeader* header, const OpenRequest& reque return entryIndex; } -int OpenExternalFont(const OpenRequest& request, std::uint32_t libraryLock, FontHandle* pFontHandle) { +int OpenFontInContext(const OpenRequest& request, std::uint32_t libraryLock, FontHandle* pFontHandle) { FontLibNative* library = request.library; FontHandle handle = PrepareHandle(library, *pFontHandle); if (!handle) { @@ -154,8 +139,8 @@ int OpenExternalFont(const OpenRequest& request, std::uint32_t libraryLock, Font *pFontHandle = nullptr; return rc; }; - auto* header = static_cast(library->external_fonts_ctx); - const std::uint32_t entryIndex = ClaimExternalEntry(header, request); + auto* header = static_cast(request.context); + const std::uint32_t entryIndex = ClaimEntry(header, request); if (entryIndex == 0xFFFFFFFFu) return fail(SCE_FONT_ERROR_FONT_OPEN_MAX); const std::uint32_t packedId = request.uniqueId == -1 ? entryIndex + 0x40000000u : (static_cast(request.uniqueId) | 0x80000000u); FontObj* fontObj = nullptr; @@ -183,6 +168,7 @@ int OpenExternalFont(const OpenRequest& request, std::uint32_t libraryLock, Font } if (needOpen) { SetOpenFlags(library, request.modeLow); + if (request.context == library->sysfonts_ctx) library->sysfont_flags |= SYSFONT_FLAG_SYSTEM_SET; rc = driver->open(library, request.driverMode, request.source, request.sourceSize, request.subFontIndex, packedId, &fontObj); library->sysfont_flags = 0; if (rc == SCE_FONT_OK) { @@ -221,11 +207,23 @@ int BeginOpen(FontLibrary library, FontHandle* pFontHandle, std::uint32_t& libra return rc; } -bool ReadFileBytes(const std::filesystem::path& path, std::vector& bytes) { +std::shared_ptr> ReadFileBytes(const std::filesystem::path& path) { std::ifstream file(path, std::ios::binary); - if (!file) return false; - bytes.assign(std::istreambuf_iterator(file), std::istreambuf_iterator()); - return !file.bad(); + if (!file) return nullptr; + std::vector bytes{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + if (file.bad()) return nullptr; + return std::make_shared>(std::move(bytes)); +} + +std::shared_ptr> SystemFontBytes(const std::filesystem::path& path) { + static std::mutex mutex; + static std::map>> loaded; + std::lock_guard lock(mutex); + auto& slot = loaded[path]; + if (auto bytes = slot.lock()) return bytes; + auto bytes = ReadFileBytes(path); + slot = bytes; + return bytes; } void CopyStyleToCache(FontHandleNative* font) { @@ -250,11 +248,11 @@ int APS5_VABI sceFontOpenFontMemory(FontLibrary library, const void* fontAddress if (pFontHandle) *pFontHandle = nullptr; return SCE_FONT_ERROR_INVALID_PARAMETER; } - const OpenRequest request{lib, 1, 1, fontAddress, fontSize, true, subFontIndex, uniqueId}; - const int rc = OpenExternalFont(request, libraryLock, pFontHandle); + const OpenRequest request{lib, lib->external_fonts_ctx, 1, 1, fontAddress, fontSize, true, subFontIndex, uniqueId}; + const int rc = OpenFontInContext(request, libraryLock, pFontHandle); if (rc != SCE_FONT_OK) return rc; const auto* bytes = static_cast(fontAddress); - LoadStateFace(ResetState(*pFontHandle), std::vector(bytes, bytes + fontSize), subFontIndex); + LoadStateFace(ResetState(*pFontHandle), std::make_shared>(bytes, bytes + fontSize), subFontIndex); return SCE_FONT_OK; } @@ -278,12 +276,11 @@ int APS5_VABI sceFontOpenFontFile(FontLibrary library, const char* path, std::ui *pFontHandle = nullptr; return SCE_FONT_ERROR_INVALID_PARAMETER; } - const OpenRequest request{lib, modeLow, modeLow + 4, hostPathString.c_str(), 0, false, subFontIndex, uniqueId}; - const int rc = OpenExternalFont(request, libraryLock, pFontHandle); + const OpenRequest request{lib, lib->external_fonts_ctx, modeLow, modeLow + 4, hostPathString.c_str(), 0, false, subFontIndex, uniqueId}; + const int rc = OpenFontInContext(request, libraryLock, pFontHandle); if (rc != SCE_FONT_OK) return rc; - std::vector bytes; FontState& state = ResetState(*pFontHandle); - if (ReadFileBytes(hostPath, bytes)) LoadStateFace(state, std::move(bytes), subFontIndex); + if (auto bytes = ReadFileBytes(hostPath)) LoadStateFace(state, std::move(bytes), subFontIndex); return SCE_FONT_OK; } @@ -309,7 +306,7 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon const std::uint32_t modeLow = source->flags & 0x0Fu; const std::uint32_t subFontIndex = source->open_info.sub_font_index; const std::uint32_t entryIndex = source->open_info.ctx_entry_index; - auto* header = static_cast(library->external_fonts_ctx); + auto* header = static_cast(FontContext(library, source)); if (!header) return fail(SCE_FONT_ERROR_INVALID_FONT_HANDLE); FontHandle target = setupFont; bool owned = false; @@ -371,7 +368,7 @@ int APS5_VABI sceFontOpenFontInstance(FontHandle fontHandle, FontHandle setupFon if (FontState* sourceState = TryGetState(fontHandle)) { targetState.scaleW = sourceState->scaleW; targetState.scaleH = sourceState->scaleH; - if (!sourceState->faceData.empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); + if (sourceState->faceData && !sourceState->faceData->empty()) LoadStateFace(targetState, sourceState->faceData, subFontIndex); } if (pFontHandle) *pFontHandle = target; return SCE_FONT_OK; @@ -385,6 +382,8 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, return SCE_FONT_ERROR_INVALID_LIBRARY; } if (!pFontHandle) return SCE_FONT_ERROR_INVALID_PARAMETER; + const std::optional file = FindSystemFontFile(fontSetType); + APS5_LOG_OUT("fontSetType=0x%08X file=%s", fontSetType, file ? file->path.string().c_str() : "(none)"); std::uint32_t libraryLock = 0; if (!AcquireLibraryLock(lib, libraryLock)) { *pFontHandle = nullptr; @@ -399,10 +398,20 @@ int APS5_VABI sceFontOpenFontSet(FontLibrary library, std::uint32_t fontSetType, rc = SCE_FONT_ERROR_INVALID_MEMORY; } else if (!lib->sysfonts_ctx) { rc = SCE_FONT_ERROR_NO_SUPPORT_FUNCTION; - } else if (std::find(SYSTEM_FONT_SET_TYPES.begin(), SYSTEM_FONT_SET_TYPES.end(), fontSetType) == SYSTEM_FONT_SET_TYPES.end()) { + } else if (!IsSystemFontSet(fontSetType)) { rc = SCE_FONT_ERROR_NO_SUPPORT_FONTSET; - } else { + } else if (!file) { rc = SCE_FONT_ERROR_FONT_OPEN_FAILED; + } else { + const std::string hostPathString = file->path.string(); + const OpenRequest request{lib, lib->sysfonts_ctx, openMode, openMode + 4, hostPathString.c_str(), 0, false, file->subFontIndex, static_cast(fontSetType)}; + const int openRc = OpenFontInContext(request, libraryLock, pFontHandle); + if (openRc == SCE_FONT_ERROR_FS_OPEN_FAILED || openRc == SCE_FONT_ERROR_NO_SUPPORT_FORMAT) return SCE_FONT_ERROR_FONT_OPEN_FAILED; + if (openRc != SCE_FONT_OK) return openRc; + GetNativeFont(*pFontHandle)->open_info.fontset_flags = fontSetType; + FontState& state = ResetState(*pFontHandle); + if (auto bytes = SystemFontBytes(file->path)) LoadStateFace(state, std::move(bytes), file->subFontIndex); + return SCE_FONT_OK; } ReleaseLibraryLock(lib, libraryLock); *pFontHandle = nullptr; diff --git a/core/libs/prx/libSceFont/src/State.cpp b/core/libs/prx/libSceFont/src/State.cpp index 58b7e0e49..6911fa24f 100644 --- a/core/libs/prx/libSceFont/src/State.cpp +++ b/core/libs/prx/libSceFont/src/State.cpp @@ -193,6 +193,10 @@ Font::FontObj* Font::FindSubFont(FontObj* head, std::uint32_t subFontIndex) { return nullptr; } +void* Font::FontContext(const FontLibNative* library, const FontHandleNative* font) { + return font->open_info.fontset_flags != 0 ? library->sysfonts_ctx : library->external_fonts_ctx; +} + void Font::ReleaseFontObjectsForHandle(FontHandleNative* font) { auto* library = static_cast(font->library); if (!library || library->magic != LIBRARY_MAGIC || !library->sys_driver || !library->sys_driver->close) return; @@ -201,7 +205,7 @@ void Font::ReleaseFontObjectsForHandle(FontHandleNative* font) { const std::uint32_t modeLow = font->flags & 0x0Fu; FontObj* head = nullptr; std::uint32_t lockWord = 0; - auto* entry = AcquireFontCtxEntry(library->external_fonts_ctx, fontId, modeLow, &head, &lockWord); + auto* entry = AcquireFontCtxEntry(FontContext(library, font), fontId, modeLow, &head, &lockWord); if (!entry) return; FontObj* match = FindSubFont(head, font->open_info.sub_font_index); const std::uint32_t openFlag = lockWord & OPEN_BIT; @@ -243,16 +247,16 @@ void Font::RemoveState(FontHandle handle) { fontStates.erase(handle); } -void Font::LoadStateFace(FontState& state, std::vector bytes, std::uint32_t subFontIndex) { +void Font::LoadStateFace(FontState& state, std::shared_ptr> bytes, std::uint32_t subFontIndex) { if (state.face) { FT_Done_Face(state.face); state.face = nullptr; } state.faceData = std::move(bytes); const FT_Library library = HostFreeTypeLibrary(); - if (!library || state.faceData.empty()) return; + if (!library || !state.faceData || state.faceData->empty()) return; FT_Face face = nullptr; - if (FT_New_Memory_Face(library, state.faceData.data(), static_cast(state.faceData.size()), static_cast(subFontIndex), &face) != 0) return; + if (FT_New_Memory_Face(library, state.faceData->data(), static_cast(state.faceData->size()), static_cast(subFontIndex), &face) != 0) return; FT_Select_Charmap(face, FT_ENCODING_UNICODE); state.face = face; } diff --git a/core/libs/prx/libSceFont/src/SystemFont.cpp b/core/libs/prx/libSceFont/src/SystemFont.cpp new file mode 100644 index 000000000..bf1281380 --- /dev/null +++ b/core/libs/prx/libSceFont/src/SystemFont.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#endif + +#include "prx/libSceFont/include/FontInternal.hpp" + +namespace { + +constexpr std::array SYSTEM_FONT_SET_TYPES = { + 0x18070043u, 0x18070044u, 0x18070045u, 0x18070047u, 0x18070053u, 0x18070054u, + 0x18070055u, 0x18070057u, 0x180700C3u, 0x180700C4u, 0x180700C5u, 0x180700C7u, + 0x18070444u, 0x18070447u, 0x18070454u, 0x18070457u, 0x180704C4u, 0x180704C7u, + 0x18071053u, 0x18071054u, 0x18071055u, 0x18071057u, 0x18071454u, 0x18071457u, + 0x18072444u, 0x18072447u, 0x180724C4u, 0x180724C7u, 0x18073454u, 0x18073457u, + 0x180734D4u, 0x180734D7u, 0x18078044u, 0x180780C4u, 0x18079054u, 0x1807A044u, + 0x1807A0C4u, 0x1807A444u, 0x1807A4C4u, 0x1807AC44u, 0x1807ACC4u, 0x1807B054u, + 0x1807B0D4u, 0x1807B454u, 0x1807B4D4u, 0x1807BC54u, 0x1807BCD4u, 0x18080444u, + 0x18080447u, 0x18080454u, 0x18080457u, 0x180804C4u, 0x180804C7u, 0x18081454u, + 0x18081457u, 0x18082444u, 0x18082447u, 0x180824C4u, 0x180824C7u, 0x18083454u, + 0x18083457u, 0x180834D4u, 0x180834D7u, 0x1808A444u, 0x1808A4C4u, 0x1808B454u, + 0x1808B4D4u, 0x180C8044u, 0x180C80C4u, 0x180C9054u, 0x180CA044u, 0x180CA0C4u, + 0x180CAC44u, 0x180CACC4u, 0x180CB054u, 0x180CB0D4u, 0x180CBC54u, 0x180CBCD4u, + 0x18170043u, 0x18170044u, 0x18170045u, 0x18170047u, 0x18170444u, 0x18170447u, + 0x18370044u, 0x18370047u, 0x18370444u, 0x18370447u, 0x1A070543u, 0x1A070547u, + 0x1A070553u, 0x1A070557u, 0x1A0705C3u, 0x1A0705C7u, 0x1A071553u, 0x1A071557u, + 0x1A072543u, 0x1A072547u, 0x1A0725C3u, 0x1A0725C7u, 0x1A073553u, 0x1A073557u, + 0x1A0735D3u, 0x1A0735D7u, 0x1A080543u, 0x1A080547u, 0x1A080553u, 0x1A080557u, + 0x1A0805C3u, 0x1A0805C7u, 0x1A081553u, 0x1A081557u, 0x1A082543u, 0x1A082547u, + 0x1A0825C3u, 0x1A0825C7u, 0x1A083553u, 0x1A083557u, 0x1A0835D3u, 0x1A0835D7u, +}; + +enum class Family { Latin, LatinItalic, Typewriter, Vietnamese, Thai, Japanese, ChineseGb }; + +struct FamilyFiles { + std::array system; + std::array substitute; + std::uint32_t substituteSubFont; +}; + +constexpr std::array FAMILY_FILES = {{ + {{"SST-Light.otf", "SST-Roman.otf", "SST-Medium.otf", "SST-Bold.otf"}, + {"NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-Bold.ttf"}, 0}, + {{"SST-LightItalic.otf", "SST-Italic.otf", "SST-MediumItalic.otf", "SST-BoldItalic.otf"}, + {"NotoSans-LightItalic.ttf", "NotoSans-Italic.ttf", "NotoSans-MediumItalic.ttf", "NotoSans-BoldItalic.ttf"}, 0}, + {{"SSTTypewriter-Roman.otf", "SSTTypewriter-Roman.otf", "SSTTypewriter-Roman.otf", "SSTTypewriter-Bd.otf"}, + {"NotoSansMono-Light.ttf", "NotoSansMono-Regular.ttf", "NotoSansMono-Medium.ttf", "NotoSansMono-Bold.ttf"}, 0}, + {{"SSTVietnamese-Light.otf", "SSTVietnamese-Roman.otf", "SSTVietnamese-Medium.otf", "SSTVietnamese-Bold.otf"}, + {"NotoSans-Light.ttf", "NotoSans-Regular.ttf", "NotoSans-Medium.ttf", "NotoSans-Bold.ttf"}, 0}, + {{"SSTThai-Light.otf", "SSTThai-Roman.otf", "SSTThai-Medium.otf", "SSTThai-Bold.otf"}, + {"NotoSansThai-Light.ttf", "NotoSansThai-Regular.ttf", "NotoSansThai-Medium.ttf", "NotoSansThai-Bold.ttf"}, 0}, + {{"SSTJpPro-Regular.otf", "SSTJpPro-Regular.otf", "SSTJpPro-Regular.otf", "SSTJpPro-Bold.otf"}, + {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 0}, + {{"DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf", "DFHEI5-SONY.ttf"}, + {"NotoSansCJK-Light.ttc", "NotoSansCJK-Regular.ttc", "NotoSansCJK-Medium.ttc", "NotoSansCJK-Bold.ttc"}, 2}, +}}; + +Family FamilyOf(std::uint32_t fontSetType) { + const std::uint32_t region = (fontSetType >> 16) & 0xFFu; + const std::uint32_t scripts = (fontSetType >> 8) & 0xFEu; + const bool vietnamese = (fontSetType & 0x10u) != 0; + switch (region) { + case 0x17: return Family::LatinItalic; + case 0x37: return Family::Typewriter; + case 0x08: return scripts == 0x04 && vietnamese ? Family::Vietnamese : Family::Japanese; + case 0x0C: return Family::ChineseGb; + default: + if (scripts == 0x10) return Family::Thai; + if (scripts == 0 && vietnamese) return Family::Vietnamese; + return Family::Latin; + } +} + +std::size_t WeightOf(std::uint32_t fontSetType) { + switch (fontSetType & 0x0Fu) { + case 0x3: return 0; + case 0x5: return 2; + case 0x7: return 3; + default: return 1; + } +} + +std::filesystem::path ExecutableDirectory() { +#ifdef _WIN32 + wchar_t module[MAX_PATH]; + const auto length = GetModuleFileNameW(nullptr, module, MAX_PATH); + if (length == 0 || length == MAX_PATH) throw std::runtime_error("libSceFont: cannot locate the executable"); + return std::filesystem::path(module).parent_path(); +#else + return std::filesystem::read_symlink("/proc/self/exe").parent_path(); +#endif +} + +} + +bool Font::IsSystemFontSet(std::uint32_t fontSetType) { + return std::find(SYSTEM_FONT_SET_TYPES.begin(), SYSTEM_FONT_SET_TYPES.end(), fontSetType) != SYSTEM_FONT_SET_TYPES.end(); +} + +std::vector Font::SystemFontCandidates(std::uint32_t fontSetType) { + if (!IsSystemFontSet(fontSetType)) return {}; + const FamilyFiles& files = FAMILY_FILES[static_cast(FamilyOf(fontSetType))]; + const std::size_t weight = WeightOf(fontSetType); + return {{std::filesystem::path(files.system[weight]), 0}, {std::filesystem::path(files.substitute[weight]), files.substituteSubFont}}; +} + +std::filesystem::path Font::SystemFontDirectory() { + const char* configured = std::getenv("ANYPS5_SYSTEM_FONTS"); + if (!configured || configured[0] == '\0') return ExecutableDirectory() / "anyps5-fonts"; + const std::filesystem::path directory(configured); + std::error_code error; + if (!std::filesystem::is_directory(directory, error)) throw std::runtime_error("libSceFont: ANYPS5_SYSTEM_FONTS is not a directory: " + directory.string()); + return directory; +} + +std::optional Font::FindSystemFontFile(std::uint32_t fontSetType) { + const std::vector candidates = SystemFontCandidates(fontSetType); + if (candidates.empty()) return std::nullopt; + const std::filesystem::path directory = SystemFontDirectory(); + for (const SystemFontFile& candidate : candidates) { + std::error_code error; + const std::filesystem::path path = directory / candidate.path; + if (std::filesystem::is_regular_file(path, error)) return SystemFontFile{path, candidate.subFontIndex}; + } + return std::nullopt; +} diff --git a/core/libs/prx/libSceFont/src/Unimplemented.cpp b/core/libs/prx/libSceFont/src/Unimplemented.cpp index c1b833af5..56e6eb21b 100644 --- a/core/libs/prx/libSceFont/src/Unimplemented.cpp +++ b/core/libs/prx/libSceFont/src/Unimplemented.cpp @@ -457,6 +457,16 @@ int APS5_VABI sceFontWritingLineWritesOrder() { return 0; } +int APS5_VABI sceFontGraphicsDrawupFillTextureImageObject(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceFontGraphicsDrawupFillTexturePatternObject(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } #pragma GCC visibility pop diff --git a/core/libs/prx/libSceFontFt/src/Driver.cpp b/core/libs/prx/libSceFontFt/src/Driver.cpp index 71c1ff144..ad09bd5a2 100644 --- a/core/libs/prx/libSceFontFt/src/Driver.cpp +++ b/core/libs/prx/libSceFontFt/src/Driver.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -26,6 +27,8 @@ struct FtLibraryContext { const FontMemoryInterface* iface; FT_Memory memory; FT_Library library; + FT_Memory hostMemory; + FT_Library hostLibrary; }; void* FtAlloc(FT_Memory memory, long size) { @@ -58,6 +61,42 @@ void* FtRealloc(FT_Memory memory, long currentSize, long newSize, void* block) { return result; } +void* HostAlloc(FT_Memory, long size) { + return size > 0 ? std::malloc(static_cast(size)) : nullptr; +} + +void HostFree(FT_Memory, void* block) { + std::free(block); +} + +void* HostRealloc(FT_Memory, long, long newSize, void* block) { + if (newSize <= 0) { + std::free(block); + return nullptr; + } + return std::realloc(block, static_cast(newSize)); +} + +FT_Library SystemFontLibrary(FtLibraryContext* ctx) { + if (ctx->hostLibrary) return ctx->hostLibrary; + auto* hostMemory = static_cast(ctx->iface->alloc(ctx->allocCtx, sizeof(FT_MemoryRec_))); + if (!hostMemory) return nullptr; + std::memset(hostMemory, 0, sizeof(*hostMemory)); + hostMemory->user = ctx; + hostMemory->alloc = &HostAlloc; + hostMemory->free = &HostFree; + hostMemory->realloc = &HostRealloc; + FT_Library hostLibrary = nullptr; + if (FT_New_Library(hostMemory, &hostLibrary) != 0 || !hostLibrary) { + ctx->iface->dealloc(ctx->allocCtx, hostMemory); + return nullptr; + } + FT_Add_Default_Modules(hostLibrary); + ctx->hostMemory = hostMemory; + ctx->hostLibrary = hostLibrary; + return hostLibrary; +} + void WriteU16(std::uint8_t* out, std::size_t offset, std::uint16_t value) { std::memcpy(out + offset, &value, sizeof(value)); } @@ -131,6 +170,14 @@ int APS5_VABI LibraryTerm(FontLibNative* library) { FT_Done_Library(ctx->library); ctx->library = nullptr; } + if (ctx->hostLibrary) { + FT_Done_Library(ctx->hostLibrary); + ctx->hostLibrary = nullptr; + } + if (ctx->hostMemory) { + freeFn(library->alloc_ctx, ctx->hostMemory); + ctx->hostMemory = nullptr; + } if (ctx->memory) { freeFn(library->alloc_ctx, ctx->memory); ctx->memory = nullptr; @@ -162,12 +209,15 @@ int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const } auto* ctx = static_cast(library->fontset_registry); if (!ctx || !ctx->library) return SCE_FONT_ERROR_INVALID_LIBRARY; + const bool systemSet = (library->sysfont_flags & SYSFONT_FLAG_SYSTEM_SET) != 0; + FT_Library* ftLibrary = systemSet ? &ctx->hostLibrary : &ctx->library; + if (systemSet && !SystemFontLibrary(ctx)) return SCE_FONT_ERROR_ALLOCATION_FAILED; FT_Face face = nullptr; FT_Error error; if (fromMemory) { - error = FT_New_Memory_Face(ctx->library, static_cast(fontAddress), static_cast(fontSize), static_cast(subFontIndex), &face); + error = FT_New_Memory_Face(*ftLibrary, static_cast(fontAddress), static_cast(fontSize), static_cast(subFontIndex), &face); } else { - error = FT_New_Face(ctx->library, static_cast(fontAddress), static_cast(subFontIndex), &face); + error = FT_New_Face(*ftLibrary, static_cast(fontAddress), static_cast(subFontIndex), &face); } if (error != 0 || !face) { if (fromMemory || error == FT_Err_Unknown_File_Format) return SCE_FONT_ERROR_NO_SUPPORT_FORMAT; @@ -186,7 +236,7 @@ int APS5_VABI LibraryOpenFont(FontLibNative* library, std::uint32_t mode, const obj->next = *inoutFontObj; if (obj->next) obj->next->prev = obj; obj->ft_face = face; - obj->ft_ctx_0x58 = &ctx->library; + obj->ft_ctx_0x58 = ftLibrary; *inoutFontObj = obj; return SCE_FONT_OK; } diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 473651532..900e9ce2e 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -13,8 +13,13 @@ if(BUILD_TESTING) target_include_directories(guest_http_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_http_tests PRIVATE libSceHttp) add_test(NAME guest_http COMMAND guest_http_tests) + add_test_executable(guest_http_response_header_tests ${LIBS_INCLUDE_DIR}/tests/GuestHttpResponseHeader.cpp) + target_include_directories(guest_http_response_header_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http_response_header_tests PRIVATE libSceHttp libc) + configure_windows_unwind(guest_http_response_header_tests) + add_test(NAME guest_http_response_header COMMAND guest_http_response_header_tests) if(WIN32) - set_tests_properties(guest_http PROPERTIES ENVIRONMENT_MODIFICATION + set_tests_properties(guest_http guest_http_response_header PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() endif() diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 0bbfa9e09..fa00ad2d7 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -4,6 +4,9 @@ #include "prx/libc/include/General.hpp" #include "prx/libSceHttp/src/HttpErrors.hpp" #include +#include +#include +#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. @@ -257,9 +260,66 @@ int APS5_VABI sceHttpSetRequestStatusCallback(void) { return 0; } -int APS5_VABI sceHttpParseResponseHeader(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceHttpParseResponseHeader(const char* header, std::size_t headerLen, const char* fieldStr, + const char** fieldValue, std::size_t* valueLen) { + constexpr int invalidResponse = static_cast(0x80432060); + constexpr int invalidValue = static_cast(0x804321FE); + constexpr int notFound = static_cast(0x80432025); + if (!header) return invalidResponse; + if (!fieldStr || !fieldValue || !valueLen) return invalidValue; + + std::size_t fieldLen = 0; + while (fieldLen < 0xfff && fieldStr[fieldLen] != '\0') ++fieldLen; + const std::string_view input(header, headerLen); + const auto isSpace = [](char c) { return c == ' ' || (c >= '\t' && c <= '\r'); }; + const auto lower = [](char c) { return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c; }; + std::size_t start = 0; + bool found = false; + while (start < headerLen) { + if (!isSpace(input[start]) && fieldLen < headerLen - start && input[start + fieldLen] == ':') { + std::size_t i = 0; + while (i < fieldLen && lower(input[start + i]) == lower(fieldStr[i])) ++i; + if (i == fieldLen) { + start += fieldLen + 1; + found = true; + break; + } + } + const auto newline = input.find('\n', start); + if (newline == std::string_view::npos) break; + start = newline + 1; + } + if (!found) return notFound; + + while (start < headerLen && isSpace(input[start])) { + if (input[start++] == '\n') break; + } + std::size_t end = headerLen; + std::size_t consumed = headerLen; + std::size_t scan = start; + while (scan < headerLen) { + const auto newline = input.find('\n', scan); + if (newline == std::string_view::npos) break; + const auto next = newline + 1; + if (next < headerLen && (input[next] == ' ' || input[next] == '\t')) { + scan = next; + continue; + } + end = newline > start && input[newline - 1] == '\r' ? newline - 1 : newline; + consumed = next; + break; + } + if (end == start) { + *fieldValue = nullptr; + *valueLen = 0; + return 0; + } + if (consumed > static_cast(std::numeric_limits::max())) { + throw std::out_of_range("sceHttpParseResponseHeader: consumed byte count exceeds int"); + } + *fieldValue = header + start; + *valueLen = end - start; + return static_cast(consumed); } int APS5_VABI sceHttpCreateRequest2(int conn_id, const char* method, const char* path, uint64_t content_length) { diff --git a/core/libs/prx/libSceHttp2/Export.cpp b/core/libs/prx/libSceHttp2/Export.cpp index 911b65730..adaa00e78 100644 --- a/core/libs/prx/libSceHttp2/Export.cpp +++ b/core/libs/prx/libSceHttp2/Export.cpp @@ -214,4 +214,24 @@ int APS5_VABI sceHttp2GetMemoryPoolStats() { return 0; } +int APS5_VABI sceHttp2CookieFlush(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2CreateCookieBox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetCookieBox(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceHttp2SetRequestNoContentLength(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceIme/CMakeLists.txt b/core/libs/prx/libSceIme/CMakeLists.txt index 77f03e7ea..a34676059 100644 --- a/core/libs/prx/libSceIme/CMakeLists.txt +++ b/core/libs/prx/libSceIme/CMakeLists.txt @@ -18,4 +18,14 @@ if(BUILD_TESTING) set_tests_properties(guest_ime_parameters PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + + add_test_executable(guest_ime_keyboard_tests ${LIBS_INCLUDE_DIR}/tests/GuestImeKeyboard.cpp) + target_include_directories(guest_ime_keyboard_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_ime_keyboard_tests PRIVATE libSceIme libc) + configure_windows_unwind(guest_ime_keyboard_tests) + add_test(NAME guest_ime_keyboard COMMAND guest_ime_keyboard_tests) + if(WIN32) + set_tests_properties(guest_ime_keyboard PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() endif() diff --git a/core/libs/prx/libSceIme/Export.cpp b/core/libs/prx/libSceIme/Export.cpp index 150180112..db1dd7a85 100644 --- a/core/libs/prx/libSceIme/Export.cpp +++ b/core/libs/prx/libSceIme/Export.cpp @@ -10,12 +10,18 @@ namespace { +constexpr int ErrorNotOpened = static_cast(0x80bc0002u); +constexpr int ErrorInvalidUserId = static_cast(0x80bc0010u); constexpr int ErrorInvalidType = static_cast(0x80bc0011u); constexpr int ErrorInvalidOption = static_cast(0x80bc0015u); constexpr int ErrorInvalidAddress = static_cast(0x80bc0031u); +constexpr int ErrorNoResourceId = static_cast(0x80bc0023u); +constexpr int ErrorInvalidMode = static_cast(0x80bc0024u); +constexpr uint32_t ValidKeyboardModes = 0x0000007f; constexpr uint32_t TypeNumber = 4; constexpr uint32_t ValidOptions = 0x00007bff; constexpr uint32_t OptionUseOver2K = 0x00004000; +constexpr int32_t UserIdInvalid = -1; std::mutex g_keyboardMutex; std::set g_openKeyboards; @@ -47,9 +53,9 @@ int APS5_VABI sceImeKeyboardClose(int32_t user_id) { int APS5_VABI sceImeKeyboardGetInfo(uint32_t resource_id, KeyboardInfo* info) { (void)resource_id; - (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!info) return ErrorInvalidAddress; + std::lock_guard lock(g_keyboardMutex); + return g_openKeyboards.empty() ? ErrorNotOpened : ErrorNoResourceId; } int APS5_VABI sceImeKeyboardGetResourceId(int32_t user_id, KeyboardResourceIdArray* resource_ids) { @@ -67,10 +73,10 @@ int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param) { } int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode) { - (void)user_id; - (void)mode; - NotImplemented_nid_no_patch(__func__); - return 0; + if (user_id == UserIdInvalid) return ErrorInvalidUserId; + std::lock_guard lock(g_keyboardMutex); + if (!g_openKeyboards.contains(user_id)) return ErrorNotOpened; + return (mode & ~ValidKeyboardModes) == 0 ? 0 : ErrorInvalidMode; } int APS5_VABI sceImeOpen_nid_postfix(const Param* param, const ExtendedParam* extended) { diff --git a/core/libs/prx/libSceMsgDialog.native/Export.cpp b/core/libs/prx/libSceMsgDialog.native/Export.cpp index 027eff38d..678c37088 100644 --- a/core/libs/prx/libSceMsgDialog.native/Export.cpp +++ b/core/libs/prx/libSceMsgDialog.native/Export.cpp @@ -65,6 +65,24 @@ int APS5_VABI sceMsgDialogUpdateStatus(void) { return g_status.load(); } +int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta) { + (void)target; + (void)delta; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; +} + +int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg) { + (void)target; + (void)msg; + return g_status.load() == COMMON_DIALOG_STATUS_RUNNING ? 0 : COMMON_DIALOG_ERROR_NOT_RUNNING; +} + +int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate) { + (void)target; + (void)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"); diff --git a/core/libs/prx/libSceNet/Export.cpp b/core/libs/prx/libSceNet/Export.cpp index 19a4b22bd..c5a55aa7f 100644 --- a/core/libs/prx/libSceNet/Export.cpp +++ b/core/libs/prx/libSceNet/Export.cpp @@ -49,6 +49,7 @@ constexpr int NET_ECONNABORTED = 53; constexpr int NET_EMSGSIZE = 40; constexpr int NET_ETIMEDOUT = 60; constexpr int NET_ECONNREFUSED = 61; +constexpr int NET_ERROR_BASE = static_cast(0x80410100u); constexpr int NET_ERROR_RESOLVER_ENODNS = static_cast(0x804101E1u); constexpr int NET_AF_INET = 2; @@ -65,6 +66,7 @@ constexpr int NET_SO_NBIO = 0x1200; using NativeSocket = SOCKET; using NativeLength = int; constexpr NativeSocket INVALID_NATIVE_SOCKET = INVALID_SOCKET; +constexpr int NATIVE_SEND_FLAGS = 0; bool initialize_sockets() { static const int result = [] { WSADATA data{}; @@ -95,6 +97,7 @@ int native_error() { using NativeSocket = int; using NativeLength = socklen_t; constexpr NativeSocket INVALID_NATIVE_SOCKET = -1; +constexpr int NATIVE_SEND_FLAGS = MSG_NOSIGNAL; bool initialize_sockets() { return true; } void close_socket(NativeSocket socket) { ::close(socket); } int native_error() { @@ -115,6 +118,7 @@ int native_error() { case EINTR: return 4; case EINVAL: return NET_EINVAL; case ENOTSOCK: return NET_ENOTSOCK; + case EPIPE: return 32; default: return 5; } } @@ -212,7 +216,7 @@ int* errno_slot() { int fail(int err) { *errno_slot() = err; - return -1; + return NET_ERROR_BASE | err; } void log_soft(const char* func, const char* what) { @@ -484,7 +488,7 @@ int64_t APS5_VABI sceNetSend(int s, const void* buf, size_t len, int flags) { if (it == g_socks.end()) return fail(NET_EBADF); socket = it->second; } - const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), 0); + const int result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS); return result >= 0 ? result : fail(native_error()); } @@ -502,13 +506,13 @@ int64_t APS5_VABI sceNetSendto(int s, const void* buf, size_t len, int flags, co int result; if (!to) { if (tolen != 0) return fail(NET_EINVAL); - result = ::send(socket.native->value, static_cast(buf), static_cast(len), 0); + result = ::send(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS); } else { sockaddr_storage destination{}; NativeLength destination_length = 0; if (!guest_to_native_address(to, tolen, destination, destination_length)) return fail(NET_EINVAL); if (destination.ss_family != (socket.family == NET_AF_INET ? AF_INET : AF_INET6)) return fail(NET_EAFNOSUPPORT); - result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), 0, + result = ::sendto(socket.native->value, static_cast(buf), static_cast(len), NATIVE_SEND_FLAGS, reinterpret_cast(&destination), destination_length); } return result >= 0 ? result : fail(native_error()); @@ -911,4 +915,10 @@ int APS5_VABI sceNetResolverStartAton(int rid, const void* addr, char* hostname, } return 0; } + +int APS5_VABI sceNetResolverGetError(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libScePad/CMakeLists.txt b/core/libs/prx/libScePad/CMakeLists.txt index ccb006f0d..75308dd0f 100644 --- a/core/libs/prx/libScePad/CMakeLists.txt +++ b/core/libs/prx/libScePad/CMakeLists.txt @@ -14,7 +14,7 @@ endif() if(BUILD_TESTING) add_test_executable(guest_pad_tests ${LIBS_INCLUDE_DIR}/tests/GuestPad.cpp) - target_include_directories(guest_pad_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_include_directories(guest_pad_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/SDL2/include) target_link_libraries(guest_pad_tests PRIVATE libScePad libc) configure_windows_unwind(guest_pad_tests) add_test(NAME guest_pad COMMAND guest_pad_tests) diff --git a/core/libs/prx/libScePad/Export.cpp b/core/libs/prx/libScePad/Export.cpp index f2437a5a5..adc42a6b7 100644 --- a/core/libs/prx/libScePad/Export.cpp +++ b/core/libs/prx/libScePad/Export.cpp @@ -233,4 +233,10 @@ int APS5_VABI scePadSetVibrationTriggerEffectWeakWhileEmbeddedMicInUse(bool enab return PAD_OK; } +APS5_EXPORT("fCWdlnmB1Ks", scePadUnknown_fCWdlnmB1Ks); +int APS5_VABI scePadUnknown_fCWdlnmB1Ks(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceShare/Export.cpp b/core/libs/prx/libSceShare/Export.cpp index caabed9b9..eb65ee977 100644 --- a/core/libs/prx/libSceShare/Export.cpp +++ b/core/libs/prx/libSceShare/Export.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -7,6 +8,7 @@ namespace { +constexpr std::int32_t ERROR_INVALID_PARAM = static_cast(0x81960002); constexpr std::int32_t ERROR_NOT_SUPPORTED = static_cast(0x81960007); constexpr std::int32_t REQUEST_ID_INVALID = -1; @@ -15,17 +17,19 @@ constexpr std::int32_t REQUEST_ID_INVALID = -1; extern "C" { int APS5_VABI sceShareCaptureScreenshot(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)param; + if (req_id != nullptr) { + *req_id = REQUEST_ID_INVALID; + } + return ERROR_NOT_SUPPORTED; } int APS5_VABI sceShareCaptureVideoClip(const void* param, int32_t* req_id) { - (void)param; - (void)req_id; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)param; + if (req_id != nullptr) { + *req_id = REQUEST_ID_INVALID; + } + return ERROR_NOT_SUPPORTED; } int APS5_VABI sceShareFeaturePermit(uint32_t feature_flags) { @@ -39,10 +43,11 @@ int APS5_VABI sceShareFeatureProhibit(uint32_t feature_flags) { } int APS5_VABI sceShareGetCurrentStatus(uint32_t feature_flag, ShareCurrentStatus* status) { - (void)feature_flag; - (void)status; - NotImplemented_nid_no_patch(__func__); - return 0; + if (feature_flag == 0 || status == nullptr) { + return ERROR_INVALID_PARAM; + } + std::memset(status, 0, sizeof(*status)); + return 0; } int APS5_VABI sceShareInitialize(size_t heap_size, int thread_priority, uint64_t affinity_mask) { @@ -53,9 +58,8 @@ int APS5_VABI sceShareInitialize(size_t heap_size, int thread_priority, uint64_t } int APS5_VABI sceShareOpenMenuForContent(const void* content_id) { - (void)content_id; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)content_id; + return ERROR_NOT_SUPPORTED; } int APS5_VABI sceShareRegisterContentEventCallback(void* callback, void* user_data) { diff --git a/core/libs/prx/libSceSsl/Export.cpp b/core/libs/prx/libSceSsl/Export.cpp index 4cca48fc7..5ef04eacb 100644 --- a/core/libs/prx/libSceSsl/Export.cpp +++ b/core/libs/prx/libSceSsl/Export.cpp @@ -73,4 +73,34 @@ int APS5_VABI sceSslGetMemoryPoolStats() { return 0; } +int APS5_VABI sceSslFreeSslCertName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetIssuerName(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetNameEntryCount(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetNameEntryInfo(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetPem(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSslGetSubjectName(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 205897fe2..19358d5c1 100644 --- a/core/libs/prx/libSceSystemService/Export.cpp +++ b/core/libs/prx/libSceSystemService/Export.cpp @@ -96,8 +96,7 @@ int APS5_VABI sceSystemServiceParamGetString(int paramId, char* buf, size_t bufS } int APS5_VABI sceSystemServicePowerTick(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceReceiveEvent(SystemServiceEvent* event) { @@ -109,8 +108,7 @@ int APS5_VABI sceSystemServiceReceiveEvent(SystemServiceEvent* event) { int APS5_VABI sceSystemServiceReportAbnormalTermination(const void* info) { (void)info; - NotImplemented_nid_no_patch(__func__); - return 0; + return SYSTEM_SERVICE_OK; } int APS5_VABI sceSystemServiceSetNoticeScreenSkipFlag(void) { @@ -137,4 +135,29 @@ int APS5_VABI sceSystemServiceLaunchPlayerDialog(const void* param) { return SYSTEM_SERVICE_OK; } +int APS5_VABI sceSystemServiceDisableMusicPlayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceOpenChallengeActivity(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceOpenTournamentOccurrence(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceReenableMusicPlayer(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceSystemServiceShowControllerSettings(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceUserService/Export.cpp b/core/libs/prx/libSceUserService/Export.cpp index 1695f4546..9ae783818 100644 --- a/core/libs/prx/libSceUserService/Export.cpp +++ b/core/libs/prx/libSceUserService/Export.cpp @@ -48,11 +48,20 @@ int APS5_VABI sceUserServiceGetAccessibilityZoomEnabled(int user_id, int32_t* zo return USER_SERVICE_OK; } +int APS5_VABI sceUserServiceGetAccessibilityZoomFollowFocus(int user_id, int32_t* zoom_follow_focus) { + if (zoom_follow_focus == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *zoom_follow_focus = 0; + return USER_SERVICE_OK; +} + int APS5_VABI sceUserServiceGetAgeLevel(int user_id, uint32_t* age_level) { - (void)user_id; - (void)age_level; - NotImplemented_nid_no_patch(__func__); - return 0; + if (age_level == nullptr || user_id != USER_SERVICE_INITIAL_USER_ID) { + return USER_SERVICE_ERROR_INVALID_ARGUMENT; + } + *age_level = 0; + return USER_SERVICE_OK; } // The initial user is reported as logging in once; afterwards there are no user events. diff --git a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp index adf59924a..97c1ef47f 100644 --- a/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp +++ b/core/libs/prx/libSceVideoOut/include/VideoOutDriver.hpp @@ -64,6 +64,7 @@ static constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0; static constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1; static constexpr std::uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE = 0; static constexpr std::uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8; +static constexpr std::uint32_t VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT = 0x10026c; static constexpr int VIDEO_OUT_EVENT_FLIP = 0; static constexpr int VIDEO_OUT_EVENT_VBLANK = 1; diff --git a/core/libs/prx/libSceVideoOut/src/Buffer.cpp b/core/libs/prx/libSceVideoOut/src/Buffer.cpp index 25fb6a241..5c8c2870e 100644 --- a/core/libs/prx/libSceVideoOut/src/Buffer.cpp +++ b/core/libs/prx/libSceVideoOut/src/Buffer.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -12,8 +13,18 @@ AgcDriver::DisplayBuffer DescribeVideoOutBuffer(const VideoOutBuffer& buffer, co const auto& attribute = group.attribute; if (attribute.reserved0 != 0 || attribute.pad0 != 0 || attribute.reserved1[0] != 0 || attribute.reserved1[1] != 0 || attribute.reserved1[2] != 0) throw std::runtime_error("VideoOut: reserved buffer attribute bits are set"); if (attribute.tiling_mode > 1 || (attribute.tiling_mode == 0 && attribute.pitch_in_pixel != 0) || attribute.aspect_ratio != 0 || (attribute.option != VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE && attribute.option != VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY)) throw std::runtime_error("VideoOut: unsupported tiling " + std::to_string(attribute.tiling_mode) + ", pitch " + std::to_string(attribute.pitch_in_pixel) + ", aspect ratio " + std::to_string(attribute.aspect_ratio) + " or buffer option " + std::to_string(attribute.option)); - if (group.category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED || buffer.metadataAddress != 0 || attribute.dcc_control != 0 || attribute.dcc_cb_register_clear_color != 0) throw std::runtime_error("VideoOut: DCC presentation is not implemented"); - const AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel}; + const auto hex = [](std::uint64_t value) { + char text[24]; + std::snprintf(text, sizeof(text), "0x%llx", static_cast(value)); + return std::string(text); + }; + const bool compressed = group.category == VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED; + if (!compressed && group.category != VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED) throw std::runtime_error("VideoOut: unknown buffer category " + std::to_string(group.category)); + if (!compressed && (buffer.metadataAddress != 0 || attribute.dcc_control != 0 || attribute.dcc_cb_register_clear_color != 0)) throw std::runtime_error("VideoOut: uncompressed buffer with DCC metadata " + hex(buffer.metadataAddress) + ", dcc_control " + hex(attribute.dcc_control) + " or DCC clear color " + hex(attribute.dcc_cb_register_clear_color)); + if (compressed && buffer.metadataAddress == 0) throw std::runtime_error("VideoOut: DCC-compressed buffer " + hex(buffer.dataAddress) + " without DCC metadata"); + if (compressed && attribute.tiling_mode != 0) throw std::runtime_error("VideoOut: DCC-compressed buffer " + hex(buffer.dataAddress) + " with linear tiling"); + if (compressed && (attribute.dcc_control & ~VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT) != 0) throw std::runtime_error("VideoOut: unsupported DCC control " + hex(attribute.dcc_control) + " (only the block size and independence fields " + hex(VIDEO_OUT_DCC_CONTROL_BLOCK_LAYOUT) + " are known)"); + const AgcDriver::DisplayBuffer result{buffer.dataAddress, attribute.pixel_format, attribute.width, attribute.height, attribute.tiling_mode, attribute.pitch_in_pixel, compressed ? buffer.metadataAddress : 0, compressed ? attribute.dcc_cb_register_clear_color : 0}; static_cast(AgcDriverDisplayBufferSize_nid_postfix(result)); return result; } diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 3469cb70d..ab3aa11a8 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -132,6 +132,9 @@ PadInputState PadInput::sampleController() const { addButton(SDL_CONTROLLER_BUTTON_Y, Pad::PadButton::Triangle); addButton(SDL_CONTROLLER_BUTTON_LEFTSHOULDER, Pad::PadButton::L1); addButton(SDL_CONTROLLER_BUTTON_RIGHTSHOULDER, Pad::PadButton::R1); + const auto type = SDL_GameControllerGetType(controller); + const bool viewPressed = type != SDL_CONTROLLER_TYPE_PS4 && type != SDL_CONTROLLER_TYPE_PS5 && readButton(SDL_CONTROLLER_BUTTON_BACK); + if (viewPressed) result.buttons |= static_cast(Pad::PadButton::TouchPad); addButton(SDL_CONTROLLER_BUTTON_START, Pad::PadButton::Options); addButton(SDL_CONTROLLER_BUTTON_LEFTSTICK, Pad::PadButton::L3); addButton(SDL_CONTROLLER_BUTTON_RIGHTSTICK, Pad::PadButton::R3); @@ -185,6 +188,9 @@ PadInputState PadInput::sampleController() const { result.touch[finger].y = static_cast(std::clamp(y, 0.0f, 1.0f) * 942.0f); } } + if (viewPressed && !result.touch[0].active && !result.touch[1].active) { + result.touch[0] = {true, 960, 471, 0}; + } return result; } diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index 9dd1463b0..c0835a26e 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -6,6 +6,7 @@ #include "prx/libSceAgcDriver/Execution/include/Driver.hpp" #include "prx/libSceAgcDriver/Submit/include/Dcb.hpp" #include "prx/libc/include/Shutdown.hpp" +#include #include #include #include @@ -174,6 +175,26 @@ void testDecode() { group.attribute.option = option; check(expectFailure([&] { DescribeVideoOutBuffer(registered, group); }).find("buffer option " + std::to_string(option)) != std::string::npos, "an unsupported buffer option was accepted"); } + VideoOutBuffer dccBuffer{0, reinterpret_cast(tiled.data()), 0x7f0000}; + sceVideoOutSetBufferAttribute2(&group.attribute, 0x8000000000000000ull, 0, 259, 137, VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_NONE, 0x208, 0x11223344); + group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED; + const auto compressed = DescribeVideoOutBuffer(dccBuffer, group); + check(compressed.address == dccBuffer.dataAddress && compressed.dccAddress == 0x7f0000 && compressed.dccClearColor == 0x11223344, "DCC metadata or clear color was not described"); + group.attribute.dcc_control = 0x100024; + check(DescribeVideoOutBuffer(dccBuffer, group).dccAddress == 0x7f0000, "independent 128-byte DCC blocks were rejected"); + group.attribute.dcc_control = 0x209; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("DCC control 0x209") != std::string::npos, "an unknown DCC control bit was accepted"); + group.attribute.dcc_control = 0x208; + group.attribute.tiling_mode = 1; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("linear") != std::string::npos, "a linear DCC buffer was accepted"); + group.attribute.tiling_mode = 0; + dccBuffer.metadataAddress = 0; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("without DCC metadata") != std::string::npos, "a compressed buffer without metadata was accepted"); + dccBuffer.metadataAddress = 0x7f0000; + group.category = VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("DCC metadata 0x7f0000") != std::string::npos, "an uncompressed buffer with DCC metadata was accepted"); + group.category = 2; + check(expectFailure([&] { DescribeVideoOutBuffer(dccBuffer, group); }).find("category 2") != std::string::npos, "an unknown buffer category was accepted"); } void testOpenParam() { @@ -284,6 +305,36 @@ void testPresentation(bool expectUnavailable) { LibcRunShutdown_nid_postfix(); } +void testCompressedPresentation() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::vector allocation(6 * 65536 + 65535); + const auto storage = alignedBuffer(allocation); + fillBuffer(storage, 259, 137); + std::vector keys(storage.size() / 256); + VideoOutBuffers buffer{storage.data(), keys.data(), {nullptr, nullptr}}; + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0x208, 0xff102030); + sceVideoOutRegisterBuffers2(handle, 0, 0, &buffer, 1, &attribute, VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED, nullptr); + int64_t argument = 2000; + for (std::uint8_t key : {0x00, 0xff, 0x20, 0xc0}) { + std::fill(keys.begin(), keys.end(), key); + uint64_t target; + { + std::lock_guard lock(cfg->mutex); + target = cfg->flipStatus.count + 1; + } + sceVideoOutSubmitFlip(handle, 0, 1, ++argument); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == target; }), "DCC presentation did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.flipPendingNum == 0, "DCC flip status is wrong"); + } + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); +} + // Flips of two buffers submitted without waiting for each other: every one completes in order // (presentations trail on the GPU, APS5_FLIP_INFLIGHT), and the shutdown retires the ones in flight. void testBackToBack() { @@ -426,6 +477,7 @@ int main(int argc, char** argv) { else if (argc == 2 && std::string(argv[1]) == "controls") testControls(); else if (argc == 2 && std::string(argv[1]) == "present") testPresentation(false); else if (argc == 2 && std::string(argv[1]) == "backtoback") testBackToBack(); + else if (argc == 2 && std::string(argv[1]) == "dcc") testCompressedPresentation(); else if (argc == 2 && std::string(argv[1]) == "pacing") testReleaseVblank(); else if (argc == 2 && std::string(argv[1]) == "aftergpu") testFlipAfterGpuWork(); else if (argc == 2 && std::string(argv[1]) == "unavailable") testPresentation(true); diff --git a/core/libs/prx/libSceVoiceQoS/CMakeLists.txt b/core/libs/prx/libSceVoiceQoS/CMakeLists.txt new file mode 100644 index 000000000..29d56e9ad --- /dev/null +++ b/core/libs/prx/libSceVoiceQoS/CMakeLists.txt @@ -0,0 +1,21 @@ +add_library(libSceVoiceQoS SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceVoiceQoS PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceVoiceQoS) + +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_voice_qos_tests ${LIBS_INCLUDE_DIR}/tests/GuestVoiceQoS.cpp) + target_include_directories(guest_voice_qos_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_voice_qos_tests PRIVATE libSceVoiceQoS libc) + configure_windows_unwind(guest_voice_qos_tests) + add_test(NAME guest_voice_qos COMMAND guest_voice_qos_tests) + if(WIN32) + set_tests_properties(guest_voice_qos PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceVoiceQoS/Export.cpp b/core/libs/prx/libSceVoiceQoS/Export.cpp index 4cb9ab16f..20ff9b24d 100644 --- a/core/libs/prx/libSceVoiceQoS/Export.cpp +++ b/core/libs/prx/libSceVoiceQoS/Export.cpp @@ -1,16 +1,29 @@ #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr std::int32_t VOICE_QOS_APP_TYPE_GAME = 0x20000000; + +std::mutex g_mutex; +bool g_initialized = false; + +} + extern "C" { int APS5_VABI sceVoiceQoSInit(void* mem_block, uint32_t mem_size, int32_t app_type) { - (void)mem_block; - (void)mem_size; - (void)app_type; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!mem_block || mem_size == 0) throw std::invalid_argument("sceVoiceQoSInit: null or empty memory block"); + if (app_type != VOICE_QOS_APP_TYPE_GAME) throw std::invalid_argument("sceVoiceQoSInit: unsupported app type " + std::to_string(app_type)); + std::lock_guard lock(g_mutex); + if (g_initialized) throw std::runtime_error("sceVoiceQoSInit: already initialized"); + g_initialized = true; + return 0; } int APS5_VABI sceVoiceQoSEnd() { diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 070d85afa..847b8ffc3 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -77,6 +77,16 @@ if(BUILD_TESTING) set_tests_properties(guest_math PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_execute_once_tests ${LIBS_INCLUDE_DIR}/tests/GuestExecuteOnce.cpp) + target_include_directories(guest_execute_once_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_execute_once_tests PRIVATE libc) + configure_windows_unwind(guest_execute_once_tests) + add_test(NAME guest_execute_once COMMAND guest_execute_once_tests) + set_tests_properties(guest_execute_once PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_execute_once PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_working_directory_tests ${LIBS_INCLUDE_DIR}/tests/GuestWorkingDirectory.cpp) target_include_directories(guest_working_directory_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_working_directory_tests PRIVATE libc) @@ -176,6 +186,15 @@ if(BUILD_TESTING) set_tests_properties(guest_strings PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_wcsrtombs_s_tests ${LIBS_INCLUDE_DIR}/tests/GuestWcsrtombsS.cpp) + target_include_directories(guest_wcsrtombs_s_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_wcsrtombs_s_tests PRIVATE libc) + configure_windows_unwind(guest_wcsrtombs_s_tests) + add_test(NAME guest_wcsrtombs_s COMMAND guest_wcsrtombs_s_tests) + if(WIN32) + set_tests_properties(guest_wcsrtombs_s PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(UNIX) add_test_executable(exception_runtime_tests ${LIBS_INCLUDE_DIR}/tests/ExceptionRuntime.cpp) target_include_directories(exception_runtime_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libc/Export.cpp b/core/libs/prx/libc/Export.cpp index 3fb873fd7..bdb6ca001 100644 --- a/core/libs/prx/libc/Export.cpp +++ b/core/libs/prx/libc/Export.cpp @@ -38,15 +38,6 @@ int APS5_VABI libcUnknown_u2tMGOLaqnE() { return 0; } -// Live float->float import of Cyberpunk 2077 (unnamed); callers pass one -// float in xmm0 and consume the result, so return 0.0f instead of integer 0. -APS5_EXPORT("DQbtGaBKlaw", libcCyberUnknown17); -float APS5_VABI libcCyberUnknown17(float x) { - (void)x; - NotImplemented_nid_no_patch("DQbtGaBKlaw"); - return 0.0f; -} - // Live Cyberpunk 2077 import used in an fopen/fseek/ftell-like file-size // idiom as (handle, 0, 2); returning 0 reports success. APS5_EXPORT("rWSuTWY2JN0", libcCyberUnknown18); @@ -64,12 +55,6 @@ int APS5_VABI libcCyberUnknown19(void) { } -APS5_EXPORT("DiGVep5yB5w", libcCyberUnknown01); -std::uint64_t APS5_VABI libcCyberUnknown01(void) { - NotImplemented_nid_no_patch("DiGVep5yB5w"); - return 0; -} - APS5_EXPORT("Ye20uNnlglA", libcCyberUnknown02); std::uint64_t APS5_VABI libcCyberUnknown02(void) { NotImplemented_nid_no_patch("Ye20uNnlglA"); @@ -82,24 +67,12 @@ std::uint64_t APS5_VABI libcCyberUnknown03(void) { return 0; } -APS5_EXPORT("79s2tnYQI6I", libcCyberUnknown04); -std::uint64_t APS5_VABI libcCyberUnknown04(void) { - NotImplemented_nid_no_patch("79s2tnYQI6I"); - return 0; -} - APS5_EXPORT("7yMFgcS8EPA", libcCyberUnknown05); std::uint64_t APS5_VABI libcCyberUnknown05(void) { NotImplemented_nid_no_patch("7yMFgcS8EPA"); return 0; } -APS5_EXPORT("Cj+Fw5q1tUo", libcCyberUnknown06); -std::uint64_t APS5_VABI libcCyberUnknown06(void) { - NotImplemented_nid_no_patch("Cj+Fw5q1tUo"); - return 0; -} - APS5_EXPORT("CyXs2l-1kNA", libcCyberUnknown07); std::uint64_t APS5_VABI libcCyberUnknown07(void) { NotImplemented_nid_no_patch("CyXs2l-1kNA"); diff --git a/core/libs/prx/libc/src/CxxAbiSupport.cpp b/core/libs/prx/libc/src/CxxAbiSupport.cpp index b3e467664..8f32a8572 100644 --- a/core/libs/prx/libc/src/CxxAbiSupport.cpp +++ b/core/libs/prx/libc/src/CxxAbiSupport.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "prx/libc/include/General.hpp" @@ -40,4 +41,15 @@ int APS5_VABI LibcInternalExtCxaThreadAtexit_nid_postfix(void (*destructor)(void const std::error_category* _ZSt17iostream_categoryv_nid_postfix() { return &std::iostream_category(); } +int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix( + int* flag, int (APS5_VABI *callback)(void*, void*, void**), void* arg +) { + static std::recursive_mutex mutex; + std::lock_guard lock(mutex); + if (*flag != 0) return 1; + if (callback(nullptr, arg, nullptr) == 0) return 0; + *flag = 1; + return 1; +} + } diff --git a/core/libs/prx/libc/src/Multibyte.cpp b/core/libs/prx/libc/src/Multibyte.cpp index 0ea78acb3..c521ced22 100644 --- a/core/libs/prx/libc/src/Multibyte.cpp +++ b/core/libs/prx/libc/src/Multibyte.cpp @@ -3,6 +3,7 @@ #include #include #include +#include extern "C" { @@ -47,6 +48,78 @@ std::size_t APS5_VABI wcrtomb_nid_postfix(char* destination, std::uint16_t value return 1; } +int APS5_VABI wcsrtombs_s_nid_postfix( + std::size_t* result, char* destination, std::size_t capacity, const std::uint16_t** source, std::size_t limit, + void* state +) { + constexpr int GuestEinval = 22; + constexpr int GuestErange = 34; + constexpr int GuestEilseq = 86; + constexpr auto Failed = static_cast(-1); + if (!result || !source || !state || !*source || (destination && (static_cast(capacity) <= 0 + || static_cast(limit) < 0))) { + if (destination && static_cast(capacity) > 0) destination[0] = '\0'; + if (result) *result = Failed; + return GuestEinval; + } + if (!destination && capacity) { + *result = Failed; + return GuestEinval; + } + const auto* wide = *source; + char bytes[MB_LEN_MAX]; + if (!destination) { + std::size_t total = 0; + for (;; ++wide) { + const auto length = wcrtomb_nid_postfix(bytes, *wide, state); + if (length == Failed) { + *result = Failed; + return GuestEilseq; + } + if (length && bytes[length - 1] == '\0') { + *result = total + length - 1; + return 0; + } + total += length; + } + } + std::size_t written = 0; + while (limit) { + const auto length = wcrtomb_nid_postfix(bytes, *wide, state); + if (length == Failed) { + destination[written] = '\0'; + *result = Failed; + return GuestEilseq; + } + if (length > limit || length > capacity) { + *source = wide; + destination[0] = '\0'; + *result = Failed; + return GuestErange; + } + std::memcpy(destination + written, bytes, length); + written += length; + if (length && destination[written - 1] == '\0') { + *source = nullptr; + *result = written - 1; + return 0; + } + ++wide; + capacity -= length; + if (!capacity) { + *source = wide; + destination[0] = '\0'; + *result = Failed; + return GuestErange; + } + limit -= length; + } + *source = wide; + destination[written] = '\0'; + *result = written; + return 0; +} + std::size_t APS5_VABI mbsrtowcs_nid_postfix(std::uint16_t* destination, const char** source, std::size_t capacity, void*) { if (!destination) return std::strlen(*source); std::size_t converted = 0; diff --git a/core/libs/prx/libc/src/StringExtensions.cpp b/core/libs/prx/libc/src/StringExtensions.cpp index 9533fd26b..a9a759763 100644 --- a/core/libs/prx/libc/src/StringExtensions.cpp +++ b/core/libs/prx/libc/src/StringExtensions.cpp @@ -26,6 +26,10 @@ std::size_t APS5_VABI strnlen_nid_postfix(const char* text, std::size_t limit) { return length; } +std::size_t APS5_VABI strnlen_s_nid_postfix(const char* text, std::size_t limit) { + return text ? strnlen_nid_postfix(text, limit) : 0; +} + char* APS5_VABI strncat_nid_postfix(char* destination, const char* source, std::size_t limit) { return std::strncat(destination, source, limit); } diff --git a/core/libs/prx/libc/src/Time.cpp b/core/libs/prx/libc/src/Time.cpp index 64bc20ddb..53199eed7 100644 --- a/core/libs/prx/libc/src/Time.cpp +++ b/core/libs/prx/libc/src/Time.cpp @@ -21,6 +21,19 @@ int64_t APS5_VABI time_nid_postfix(int64_t* timer) { return libc_time_nid_postfix(timer); } +int64_t APS5_VABI _Xtime_get_ticks_nid_postfix() { +#ifdef _WIN32 + FILETIME ft{}; + GetSystemTimePreciseAsFileTime(&ft); + const uint64_t t = ((static_cast(ft.dwHighDateTime) << 32) | ft.dwLowDateTime) - 116444736000000000ULL; + return static_cast(t / 10); +#else + timespec now{}; + clock_gettime(CLOCK_REALTIME, &now); + return static_cast(now.tv_sec) * 1000000 + now.tv_nsec / 1000; +#endif +} + double APS5_VABI libc_difftime_nid_postfix(int64_t time1, int64_t time0) { return std::difftime(static_cast(time1), static_cast(time0)); } diff --git a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp index aa36abe72..a584cf3b2 100644 --- a/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp +++ b/core/libs/prx/libkernel/AppMetadata/src/ParamJsonParser.cpp @@ -249,12 +249,21 @@ ParsedParamJson parseParamJson(const std::filesystem::path& paramJsonPath) { if (localizedParametersValue->type != JsonType::Object) throw std::runtime_error("param.json localizedParameters is not an object"); if (localizedParametersValue->objectValue.empty()) throw std::runtime_error("param.json localizedParameters is empty"); std::string selectedLanguage; - const JsonValue* defaultLanguageValue = findObjectMember(root, "defaultLanguage"); + const JsonValue* defaultLanguageValue = findObjectMember(*localizedParametersValue, "defaultLanguage"); + if (defaultLanguageValue == nullptr) defaultLanguageValue = findObjectMember(root, "defaultLanguage"); if (defaultLanguageValue != nullptr) selectedLanguage = asString(*defaultLanguageValue, "defaultLanguage"); const JsonValue* languageObject = nullptr; if (!selectedLanguage.empty()) languageObject = findObjectMember(*localizedParametersValue, selectedLanguage); if (languageObject == nullptr) languageObject = findObjectMember(*localizedParametersValue, "en-US"); - if (languageObject == nullptr) languageObject = &localizedParametersValue->objectValue.front().second; + if (languageObject == nullptr) { + for (const auto& member : localizedParametersValue->objectValue) { + if (member.second.type == JsonType::Object) { + languageObject = &member.second; + break; + } + } + } + if (languageObject == nullptr) throw std::runtime_error("param.json localizedParameters has no language entry"); const JsonValue* titleValue = findObjectMember(*languageObject, "titleName"); if (titleValue == nullptr) throw std::runtime_error("param.json localized entry is missing titleName"); const std::string title = asString(*titleValue, "titleName"); diff --git a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp index ecdbd67e6..99bd8beb7 100644 --- a/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp +++ b/core/libs/prx/libkernel/Apr/include/AprCommandBuffer.hpp @@ -8,7 +8,7 @@ // The guest treats both the command buffer object and its memory as opaque, so the encoding is ours. namespace Apr { -enum class BufferType : std::uint32_t { +enum class BufferType : std::uint16_t { Generic = 0, Apr = 1, }; @@ -19,9 +19,12 @@ struct CommandBufferObject { std::uint32_t offset; std::uint32_t numCommands; BufferType type; + std::uint16_t recording; }; static_assert(sizeof(CommandBufferObject) == 0x18, "guest reserves 0x18 bytes for sce::Ampr::CommandBuffer"); +inline constexpr std::uint16_t ScatterGatherValid = 1; + enum class Opcode : std::uint32_t { Nop = 0, ReadFile = 1, @@ -36,6 +39,10 @@ enum class Opcode : std::uint32_t { PushMarker = 10, PopMarker = 11, SetMarker = 12, + ReadFileGather = 13, + ReadFileScatter = 14, + ReadFileGatherScatter = 15, + ResetGatherScatterState = 16, }; struct CommandHeader { @@ -52,6 +59,10 @@ struct ReadFileCommand { std::uint64_t offset; }; +struct ResetGatherScatterStateCommand { + CommandHeader header; +}; + struct WriteAddressCommand { CommandHeader header; std::uint64_t address; @@ -60,10 +71,32 @@ struct WriteAddressCommand { std::uint32_t reserved; }; +enum class CounterAccess : std::uint8_t { + Size8 = 0, + Size4 = 1, + Size2Offset0 = 2, + Size2Offset1 = 3, + Size1Offset0 = 4, + Size1Offset1 = 5, + Size1Offset2 = 6, + Size1Offset3 = 7, +}; + +enum class CounterOperation : std::uint8_t { + Store = 0, + AtomicOr = 1, + AtomicAndComplement = 2, + AtomicXor = 3, + AtomicAdd = 4, +}; + struct WriteCounterCommand { CommandHeader header; std::uint32_t counter; - std::uint32_t value; + CounterAccess access; + CounterOperation operation; + std::uint16_t reserved; + std::uint64_t value; }; struct WaitCommand { @@ -72,7 +105,9 @@ struct WaitCommand { std::uint64_t reference; std::uint64_t mask; std::uint32_t counter; - std::uint32_t compare; + std::uint8_t compare; + CounterAccess access; + std::uint16_t reserved; }; struct WriteKernelEventQueueCommand { diff --git a/core/libs/prx/libkernel/Apr/src/Apr.cpp b/core/libs/prx/libkernel/Apr/src/Apr.cpp index 9152f9419..7c652701b 100644 --- a/core/libs/prx/libkernel/Apr/src/Apr.cpp +++ b/core/libs/prx/libkernel/Apr/src/Apr.cpp @@ -161,10 +161,63 @@ void _writeAddress(const Apr::WriteAddressCommand& command) { std::array, 256> g_counters{}; +std::mutex g_counterWrites; + std::uint32_t _counter(std::uint32_t index) { return g_counters[index % g_counters.size()].load(std::memory_order_acquire); } +struct CounterField { + std::uint32_t bits; + std::uint32_t shift; +}; + +CounterField _counterField(Apr::CounterAccess access) { + const auto value = static_cast(access); + if (value == 0) return {64, 0}; + if (value == 1) return {32, 0}; + if (value < 4) return {16, (value - 2) * 16}; + if (value < 8) return {8, (value - 4) * 8}; + throw std::runtime_error("APR: counter access " + std::to_string(value) + " not implemented"); +} + +std::uint64_t _fieldMask(std::uint32_t bits) { + return bits == 64 ? ~std::uint64_t{0} : (std::uint64_t{1} << bits) - 1u; +} + +std::uint64_t _readCounter(std::uint32_t index, Apr::CounterAccess access) { + const auto field = _counterField(access); + if (field.bits == 64) return _counter(index) | static_cast(_counter(index + 1u)) << 32u; + return (_counter(index) >> field.shift) & _fieldMask(field.bits); +} + +std::uint64_t _applyCounterOperation(Apr::CounterOperation operation, std::uint64_t current, std::uint64_t value) { + switch (operation) { + case Apr::CounterOperation::Store: return value; + case Apr::CounterOperation::AtomicOr: return current | value; + case Apr::CounterOperation::AtomicAndComplement: return current & ~value; + case Apr::CounterOperation::AtomicXor: return current ^ value; + case Apr::CounterOperation::AtomicAdd: return current + value; + } + throw std::runtime_error("APR: counter operation " + std::to_string(static_cast(operation)) + " not implemented"); +} + +void _writeCounter(const Apr::WriteCounterCommand& command) { + const auto field = _counterField(command.access); + const std::lock_guard lock(g_counterWrites); + const std::uint64_t current = _readCounter(command.counter, command.access); + const std::uint64_t next = _applyCounterOperation(command.operation, current, command.value) & _fieldMask(field.bits); + auto& low = g_counters[command.counter % g_counters.size()]; + if (field.bits == 64) { + g_counters[(command.counter + 1u) % g_counters.size()].store(static_cast(next >> 32u), std::memory_order_release); + low.store(static_cast(next), std::memory_order_release); + return; + } + const auto mask = static_cast(_fieldMask(field.bits) << field.shift); + const auto bits = static_cast(next << field.shift); + low.store((low.load(std::memory_order_relaxed) & ~mask) | bits, std::memory_order_release); +} + bool _waitSatisfied(std::uint32_t compare, std::uint64_t value, std::uint64_t reference) { constexpr std::uint64_t sign = std::uint64_t{1} << 63u; switch (compare) { @@ -186,8 +239,27 @@ TCommand _read(const Apr::CommandBufferObject& buffer, std::uint32_t cursor) { return command; } +struct ReadCursor { + bool valid = false; + std::uint32_t fileId = 0; + std::uint64_t nextDestination = 0; + std::uint64_t nextOffset = 0; +}; + +void _readResolved(Apr::Opcode opcode, Apr::ReadFileCommand command, ReadCursor& read) { + if (opcode != Apr::Opcode::ReadFile) { + if (!read.valid) throw std::runtime_error("APR: gather or scatter read without a preceding read"); + command.fileId = read.fileId; + if (opcode == Apr::Opcode::ReadFileGather) command.destination = read.nextDestination; + if (opcode == Apr::Opcode::ReadFileScatter) command.offset = read.nextOffset; + } + _readFile(command); + read = {true, command.fileId, command.destination + command.size, command.offset + command.size}; +} + void _execute(const Apr::CommandBufferObject& buffer) { std::uint32_t cursor = 0; + ReadCursor read; for (std::uint32_t index = 0; index < buffer.numCommands; ++index) { if (cursor + sizeof(Apr::CommandHeader) > buffer.offset) throw std::runtime_error("APR: truncated command buffer"); Apr::CommandHeader header; @@ -199,12 +271,15 @@ void _execute(const Apr::CommandBufferObject& buffer) { case Apr::Opcode::PopMarker: case Apr::Opcode::SetMarker: break; - case Apr::Opcode::ReadFile: { - Apr::ReadFileCommand command; - std::memcpy(&command, buffer.base + cursor, sizeof(command)); - _readFile(command); + case Apr::Opcode::ReadFile: + case Apr::Opcode::ReadFileGather: + case Apr::Opcode::ReadFileScatter: + case Apr::Opcode::ReadFileGatherScatter: + _readResolved(header.opcode, _read(buffer, cursor), read); + break; + case Apr::Opcode::ResetGatherScatterState: + read = {}; break; - } case Apr::Opcode::WriteAddress: { Apr::WriteAddressCommand command; std::memcpy(&command, buffer.base + cursor, sizeof(command)); @@ -212,18 +287,20 @@ void _execute(const Apr::CommandBufferObject& buffer) { break; } case Apr::Opcode::WriteCounter: { - const auto command = _read(buffer, cursor); - g_counters[command.counter % g_counters.size()].store(command.value, std::memory_order_release); + _writeCounter(_read(buffer, cursor)); break; } case Apr::Opcode::WaitOnAddress: case Apr::Opcode::WaitOnCounter: { const auto command = _read(buffer, cursor); + const bool counter = header.opcode == Apr::Opcode::WaitOnCounter; + const std::uint32_t unused = counter ? 64u - _counterField(command.access).bits : 0u; const auto current = [&]() -> std::uint64_t { - if (header.opcode == Apr::Opcode::WaitOnCounter) return _counter(command.counter); + if (counter) return _readCounter(command.counter, command.access); return std::atomic_ref(*reinterpret_cast(command.address)).load(std::memory_order_acquire); }; - while (!_waitSatisfied(command.compare, current() & command.mask, command.reference & command.mask)) std::this_thread::sleep_for(std::chrono::microseconds(50)); + const std::uint64_t reference = (command.reference & command.mask) << unused; + while (!_waitSatisfied(command.compare, (current() & command.mask) << unused, reference)) std::this_thread::sleep_for(std::chrono::microseconds(50)); break; } case Apr::Opcode::WriteKernelEventQueue: { diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index cf4c3a305..4d597c117 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -487,4 +487,14 @@ int APS5_VABI sceKernelUtimes_nid_postfix(const char* path, const KernelTimeval* return 0; } +int APS5_VABI utimes_nid_postfix(const char* path, const KernelTimeval* times) { + if (const int error = PathError(path)) return PosixFailure(error); + return PosixResult(sceKernelUtimes_nid_postfix(path, times)); +} + +int APS5_VABI fsync_nid_postfix(int fd) { + if (sceKernelFsync(fd) != 0) return PosixFailure(errno); + return 0; +} + } diff --git a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp index e1091def8..ef4790b78 100644 --- a/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp +++ b/core/libs/prx/libkernel/Module/src/DynamicLoader.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/General.hpp" #include #include +#include #include #include #include @@ -57,6 +58,13 @@ char* APS5_VABI dlerror_nid_postfix() { pendingError = false; return loaderError.data(); } +static std::filesystem::path RelinkedModulePath(const std::filesystem::path& path) { + auto relinked = path; + relinked += ".guest.prx"; + std::error_code error; + return std::filesystem::is_regular_file(relinked, error) ? relinked : path; +} + void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { if ((flags & ~0x103) || (flags & 3) == 0 || (flags & 3) == 3) { Error("dlopen: unsupported flags"); return nullptr; @@ -70,7 +78,7 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { module->owned = false; } else { if (!*path) { Error("dlopen: empty module path"); return nullptr; } - const auto resolved = ResolvePath_nid_no_patch(path); + const auto resolved = RelinkedModulePath(ResolvePath_nid_no_patch(path)); module->native = LoadLibraryExW(resolved.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); } if (!module->native) { @@ -79,7 +87,7 @@ void* APS5_VABI dlopen_nid_postfix(const char* path, int flags) { Error(message); return nullptr; } #else - const auto resolved = path ? ResolvePath_nid_no_patch(path).string() : std::string{}; + const auto resolved = path ? RelinkedModulePath(ResolvePath_nid_no_patch(path)).string() : std::string{}; const int nativeFlags = ((flags & 3) == 1 ? RTLD_LAZY : RTLD_NOW) | ((flags & 0x100) ? RTLD_GLOBAL : RTLD_LOCAL); module->native = ::dlopen(path ? resolved.c_str() : nullptr, nativeFlags); diff --git a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp index fc9fd94d8..b2281ac47 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Thread.cpp @@ -5,6 +5,7 @@ #include "prx/libc/include/General.hpp" #include "../include/Pthread.hpp" #include "Common.hpp" +#include "prx/libkernel/KernelErrors.hpp" #include extern "C" { @@ -39,6 +40,18 @@ void APS5_VABI pthread_exit_nid_postfix(void* value) { scePthreadExit(value); } +int APS5_VABI scePthreadGetschedparam(Pthread thread, int* policy, KernelSchedParam* param) { + if (!policy || !param) return SCE_KERNEL_ERROR_EINVAL; + *policy = GUEST_SCHED_FIFO; + return scePthreadGetprio(thread, ¶m->sched_priority); +} + +int APS5_VABI scePthreadSetschedparam(Pthread thread, int policy, const KernelSchedParam* param) { + (void)policy; + if (!param) return SCE_KERNEL_ERROR_EINVAL; + return scePthreadSetprio(thread, param->sched_priority); +} + int APS5_VABI pthread_getschedparam_nid_postfix(Pthread thread, int* policy, KernelSchedParam* param) { if (!policy || !param) return PosixThread::GUEST_EINVAL; *policy = GUEST_SCHED_FIFO; diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 3dc69cbab..23b69cf56 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -42,6 +42,27 @@ static std::atomic threadAtexitCount{nullptr}; static std::atomic threadAtexitReport{nullptr}; static thread_local PthreadPrivate* currentThread = nullptr; static thread_local bool threadFinishing = false; + +static void SetStackFromHost(PthreadPrivate* thread) { +#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); +#else + pthread_attr_t attr; + if (pthread_getattr_np(pthread_self(), &attr) != 0) throw std::runtime_error("Cannot query the host thread stack"); + void* address = nullptr; + std::size_t size = 0; + const int result = pthread_attr_getstack(&attr, &address, &size); + 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; +#endif +} #ifndef _WIN32 static thread_local std::unique_ptr adoptedThread; // pthread_exit force-unwinds through guest frames, whose personality resolves to @@ -96,6 +117,7 @@ static void RunThread(std::unique_ptr args) { const auto entry = args->entry; void* arg = args->arg; PthreadPrivate* self = args->self; + if (!self->stackAddress) SetStackFromHost(self); currentThread = self; args.reset(); finishThread(self, entry(arg)); @@ -327,6 +349,7 @@ Pthread APS5_VABI scePthreadSelf() { adopted->threadId = std::this_thread::get_id(); adopted->_detached = true; adopted->references.store(1, std::memory_order_relaxed); + SetStackFromHost(adopted.get()); currentThread = adopted.release(); } #else @@ -335,6 +358,7 @@ Pthread APS5_VABI scePthreadSelf() { adoptedThread->_detached = true; adoptedThread->_adopted = true; adoptedThread->threadId = std::this_thread::get_id(); + SetStackFromHost(adoptedThread.get()); currentThread = adoptedThread.get(); } #endif diff --git a/core/libs/prx/libkernel/Socket/src/Udp.cpp b/core/libs/prx/libkernel/Socket/src/Udp.cpp index 5462087a4..bffca9598 100644 --- a/core/libs/prx/libkernel/Socket/src/Udp.cpp +++ b/core/libs/prx/libkernel/Socket/src/Udp.cpp @@ -57,6 +57,7 @@ int NativeError() { case EAGAIN: return 35; case EADDRINUSE: return 48; case EADDRNOTAVAIL: return 49; + case EACCES: return 13; case EMSGSIZE: return 40; case ENETUNREACH: return 51; case EHOSTUNREACH: return 65; @@ -65,6 +66,10 @@ int NativeError() { case ECONNABORTED: return 53; case EISCONN: return 56; case ENOTCONN: return 57; + case ENOBUFS: return 55; + case ETIMEDOUT: return 60; + case EINTR: return 4; + case EINVAL: return 22; default: return 5; } #endif diff --git a/core/libs/prx/libkernel/System/src/Process.cpp b/core/libs/prx/libkernel/System/src/Process.cpp index 7755b1543..a3838db51 100644 --- a/core/libs/prx/libkernel/System/src/Process.cpp +++ b/core/libs/prx/libkernel/System/src/Process.cpp @@ -321,4 +321,12 @@ int APS5_VABI sceKernelIsTrinityMode(void) { return 0; } +int APS5_VABI sceKernelGetOperationMode(int* mode, int* submode) { + if (!mode || !submode) + throw std::invalid_argument("sceKernelGetOperationMode: null output"); + *mode = 0; + *submode = 0; + return 0; +} + } diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 96466573a..6c5ee71eb 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -27,4 +27,28 @@ int APS5_VABI pthread_cancel_nid_postfix(void) { NotImplemented_nid_no_patch("0D4-FVvEikw"); return 0; } +int APS5_VABI sceKernelFchmod(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI fchmod_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI futimes_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI recvmsg_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sendmsg_nid_postfix(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/tests/GuestAjm.cpp b/core/libs/tests/GuestAjm.cpp index 71524e885..0e9bb2483 100644 --- a/core/libs/tests/GuestAjm.cpp +++ b/core/libs/tests/GuestAjm.cpp @@ -25,6 +25,7 @@ int APS5_VABI sceAjmBatchJobGetGaplessDecode(AjmBatchInfo*, std::uint32_t, void* int APS5_VABI sceAjmBatchJobGetCodecInfo(AjmBatchInfo*, std::uint32_t, void*, std::size_t); 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); } static void Require(bool value) { if (!value) std::abort(); } @@ -201,6 +202,111 @@ void TestMp3ParseFrame() { Require(sceAjmDecMp3ParseFrame(mpeg25Above64Kbps, 4, 0, &frame) == invalidParameter); } +const std::uint8_t LAME_MPEG1_STEREO_VBR[] = { + 0xFF, 0xFB, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58, 0x69, 0x6E, 0x67, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0A, + 0x00, 0x00, 0x09, 0x28, 0x00, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x53, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, 0x6E, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x8D, 0x9F, 0x9F, 0x9F, 0x9F, + 0x9F, 0x9F, 0x9F, 0x9F, 0x9F, 0x9F, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xAC, 0xBE, 0xBE, 0xBE, 0xBE, 0xBE, 0xBE, 0xBE, 0xBE, + 0xBE, 0xBE, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x0A, 0x4C, 0x41, 0x4D, 0x45, 0x34, 0x2E, 0x30, 0x20, 0x00, 0x04, 0x48, 0x00, + 0x00, 0x00, 0x00, 0x2E, 0x1A, 0x00, 0x00, 0x15, 0x20, 0x24, 0x03, 0xB0, 0x45, 0x00, 0x01, 0x9A, 0x00, 0x00, 0x09, 0x28, 0x06, 0x5C, 0x53, 0xC5, +}; + +const std::uint8_t LAME_MPEG1_MONO_CBR[] = { + 0xFF, 0xFB, 0x98, 0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6E, 0x66, + 0x6F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x18, 0xC0, 0x00, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x19, 0x33, + 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, + 0x99, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xE6, 0xE6, 0xE6, + 0xE6, 0xE6, 0xE6, 0xE6, 0xE6, 0xE6, 0xE6, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x38, 0x4C, 0x41, 0x4D, + 0x45, 0x34, 0x2E, 0x30, 0x20, 0x00, 0x01, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x2E, 0x15, 0x00, 0x00, 0x14, 0x80, 0x24, 0x03, 0xB0, 0x22, 0x00, 0x00, + 0x80, 0x00, 0x00, 0x18, 0xC0, 0x9A, 0x66, 0xF0, 0x95, +}; + +const std::uint8_t LAME_MPEG2_MONO_CBR[] = { + 0xFF, 0xF3, 0x80, 0xC4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x49, 0x6E, 0x66, 0x6F, 0x00, 0x00, 0x00, 0x0F, 0x00, 0x00, 0x00, + 0x14, 0x00, 0x00, 0x11, 0x23, 0x00, 0x0C, 0x0C, 0x0C, 0x0C, 0x19, 0x19, 0x19, 0x19, 0x19, 0x26, 0x26, 0x26, 0x26, 0x26, 0x33, 0x33, 0x33, 0x33, + 0x33, 0x40, 0x40, 0x40, 0x40, 0x40, 0x4C, 0x4C, 0x4C, 0x4C, 0x4C, 0x59, 0x59, 0x59, 0x59, 0x59, 0x66, 0x66, 0x66, 0x66, 0x66, 0x73, 0x73, 0x73, + 0x73, 0x73, 0x80, 0x80, 0x80, 0x80, 0x80, 0x8C, 0x8C, 0x8C, 0x8C, 0x8C, 0x99, 0x99, 0x99, 0x99, 0x99, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xB3, 0xB3, + 0xB3, 0xB3, 0xB3, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xD9, 0xD9, 0xD9, 0xD9, 0xD9, 0xE6, 0xE6, 0xE6, 0xE6, 0xE6, 0xF3, + 0xF3, 0xF3, 0xF3, 0xF3, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x38, 0x4C, 0x41, 0x4D, 0x45, 0x34, 0x2E, 0x30, 0x20, 0x00, 0x01, 0x6E, + 0x00, 0x00, 0x00, 0x00, 0x2E, 0x1E, 0x00, 0x00, 0x14, 0x40, 0x24, 0x03, 0xB0, 0x22, 0x00, 0x00, 0x40, 0x00, 0x00, 0x11, 0x23, 0xF6, 0xE8, 0xE2, + 0xEB, +}; + +const std::uint8_t FGH_FRAME[] = { + 0xFF, 0xFB, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x04, 0x51, 0x00, 0x06, 0xBA, 0xA8, 0x00, 0x00, 0xA0, +}; + +const std::uint8_t VBRI_FRAME[] = { + 0xFF, 0xFB, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56, 0x42, 0x52, 0x49, 0x00, 0x01, 0x02, 0x40, 0x00, 0x4B, 0x00, 0x00, + 0x10, 0x4A, 0x00, 0x00, 0x00, 0x0A, +}; + +const std::uint8_t FGH_BAD_CRC_FRAME[] = { + 0xFF, 0xFB, 0x90, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x04, 0x51, 0x00, 0x06, 0xBA, 0xA8, 0x00, 0x00, 0xA1, +}; + +std::vector Mp3Frame(const std::uint8_t* prefix, std::size_t prefixSize, std::size_t frameSize) { + std::vector frame(frameSize, 0); + std::memcpy(frame.data(), prefix, prefixSize); + return frame; +} + +bool OflParsesTo(const std::vector& stream, std::uint32_t streamSize, std::uint32_t frames, std::uint32_t delay, std::uint32_t total, std::uint32_t type) { + AjmDecMp3ParseFrame frame; + std::memset(&frame, 0xFF, sizeof(frame)); + if (sceAjmDecMp3ParseFrame(stream.data(), streamSize, 1, &frame) != 0) return false; + return frame.num_frames == frames && frame.encoder_delay == delay && frame.total_samples == total && frame.ofl_type == type; +} + +bool OflThrows(const std::vector& stream) { + AjmDecMp3ParseFrame frame{}; + try { + sceAjmDecMp3ParseFrame(stream.data(), static_cast(stream.size()), 1, &frame); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + +void TestMp3ParseOfl() { + const auto stereo = Mp3Frame(LAME_MPEG1_STEREO_VBR, sizeof(LAME_MPEG1_STEREO_VBR), 417); + Require(OflParsesTo(stereo, 417, 10, 1152 + 576 + 529, 10000, 1)); + Require(ParsesTo(stereo.data(), 417, 417, 2, 1152, 128000, 44100)); + Require(OflParsesTo(stereo, 48, 10, 0, 0, 0)); + Require(OflParsesTo(stereo, 46, 0, 0, 0, 0)); + const auto mono = Mp3Frame(LAME_MPEG1_MONO_CBR, sizeof(LAME_MPEG1_MONO_CBR), 576); + Require(OflParsesTo(mono, 576, 10, 1152 + 576 + 529, 10000, 1)); + const auto mpeg2 = Mp3Frame(LAME_MPEG2_MONO_CBR, sizeof(LAME_MPEG2_MONO_CBR), 208); + Require(OflParsesTo(mpeg2, 208, 20, 576 + 576 + 529, 10000, 1)); + + const auto fgh = Mp3Frame(FGH_FRAME, sizeof(FGH_FRAME), 417); + Require(OflParsesTo(fgh, 417, 0, 1105, 441000, 3)); + Require(OflParsesTo(Mp3Frame(FGH_BAD_CRC_FRAME, sizeof(FGH_BAD_CRC_FRAME), 417), 417, 0, 0, 0, 0)); + auto vbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); + Require(OflParsesTo(vbri, 417, 0, 576, 0, 2)); + vbri.insert(vbri.end(), fgh.begin(), fgh.end()); + Require(OflParsesTo(vbri, 834, 0, 576 + 1105, 441000, 4)); + Require(OflParsesTo(vbri, 480, 0, 576, 0, 2)); + + auto protectedFrame = stereo; + protectedFrame[1] = 0xFA; + Require(OflThrows(protectedFrame)); + auto layer2 = stereo; + layer2[1] = 0xFD; + Require(OflThrows(layer2)); + auto monoVbri = Mp3Frame(VBRI_FRAME, sizeof(VBRI_FRAME), 417); + monoVbri[3] = 0xC4; + Require(OflThrows(monoVbri)); +} + struct GaplessDecode { std::uint32_t totalSamples; std::uint16_t skipSamples; @@ -339,6 +445,25 @@ void TestBatchWaitRelease(std::uint32_t context) { Require(sceAjmBatchWait(context, second + 1, 0, &error) == invalidBatch); } +void TestBatchCancel(std::uint32_t context) { + constexpr int invalidContext = static_cast(0x80930002); + constexpr int invalidBatch = static_cast(0x80930004); + std::vector batch(64); + AjmBatchInfo info{}; + AjmBatchError error{}; + std::uint32_t id = 0; + Require(sceAjmBatchInitialize(batch.data(), batch.size(), &info) == 0); + Require(sceAjmBatchStart(context, &info, 0, &error, &id) == 0); + Require(sceAjmBatchCancel(context, id) == 0); + Require(sceAjmBatchCancel(context, id) == 0); + Require(sceAjmBatchCancel(0, id) == invalidContext); + Require(sceAjmBatchCancel(context + 1, id) == invalidContext); + Require(sceAjmBatchWait(context, id, 0, &error) == 0); + Require(sceAjmBatchCancel(context, id) == invalidBatch); + Require(sceAjmBatchCancel(context, 0) == invalidBatch); + Require(sceAjmBatchCancel(context, id + 1) == invalidBatch); +} + } int main() { @@ -356,11 +481,13 @@ int main() { Require(sceAjmDecAt9ParseConfigData(badHeader, &info) == invalidParameter); Require(sceAjmDecAt9ParseConfigData(nullptr, &info) == invalidParameter); TestMp3ParseFrame(); + TestMp3ParseOfl(); TestMp3(context); TestOpus(context); TestDecodeSingle(context); TestGaplessDecode(context); TestCodecInfo(context); TestBatchWaitRelease(context); + TestBatchCancel(context); Require(sceAjmFinalize(context) == 0); } diff --git a/core/libs/tests/GuestAmpr.cpp b/core/libs/tests/GuestAmpr.cpp index 74dad2ece..23106e798 100644 --- a/core/libs/tests/GuestAmpr.cpp +++ b/core/libs/tests/GuestAmpr.cpp @@ -4,9 +4,12 @@ #include #include #include +#include #include +#include #include #include +#include extern "C" { int APS5_VABI sceAmprCommandBufferConstructor(Apr::CommandBufferObject*); @@ -33,6 +36,35 @@ int APS5_VABI sceAmprCommandBufferWriteAddressFromTimeCounterOnCompletion(Apr::C int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterOnCompletion(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint8_t); int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPairOnCompletion(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint8_t); int APS5_VABI sceAmprCommandBufferWriteKernelEventQueueOnCompletion(Apr::CommandBufferObject*, std::uint64_t, std::int32_t, std::uint64_t); +int APS5_VABI sceAmprCommandBufferNop(Apr::CommandBufferObject*, std::uint32_t); +int APS5_VABI sceAmprCommandBufferNopWithData(Apr::CommandBufferObject*, std::uint32_t, const std::uint32_t*); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNop(std::uint32_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeNopWithData(std::uint32_t); +int APS5_VABI sceAmprCommandBufferWaitOnAddress_04_00(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint64_t, std::uint8_t, std::uint8_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnAddress_04_00(volatile std::uint64_t*, std::uint64_t, std::uint8_t, std::uint8_t); +int APS5_VABI sceAmprCommandBufferWriteAddressFromTimeCounter_04_00(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint64_t); +int APS5_VABI sceAmprCommandBufferWriteAddressFromCounter_04_00(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint8_t, std::uint64_t); +int APS5_VABI sceAmprCommandBufferWriteAddressFromCounterPair_04_00(Apr::CommandBufferObject*, volatile std::uint64_t*, std::uint8_t, std::uint64_t); +int APS5_VABI sceAmprCommandBufferWriteKernelEventQueue_04_00(Apr::CommandBufferObject*, std::uint64_t, std::int32_t, std::uint64_t, std::uint64_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00(volatile std::uint64_t*); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(volatile std::uint64_t*, std::uint8_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(volatile std::uint64_t*, std::uint8_t); +int APS5_VABI sceAmprCommandBufferWaitOnCounter_04_00(Apr::CommandBufferObject*, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t); +int APS5_VABI sceAmprCommandBufferWriteCounter_04_00(Apr::CommandBufferObject*, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t, std::uint8_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWaitOnCounter_04_00(std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeWriteCounter_04_00(std::uint8_t, std::uint8_t, std::uint64_t, std::uint8_t); +int APS5_VABI sceAmprCommandBufferConstructNop(Apr::CommandBufferObject*, std::int16_t, const void*, std::uint32_t, const std::uint32_t*); +int APS5_VABI sceAmprCommandBufferConstructMarker(Apr::CommandBufferObject*, std::uint32_t, const char*, const std::uint32_t*); +int APS5_VABI sceKernelAprResolveFilepathsToIds(const char**, std::uint32_t, std::uint32_t*, std::uint32_t*); +int APS5_VABI sceAmprAprCommandBufferReadFile(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, std::uint32_t, void*, std::uint64_t, std::uint64_t); +int APS5_VABI sceAmprAprCommandBufferReadFileGather(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, std::uint64_t, std::uint64_t); +int APS5_VABI sceAmprAprCommandBufferReadFileScatter(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, void*, std::uint64_t); +int APS5_VABI sceAmprAprCommandBufferReadFileGatherScatter(Apr::CommandBufferObject*, std::uint64_t*, std::uint64_t*, void*, std::uint64_t, std::uint64_t); +int APS5_VABI sceAmprAprCommandBufferResetGatherScatterState(Apr::CommandBufferObject*); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGather(std::uint64_t, std::uint64_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileScatter(void*, std::uint64_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeReadFileGatherScatter(void*, std::uint64_t, std::uint64_t); +std::uint64_t APS5_VABI sceAmprMeasureCommandSizeResetGatherScatterState(); } static void Require(bool value) { if (!value) std::abort(); } @@ -212,6 +244,258 @@ void TestRejectedWaitsAndCounters() { Require(recorder.Offset() == 0 && recorder.Commands() == 0); } +void TestNops() { + Recorder recorder; + const std::uint32_t data[3] = {0x11111111u, 0x22222222u, 0x33333333u}; + for (std::uint32_t dwords = 1; dwords <= 16; ++dwords) { + const auto offset = recorder.Offset(); + const auto commands = recorder.Commands(); + Require(sceAmprCommandBufferNop(&recorder.buffer, dwords) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNop(dwords)); + } + const auto offset = recorder.Offset(); + const auto commands = recorder.Commands(); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 3, data) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); + Require(std::memcmp(recorder.memory.data() + offset + sizeof(Apr::CommandHeader), data, sizeof(data)) == 0); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 0, nullptr) == 0); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + + const auto rejected = static_cast(static_cast(invalidArgument)); + Require(sceAmprCommandBufferNop(&recorder.buffer, 0) == invalidArgument); + Require(sceAmprCommandBufferNop(&recorder.buffer, 17) == invalidArgument); + Require(sceAmprCommandBufferNopWithData(&recorder.buffer, 16, data) == invalidArgument); + Require(sceAmprMeasureCommandSizeNop(0) == rejected && sceAmprMeasureCommandSizeNop(17) == rejected); + Require(sceAmprMeasureCommandSizeNopWithData(0) == rejected && sceAmprMeasureCommandSizeNopWithData(17) == rejected); +} + +void TestVersionedCommands() { + Recorder recorder; + alignas(8) std::uint64_t value = 0x8000000000000005ull; + alignas(8) std::uint64_t single = 0; + alignas(8) std::uint64_t pair = 0; + alignas(8) std::uint64_t time = 0; + Require(sceAmprCommandBufferWaitOnAddress_04_00(&recorder.buffer, &value, 0x8000000000000003ull, 4, 0) == 0); + Require(sceAmprCommandBufferWaitOnAddress_04_00(&recorder.buffer, &value, 1, 6, 1) == 0); + Require(sceAmprCommandBufferWaitOnAddress_04_00(&recorder.buffer, &value, 0x8000000000000000ull, 5, 0) == 0); + Require(sceAmprCommandBufferWriteCounterOnCompletion(&recorder.buffer, 10, 3) == 0); + Require(sceAmprCommandBufferWriteCounterOnCompletion(&recorder.buffer, 11, 4) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounter_04_00(&recorder.buffer, &single, 10, 1) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounterPair_04_00(&recorder.buffer, &pair, 10, 0) == 0); + Require(sceAmprCommandBufferWriteAddressFromTimeCounter_04_00(&recorder.buffer, &time, 1) == 0); + auto submitted = std::async(std::launch::async, [&]() { return sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0); }); + Require(submitted.wait_for(std::chrono::seconds(10)) == std::future_status::ready); + Require(submitted.get() == 0 && single == 3 && pair == (3ull | (4ull << 32u)) && time != 0); + + const auto rejected = static_cast(static_cast(invalidArgument)); + Recorder empty; + Require(sceAmprCommandBufferWaitOnAddress_04_00(&empty.buffer, nullptr, 0, 0, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnAddress_04_00(&empty.buffer, &value, 0, 7, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnAddress_04_00(&empty.buffer, &value, 0, 0, 2) == invalidArgument); + Require(sceAmprCommandBufferWriteAddressFromCounterPair_04_00(&empty.buffer, &pair, 11, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteAddressFromTimeCounter_04_00(&empty.buffer, nullptr, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteKernelEventQueue_04_00(&empty.buffer, 0, 1, 0, 0) == invalidArgument); + Require(empty.Offset() == 0 && empty.Commands() == 0); + Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(nullptr, 0, 0, 0) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(&value, 0, 7, 0) == rejected); + Require(sceAmprMeasureCommandSizeWriteAddressFromTimeCounter_04_00(nullptr) == rejected); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(&single, 128) == sizeof(Apr::WriteAddressFromCounterCommand)); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounter_04_00(nullptr, 0) == rejected); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 128) == sizeof(Apr::WriteAddressFromCounterCommand)); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 11) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnAddress_04_00(&value, 0, 6, 1) == sizeof(Apr::WaitCommand)); + Require(sceAmprMeasureCommandSizeWriteAddressFromCounterPair_04_00(&pair, 10) == sizeof(Apr::WriteAddressFromCounterCommand)); +} + +void SubmitWithin10Seconds(const Recorder& recorder) { + auto submitted = std::async(std::launch::async, [&]() { return sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0); }); + Require(submitted.wait_for(std::chrono::seconds(10)) == std::future_status::ready); + Require(submitted.get() == 0); +} + +void TestVersionedCounters() { + enum : std::uint8_t { size8, size4, size2Offset0, size2Offset1, size1Offset0, size1Offset1, size1Offset2, size1Offset3 }; + enum : std::uint8_t { store, atomicOr, atomicAndComplement, atomicXor, atomicAdd }; + Recorder recorder; + alignas(8) std::uint64_t fields = 0; + alignas(8) std::uint64_t wide = 0; + alignas(8) std::uint64_t bits = 0; + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 20, size4, 0x11223344u, store, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 20, size1Offset2, 0x1AAu, store, 1) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 20, size2Offset0, 0xFFFFu, atomicAdd, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 22, size8, 0x0000000500000001ull, store, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 22, size8, 0xFFFFFFFFu, atomicAdd, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 24, size4, 0xF0u, store, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 24, size4, 0x0Fu, atomicOr, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 24, size4, 0x3Cu, atomicAndComplement, 0) == 0); + Require(sceAmprCommandBufferWriteCounter_04_00(&recorder.buffer, 24, size4, 0xFFu, atomicXor, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 20, size1Offset3, 0x11u, 0, 0, 0, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 20, size1Offset2, 1u, 6, 0, 0, 1) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 20, size2Offset0, 0xF000u, 4, 0, 0, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 20, size2Offset1, 0x11ABu, 2, 0, 0, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 22, size8, 0x0000000600000000ull, 0, 0, 0, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 24, size4, 0xFCu, 0, 1, 0x0Fu, 0) == 0); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&recorder.buffer, 24, size1Offset0, 0x3Cu, 0, 0, 0x0Fu, 0) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounterOnCompletion(&recorder.buffer, &fields, 20) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounterPairOnCompletion(&recorder.buffer, &wide, 22) == 0); + Require(sceAmprCommandBufferWriteAddressFromCounterOnCompletion(&recorder.buffer, &bits, 24) == 0); + Require(recorder.Commands() == 19); + SubmitWithin10Seconds(recorder); + Require(fields == 0x11AA3343u && wide == 0x0000000600000000ull && bits == 0x3Cu); + + const auto rejected = static_cast(static_cast(invalidArgument)); + Recorder empty; + Require(sceAmprCommandBufferWaitOnCounter_04_00(&empty.buffer, 0, 8, 0, 0, 0, 0, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&empty.buffer, 0, size4, 0, 7, 0, 0, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&empty.buffer, 0, size4, 0, 0, 2, 0, 0) == invalidArgument); + Require(sceAmprCommandBufferWaitOnCounter_04_00(&empty.buffer, 0, size4, 0, 0, 0, 0, 2) == invalidArgument); + Require(sceAmprCommandBufferWriteCounter_04_00(&empty.buffer, 128, size4, 0, store, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteCounter_04_00(&empty.buffer, 0, 8, 0, store, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteCounter_04_00(&empty.buffer, 0, size4, 0, 5, 0) == invalidArgument); + Require(sceAmprCommandBufferWriteCounter_04_00(nullptr, 0, size4, 0, store, 0) == invalidArgument); + Require(empty.Offset() == 0 && empty.Commands() == 0); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(200, size1Offset3, 0, 6, 1, 0, 1) == sizeof(Apr::WaitCommand)); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(0, 8, 0, 0, 0, 0, 0) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(0, size4, 0, 7, 0, 0, 0) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(0, size4, 0, 0, 2, 0, 0) == rejected); + Require(sceAmprMeasureCommandSizeWaitOnCounter_04_00(0, size4, 0, 0, 0, 0, 2) == rejected); + Require(sceAmprMeasureCommandSizeWriteCounter_04_00(127, size8, 0, atomicAdd) == sizeof(Apr::WriteCounterCommand)); + Require(sceAmprMeasureCommandSizeWriteCounter_04_00(128, size4, 0, store) == rejected); + Require(sceAmprMeasureCommandSizeWriteCounter_04_00(0, 8, 0, store) == rejected); + Require(sceAmprMeasureCommandSizeWriteCounter_04_00(0, size4, 0, 5) == rejected); +} + +void TestConstructed() { + Recorder recorder; + const std::uint8_t payload[5] = {1, 2, 3, 4, 5}; + const std::uint32_t word = 0xCAFEF00Du; + auto offset = recorder.Offset(); + auto commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 7, payload, sizeof(payload), &word) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(4)); + const std::uint8_t* data = recorder.memory.data() + offset + sizeof(Apr::CommandHeader); + const std::uint8_t padding[3] = {}; + Require(std::memcmp(data, &word, sizeof(word)) == 0 && std::memcmp(data + 4, payload, sizeof(payload)) == 0 && std::memcmp(data + 9, padding, sizeof(padding)) == 0); + + const std::array large{}; + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 0, large.data(), 60, nullptr) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(16)); + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructNop(&recorder.buffer, 0, nullptr, 0, nullptr) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::Nop, sceAmprMeasureCommandSizeNopWithData(1)); + + const std::pair markers[] = {{1, Apr::Opcode::SetMarker}, {2, Apr::Opcode::PushMarker}, {5, Apr::Opcode::SetMarker}, {6, Apr::Opcode::PushMarker}}; + for (const auto& [type, opcode] : markers) { + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructMarker(&recorder.buffer, type, "stream", &color) == 0); + RequireRecorded(recorder, offset, commands, opcode, sceAmprMeasureCommandSizeSetMarker("stream"), "stream"); + } + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprCommandBufferConstructMarker(&recorder.buffer, 3, nullptr, nullptr) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::PopMarker, sceAmprMeasureCommandSizePopMarker()); + Require(sceKernelAprSubmitCommandBuffer(&recorder.buffer, 0) == 0); + + Recorder empty; + Require(sceAmprCommandBufferConstructNop(&empty.buffer, 0, large.data(), 61, nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructNop(&empty.buffer, 0, large.data(), 57, &word) == invalidArgument); + Require(sceAmprCommandBufferConstructNop(nullptr, 0, large.data(), 4, nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 5, "stream", nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 6, "stream", nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 1, nullptr, nullptr) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 0, "stream", &color) == invalidArgument); + Require(sceAmprCommandBufferConstructMarker(&empty.buffer, 4, "stream", &color) == invalidArgument); + Require(empty.Offset() == 0 && empty.Commands() == 0); +} + +std::uint8_t FileByte(std::size_t offset) { + return static_cast(offset * 7u + 3u); +} + +bool MatchesFile(const std::uint8_t* data, std::size_t offset, std::size_t bytes) { + for (std::size_t index = 0; index < bytes; ++index) { + if (data[index] != FileByte(offset + index)) return false; + } + return true; +} + +void TestGatherScatter() { + const char* path = "ampr_gather_scatter.bin"; + { + std::ofstream file(path, std::ios::binary); + for (std::size_t offset = 0; offset < 4096; ++offset) file.put(static_cast(FileByte(offset))); + } + std::uint32_t fileId = 0; + std::uint32_t failed = 0; + Require(sceKernelAprResolveFilepathsToIds(&path, 1, &fileId, &failed) == 0); + + Recorder recorder; + auto* buffer = &recorder.buffer; + auto* map = &recorder.gatherState; + auto* state = &recorder.scatterState; + std::array first{}; + std::array second{}; + Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 8, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data(), 8) == invalidArgument); + Require(sceAmprAprCommandBufferReadFileGatherScatter(buffer, map, state, second.data(), 8, 0) == invalidArgument); + Require(recorder.Commands() == 0); + + Require(sceAmprAprCommandBufferReadFile(buffer, map, state, fileId, first.data(), 16, 100) == 0); + auto offset = recorder.Offset(); + auto commands = recorder.Commands(); + Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 8, 500) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::ReadFileGather, sceAmprMeasureCommandSizeReadFileGather(8, 500)); + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data(), 8) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::ReadFileScatter, sceAmprMeasureCommandSizeReadFileScatter(second.data(), 8)); + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprAprCommandBufferReadFileGatherScatter(buffer, map, state, second.data() + 32, 4, 1000) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::ReadFileGatherScatter, sceAmprMeasureCommandSizeReadFileGatherScatter(second.data() + 32, 4, 1000)); + Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 4, 2000) == 0); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, first.data() + 40, 6) == 0); + offset = recorder.Offset(); + commands = recorder.Commands(); + Require(sceAmprAprCommandBufferResetGatherScatterState(buffer) == 0); + RequireAppended(recorder, offset, commands, Apr::Opcode::ResetGatherScatterState, sceAmprMeasureCommandSizeResetGatherScatterState()); + Require(sceAmprAprCommandBufferReadFileGather(buffer, map, state, 8, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data(), 8) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(buffer, map, state, fileId, second.data() + 48, 4, 3000) == 0); + Require(sceAmprAprCommandBufferReadFileScatter(buffer, map, state, second.data() + 56, 4) == 0); + Require(recorder.Commands() == 9); + SubmitWithin10Seconds(recorder); + + Require(MatchesFile(first.data(), 100, 16) && MatchesFile(first.data() + 16, 500, 8)); + Require(MatchesFile(second.data(), 508, 8)); + Require(MatchesFile(second.data() + 32, 1000, 4) && MatchesFile(second.data() + 36, 2000, 4)); + Require(MatchesFile(first.data() + 40, 2004, 6)); + Require(MatchesFile(second.data() + 48, 3000, 4) && MatchesFile(second.data() + 56, 3004, 4)); + Require(first[24] == 0 && first[39] == 0 && second[8] == 0 && second[31] == 0 && second[40] == 0); + std::remove(path); + + const auto rejected = static_cast(static_cast(invalidArgument)); + auto* high = reinterpret_cast(std::uintptr_t{0xF00000000000ull}); + Recorder empty; + Require(sceAmprAprCommandBufferReadFile(&empty.buffer, map, state, fileId, first.data(), 0, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(&empty.buffer, map, state, fileId, first.data(), 0x100000001ull, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(&empty.buffer, map, state, fileId, first.data(), 4, 0x10000000000ull) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(&empty.buffer, map, state, fileId, high, 4, 0) == invalidArgument); + Require(sceAmprAprCommandBufferReadFile(nullptr, map, state, fileId, first.data(), 4, 0) == invalidArgument); + Require(empty.Offset() == 0 && empty.Commands() == 0); + Require(sceAmprMeasureCommandSizeReadFileGather(0, 0) == rejected); + Require(sceAmprMeasureCommandSizeReadFileGather(0x100000000ull, 0x10000000000ull - 1u) == sizeof(Apr::ReadFileCommand)); + Require(sceAmprMeasureCommandSizeReadFileGather(4, 0x10000000000ull) == rejected); + Require(sceAmprMeasureCommandSizeReadFileScatter(first.data(), 0x100000001ull) == rejected); + Require(sceAmprMeasureCommandSizeReadFileScatter(high, 4) == rejected); + Require(sceAmprMeasureCommandSizeReadFileGatherScatter(first.data(), 4, 0x10000000000ull) == rejected); + Require(sceAmprMeasureCommandSizeReadFileGatherScatter(nullptr, 4, 0) == sizeof(Apr::ReadFileCommand)); +} + } int main() { @@ -226,5 +510,10 @@ int main() { TestWaits(); TestCounters(); TestRejectedWaitsAndCounters(); + TestNops(); + TestVersionedCommands(); + TestVersionedCounters(); + TestConstructed(); + TestGatherScatter(); return 0; } diff --git a/core/libs/tests/GuestAudio3dPort.cpp b/core/libs/tests/GuestAudio3dPort.cpp new file mode 100644 index 000000000..c0e58ad57 --- /dev/null +++ b/core/libs/tests/GuestAudio3dPort.cpp @@ -0,0 +1,187 @@ +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include + +extern "C" { +void APS5_VABI sceAudio3dGetDefaultOpenParameters(Audio3dOpenParameters* parameters); +int APS5_VABI sceAudio3dInitialize(std::int64_t reserved); +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); +} + +namespace { + +constexpr int INVALID_PORT = static_cast(0x80EA0002); +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; + +void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +template +void RequireThrows(F f, const char* message) { + bool threw = false; + try { + f(); + } catch (const std::exception&) { + threw = true; + } + Require(threw, message); +} + +Audio3dOpenParameters Defaults() { + Audio3dOpenParameters parameters{}; + sceAudio3dGetDefaultOpenParameters(¶meters); + return parameters; +} + +int Open(const Audio3dOpenParameters& parameters, std::uint32_t* id) { + return sceAudio3dPortOpen(SYSTEM_USER, ¶meters, id); +} + +void RequireLevel(std::uint32_t level, std::uint32_t available, const char* message) { + std::uint32_t got_level = 99; + std::uint32_t got_available = 99; + Require(sceAudio3dPortGetQueueLevel(0, &got_level, &got_available) == 0, message); + Require(got_level == level && got_available == available, message); +} + +void CheckBeforeInitialize() { + std::uint32_t id = 7; + Audio3dOpenParameters parameters = Defaults(); + 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"); + Require(sceAudio3dPortOpen(1, nullptr, nullptr) == NOT_READY, "NOT_READY is checked before the arguments"); + std::uint32_t level = 0; + Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == INVALID_PORT, "queue level needs an open port"); + Require(sceAudio3dPortClose(0) == INVALID_PORT, "close needs an open port"); + Require(sceAudio3dPortAdvance(0) == INVALID_PORT, "advance needs an open port"); + Require(sceAudio3dPortPush(0, 0) == INVALID_PORT, "push needs an open port"); + float value = 0.0f; + Require(sceAudio3dPortSetAttribute(0, 0x10001, &value, sizeof(value)) == INVALID_PORT, "set attribute needs an open port"); + Require(sceAudio3dInitialize(0) == 0, "initialize"); + RequireThrows([] { sceAudio3dInitialize(0); }, "repeated initialize must throw"); +} + +void CheckOpenParameters() { + std::uint32_t id = 7; + const Audio3dOpenParameters defaults = Defaults(); + Require(sceAudio3dPortOpen(1, &defaults, &id) == INVALID_PARAMETER, "user id must be the system user"); + Require(sceAudio3dPortOpen(SYSTEM_USER, nullptr, &id) == INVALID_PARAMETER, "null parameters"); + Require(sceAudio3dPortOpen(SYSTEM_USER, &defaults, nullptr) == INVALID_PARAMETER, "null port id"); + auto rejects = [&](auto change, const char* message) { + Audio3dOpenParameters parameters = defaults; + change(parameters); + Require(Open(parameters, &id) == INVALID_PARAMETER, message); + Require(id == 7, "a failed open must not write the port id"); + }; + rejects([](auto& p) { p.size_this = 0x30; }, "unknown parameter size"); + rejects([](auto& p) { p.size_this = 0x21; }, "parameter size is not masked"); + rejects([](auto& p) { p.rate = 1; }, "only 48 kHz"); + rejects([](auto& p) { p.granularity = 0x80; }, "granularity below 256"); + rejects([](auto& p) { p.granularity = 0x180; }, "granularity not a multiple of 256"); + rejects([](auto& p) { p.max_objects = 0; }, "zero objects"); + rejects([](auto& p) { p.queue_depth = 0; }, "zero queue depth"); + rejects([](auto& p) { p.buffer_mode = 3; }, "buffer mode above 2"); + rejects([](auto& p) { p.size_this = 0x28; p.num_beds = 4; }, "4 beds"); + rejects([](auto& p) { p.size_this = 0x28; p.num_beds = 1; }, "1 bed"); + auto throws = [&](auto change, const char* message) { + Audio3dOpenParameters parameters = defaults; + change(parameters); + RequireThrows([&] { Open(parameters, &id); }, message); + }; + throws([](auto& p) { p.size_this = 0x10; }, "0x10 parameters select buffer mode 0"); + throws([](auto& p) { p.size_this = 0x18; }, "0x18 parameters select buffer mode 1"); + throws([](auto& p) { p.buffer_mode = 1; }, "buffer mode 1"); + throws([](auto& p) { p.size_this = 0x28; p.num_beds = 3; }, "3 beds"); +} + +void CheckOpenClose() { + std::uint32_t id = 7; + Audio3dOpenParameters parameters = Defaults(); + parameters.size_this = 0x28; + parameters.num_beds = 2; + Require(Open(parameters, &id) == 0 && id == 0, "open hands out port 0"); + std::uint32_t second = 7; + Require(Open(parameters, &second) == OUT_OF_RESOURCES && second == 7, "only one port"); + std::uint32_t level = 0; + Require(sceAudio3dPortGetQueueLevel(1, &level, nullptr) == INVALID_PORT, "port 1 does not exist"); + Require(sceAudio3dPortClose(1) == INVALID_PORT, "close of port 1"); + Require(sceAudio3dPortClose(0) == 0, "close"); + Require(sceAudio3dPortClose(0) == INVALID_PORT, "double close"); + Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == INVALID_PORT, "closed port"); +} + +void CheckAttributes() { + float value = 0.5f; + int flag = 1; + Require(sceAudio3dPortSetAttribute(0, 0x10001, nullptr, 4) == INVALID_PARAMETER, "null attribute"); + Require(sceAudio3dPortSetAttribute(1, 0x10001, &value, 4) == INVALID_PORT, "attribute on port 1"); + Require(sceAudio3dPortSetAttribute(0, 0x10001, &value, 4) == 0, "late reverb level"); + Require(sceAudio3dPortSetAttribute(0, 0x10002, &value, 4) == 0, "downmix spread radius"); + Require(sceAudio3dPortSetAttribute(0, 0x10003, &flag, 4) == 0, "downmix spread height aware"); + RequireThrows([&] { sceAudio3dPortSetAttribute(0, 0x10004, &value, 4); }, "unknown attribute must throw"); + RequireThrows([&] { sceAudio3dPortSetAttribute(0, 0x10001, &value, 8); }, "wrong attribute size must throw"); +} + +void CheckQueue() { + using namespace std::chrono; + constexpr std::uint32_t granularity = 0x1800; + constexpr auto frame = microseconds(1000000ull * granularity / 48000); + Audio3dOpenParameters parameters = Defaults(); + parameters.granularity = granularity; + std::uint32_t id = 7; + Require(Open(parameters, &id) == 0 && id == 0, "open"); + CheckAttributes(); + Require(sceAudio3dPortGetQueueLevel(0, nullptr, nullptr) == INVALID_PARAMETER, "both outputs null"); + std::uint32_t available = 0; + Require(sceAudio3dPortGetQueueLevel(0, nullptr, &available) == 0 && available == 2, "level pointer is optional"); + RequireLevel(0, 2, "empty queue"); + Require(sceAudio3dPortPush(0, 1) == 0, "push of an empty queue"); + RequireLevel(0, 2, "empty push queues nothing"); + Require(sceAudio3dPortAdvance(0) == 0, "advance 1"); + RequireLevel(1, 1, "one frame queued"); + Require(sceAudio3dPortAdvance(0) == 0, "advance 2"); + RequireLevel(2, 0, "queue full"); + RequireThrows([] { sceAudio3dPortAdvance(0); }, "advance into a full queue must throw"); + RequireThrows([] { sceAudio3dPortPush(0, 2); }, "unknown blocking mode must throw"); + + auto start = steady_clock::now(); + Require(sceAudio3dPortPush(0, 1) == 0, "blocking push"); + auto waited = steady_clock::now() - start; + Require(waited >= frame - milliseconds(2), "blocking push waits until one frame has played"); + RequireLevel(1, 1, "blocking push returns with one free slot"); + + Require(sceAudio3dPortAdvance(0) == 0, "advance 3"); + start = steady_clock::now(); + Require(sceAudio3dPortPush(0, 0) == 0, "async push"); + Require(steady_clock::now() - start < frame, "async push does not wait for a frame"); + std::uint32_t level = 0; + Require(sceAudio3dPortGetQueueLevel(0, &level, nullptr) == 0 && level >= 1, "async push keeps frames queued"); + std::this_thread::sleep_for(frame * 3); + RequireLevel(0, 2, "frames drain at the port's rate"); + Require(sceAudio3dPortClose(0) == 0, "close"); +} + +} + +int main() { + CheckBeforeInitialize(); + CheckOpenParameters(); + CheckOpenClose(); + CheckQueue(); +} diff --git a/core/libs/tests/GuestDynamicLoader.cpp b/core/libs/tests/GuestDynamicLoader.cpp index 2fa27e917..04b369b73 100644 --- a/core/libs/tests/GuestDynamicLoader.cpp +++ b/core/libs/tests/GuestDynamicLoader.cpp @@ -1,5 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include +#include +#include #include extern "C" { void* APS5_VABI dlopen_nid_postfix(const char*, int); @@ -37,4 +39,20 @@ int main(int argc, char** argv) { Require(add && add(2, 3) == 5); Require(dlclose_nid_postfix(second) == 0); Require(dlclose_nid_postfix(second) == -1); + + const std::filesystem::path guest = "anyps5-relinked-module-for-test.prx"; + auto relinked = guest; + relinked += ".guest.prx"; + { + std::ofstream elf(guest, std::ios::binary); + elf.write("\x7f" "ELF", 4); + } + std::filesystem::copy_file(argv[1], relinked, std::filesystem::copy_options::overwrite_existing); + void* redirected = dlopen_nid_postfix(guest.string().c_str(), 2); + Require(redirected != nullptr); + add = reinterpret_cast(dlsym_nid_postfix(redirected, "GuestModuleAdd")); + Require(add && add(40, 2) == 42); + Require(dlclose_nid_postfix(redirected) == 0); + std::filesystem::remove(guest); + std::filesystem::remove(relinked); } diff --git a/core/libs/tests/GuestExecuteOnce.cpp b/core/libs/tests/GuestExecuteOnce.cpp new file mode 100644 index 000000000..327fbe8c3 --- /dev/null +++ b/core/libs/tests/GuestExecuteOnce.cpp @@ -0,0 +1,70 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +using Callback = int (APS5_VABI *)(void*, void*, void**); +extern "C" int APS5_VABI _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(int*, Callback, void*); +static int ExecuteOnce(int* flag, Callback callback, void* arg) { + return _ZSt13_Execute_onceRSt9once_flagPFiPvS1_PS1_ES1__nid_postfix(flag, callback, arg); +} +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 void* seenFirst = &control; +static void* seenArg = nullptr; +static void** seenThird = reinterpret_cast(&control); + +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) { + ++calls; + seenFirst = first; + seenArg = arg; + seenThird = third; + while (arrived.load() != 16) std::this_thread::yield(); + int value = 17; + Require(ExecuteOnce(&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(ExecuteOnce(&control, Initialize, &token) == 1); + Require(published == 42 && nestedValue == 17); + }); + for (auto& worker : workers) worker.join(); + Require(calls == 1 && control == 1 && nested == 1); + Require(seenFirst == nullptr && seenArg == &token && seenThird == nullptr); + Require(ExecuteOnce(&control, Unreachable, nullptr) == 1 && calls == 1); + + Require(ExecuteOnce(&failing, Fail, nullptr) == 0 && failing == 0 && failures == 1); + Require(ExecuteOnce(&failing, Fail, nullptr) == 0 && failing == 0 && failures == 2); + + try { ExecuteOnce(&throwing, Throw, nullptr); std::abort(); } + catch (int value) { Require(value == 7 && throwing == 0); } + Require(ExecuteOnce(&throwing, Throw, nullptr) == 1 && throwing == 1 && throws == 2); + + int preset = -3; + Require(ExecuteOnce(&preset, Unreachable, nullptr) == 1 && preset == -3); +} diff --git a/core/libs/tests/GuestFreebsdClocks.cpp b/core/libs/tests/GuestFreebsdClocks.cpp index 6c7f959f0..75a69daa6 100644 --- a/core/libs/tests/GuestFreebsdClocks.cpp +++ b/core/libs/tests/GuestFreebsdClocks.cpp @@ -8,6 +8,8 @@ int APS5_VABI clock_getres_nid_postfix(int clockId, KernelTimespec* res); int APS5_VABI sceKernelClockGettime(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelClockGetres(KernelClockid clockId, KernelTimespec* tp); int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds); +int APS5_VABI gettimeofday_nid_postfix(KernelTimeval* tv, KernelTimezone* tz); +std::int64_t APS5_VABI _Xtime_get_ticks_nid_postfix(); } static constexpr int SCE_OK = 0; @@ -120,6 +122,21 @@ static void IdleProcessAccumulatesNone() { Require(Nanos(GUEST_CLOCK_PROF) - profStart <= IDLE_LIMIT_NANOS); } +static std::int64_t Microseconds() { + KernelTimeval time{-1, -1}; + Require(gettimeofday_nid_postfix(&time, nullptr) == SCE_OK); + return time.tv_sec * 1000000LL + time.tv_usec; +} + +static void XtimeTicksAreGettimeofdayMicroseconds() { + for (int i = 0; i < 100; ++i) { + const std::int64_t before = Microseconds(); + const std::int64_t ticks = _Xtime_get_ticks_nid_postfix(); + const std::int64_t after = Microseconds(); + Require(before <= ticks && ticks <= after); + } +} + int main() { SecondClockReportsWholeSeconds(); @@ -135,4 +152,5 @@ int main() { ProcessClocksAreOrdered(); BusyProcessAccumulatesUserTime(); IdleProcessAccumulatesNone(); + XtimeTicksAreGettimeofdayMicroseconds(); } diff --git a/core/libs/tests/GuestHttpResponseHeader.cpp b/core/libs/tests/GuestHttpResponseHeader.cpp new file mode 100644 index 000000000..e02738dbc --- /dev/null +++ b/core/libs/tests/GuestHttpResponseHeader.cpp @@ -0,0 +1,109 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +extern "C" int APS5_VABI sceHttpParseResponseHeader(const char*, std::size_t, const char*, const char**, std::size_t*); + +static void Require(bool value) { + if (!value) { + std::fputs("HTTP response header check failed\n", stderr); + std::abort(); + } +} + +static void Check(std::string_view header, const char* field, std::size_t offset, std::string_view expected, int consumed) { + const char* value = nullptr; + std::size_t length = 999; + const int result = sceHttpParseResponseHeader(header.data(), header.size(), field, &value, &length); + if (result != consumed) { + std::fprintf(stderr, "Field %s: consumed %d bytes, expected %d\n", field, result, consumed); + } + Require(result == consumed); + Require(length == expected.size()); + Require(value == (expected.empty() ? nullptr : header.data() + offset)); + if (length != 0) Require(std::memcmp(value, expected.data(), length) == 0); +} + +static void CheckGuardPage() { +#ifdef _WIN32 + SYSTEM_INFO info; + GetSystemInfo(&info); + const std::size_t pageSize = info.dwPageSize; + auto* pages = static_cast(VirtualAlloc(nullptr, 2 * pageSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)); + Require(pages != nullptr); + DWORD oldProtection = 0; + Require(VirtualProtect(pages + pageSize, pageSize, PAGE_NOACCESS, &oldProtection) != 0); +#else + const long hostPageSize = sysconf(_SC_PAGESIZE); + Require(hostPageSize > 0); + const std::size_t pageSize = static_cast(hostPageSize); + auto* pages = static_cast(mmap(nullptr, 2 * pageSize, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)); + Require(pages != MAP_FAILED); + Require(mprotect(pages + pageSize, pageSize, PROT_NONE) == 0); +#endif + constexpr std::string_view text = "X: abc\r\n\tdef\r\n"; + char* header = pages + pageSize - text.size(); + std::memcpy(header, text.data(), text.size()); + Check({header, text.size()}, "x", 3, "abc\r\n\tdef", static_cast(text.size())); + Check({header, 6}, "x", 3, "abc", 6); + const char* value = header; + std::size_t length = 123; + Require(sceHttpParseResponseHeader(header, text.size(), "not-present", &value, &length) == static_cast(0x80432025)); + Require(value == header && length == 123); + Require(sceHttpParseResponseHeader(pages + pageSize, 0, "x", &value, &length) == static_cast(0x80432025)); + pages[pageSize - 1] = 'X'; + Require(sceHttpParseResponseHeader(pages + pageSize - 1, 1, "x", &value, &length) == static_cast(0x80432025)); +#ifdef _WIN32 + Require(VirtualFree(pages, 0, MEM_RELEASE) != 0); +#else + Require(munmap(pages, 2 * pageSize) == 0); +#endif +} + +int main() { + constexpr int invalidResponse = static_cast(0x80432060); + constexpr int invalidValue = static_cast(0x804321FE); + constexpr int notFound = static_cast(0x80432025); + Check("HTTP/1.1 200 OK\r\nX: \tvalue \t\r\nNext: no\r\n", "x", 21, "value \t", 30); + Check("X: first\r\nX: second\r\n", "X", 3, "first", 10); + Check("X: abc\n def\n\tghi\nY: no\n", "x", 3, "abc\n def\n\tghi", 17); + Check("X: no-newline", "X", 3, "no-newline", 13); + Check("X: value\r", "X", 3, "value\r", 9); + Check("X:", "X", 2, "", 0); + Check("X: \t", "X", 4, "", 0); + Check("X:\r\n\r\n", "X", 5, "", 0); + Check("X:\r\nnext\r\n", "X", 4, "next", 10); + Check("Prefix: wrong\n X: wrong\nXy: wrong\nX: right\n", "X", 37, "right", 43); + Check(std::string_view("X: a\0b\n", 7), "X", 3, std::string_view("a\0b", 3), 7); + + const char* marker = "unchanged"; + const char* value = marker; + std::size_t length = 456; + Require(sceHttpParseResponseHeader(nullptr, 1, "X", &value, &length) == invalidResponse); + Require(sceHttpParseResponseHeader(nullptr, 0, nullptr, nullptr, nullptr) == invalidResponse); + Require(sceHttpParseResponseHeader("X: yes", 6, nullptr, &value, &length) == invalidValue); + Require(sceHttpParseResponseHeader("X: yes", 6, "X", nullptr, &length) == invalidValue); + Require(sceHttpParseResponseHeader("X: yes", 6, "X", &value, nullptr) == invalidValue); + Require(sceHttpParseResponseHeader("X: yes", 0, "X", &value, &length) == notFound); + Require(sceHttpParseResponseHeader("X: yes", 1, "X", &value, &length) == notFound); + Require(sceHttpParseResponseHeader("X: yes", 6, "Longer-Field", &value, &length) == notFound); + Require(sceHttpParseResponseHeader("X: yes", 6, "X:", &value, &length) == notFound); + Require(value == marker && length == 456); + + std::string field(0xfff, 'A'); + std::string header = field + ": value\n"; + field += "ignored"; + Check(header, field.c_str(), 0x1000 + 1, "value", static_cast(header.size())); + CheckGuardPage(); +} diff --git a/core/libs/tests/GuestImeKeyboard.cpp b/core/libs/tests/GuestImeKeyboard.cpp new file mode 100644 index 000000000..cd3dbaf24 --- /dev/null +++ b/core/libs/tests/GuestImeKeyboard.cpp @@ -0,0 +1,67 @@ +#include "SceTypes.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceImeKeyboardOpen(int32_t user_id, const KeyboardParam* param); +int APS5_VABI sceImeKeyboardClose(int32_t user_id); +int APS5_VABI sceImeKeyboardGetInfo(uint32_t resource_id, KeyboardInfo* info); +int APS5_VABI sceImeKeyboardSetMode(int32_t user_id, uint32_t mode); +} + +static_assert(sizeof(KeyboardInfo) == 36); + +constexpr int NotOpened = static_cast(0x80bc0002u); +constexpr int InvalidUserId = static_cast(0x80bc0010u); +constexpr int NoResourceId = static_cast(0x80bc0023u); +constexpr int InvalidMode = static_cast(0x80bc0024u); +constexpr int InvalidAddress = static_cast(0x80bc0031u); + +static void Require(bool value, const char* message) { + if (value) return; + std::fprintf(stderr, "%s\n", message); + std::abort(); +} + +static void RequireUntouched(const KeyboardInfo& info) { + const auto* bytes = reinterpret_cast(&info); + for (size_t i = 0; i < sizeof(info); ++i) Require(bytes[i] == 0xa5, "keyboard info written"); +} + +static void CheckClosed() { + KeyboardInfo info; + std::memset(&info, 0xa5, sizeof(info)); + Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info"); + Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info without an open keyboard"); + RequireUntouched(info); + Require(sceImeKeyboardSetMode(-1, 0) == InvalidUserId, "set mode invalid user"); + Require(sceImeKeyboardSetMode(1, 0) == NotOpened, "set mode without an open keyboard"); + Require(sceImeKeyboardSetMode(1, 0x80) == NotOpened, "set mode checks the mode before the keyboard"); +} + +static void CheckOpened() { + KeyboardParam param{}; + Require(sceImeKeyboardOpen(1, ¶m) == 0, "keyboard open failed"); + KeyboardInfo info; + std::memset(&info, 0xa5, sizeof(info)); + Require(sceImeKeyboardGetInfo(0, nullptr) == InvalidAddress, "null info with an open keyboard"); + Require(sceImeKeyboardGetInfo(0, &info) == NoResourceId, "resource id 0 reported as a keyboard"); + Require(sceImeKeyboardGetInfo(0x12345678, &info) == NoResourceId, "unknown resource id reported as a keyboard"); + RequireUntouched(info); + Require(sceImeKeyboardSetMode(-1, 0) == InvalidUserId, "set mode invalid user with an open keyboard"); + Require(sceImeKeyboardSetMode(2, 0) == NotOpened, "set mode for a user without an open keyboard"); + Require(sceImeKeyboardSetMode(1, 0) == 0, "mode 0 rejected"); + Require(sceImeKeyboardSetMode(1, 0x7f) == 0, "all mode bits rejected"); + Require(sceImeKeyboardSetMode(1, 0x41) == 0, "manual mode without format characters rejected"); + Require(sceImeKeyboardSetMode(1, 0x80) == InvalidMode, "mode bit 7 accepted"); + Require(sceImeKeyboardSetMode(1, 0x80000001u) == InvalidMode, "mode bit 31 accepted"); + Require(sceImeKeyboardClose(1) == 0, "keyboard close failed"); + Require(sceImeKeyboardGetInfo(0, &info) == NotOpened, "info after close"); + Require(sceImeKeyboardSetMode(1, 0) == NotOpened, "set mode after close"); +} + +int main() { + CheckClosed(); + CheckOpened(); +} diff --git a/core/libs/tests/GuestMsgDialogNative.cpp b/core/libs/tests/GuestMsgDialogNative.cpp index 6e3b757e2..34f98d06b 100644 --- a/core/libs/tests/GuestMsgDialogNative.cpp +++ b/core/libs/tests/GuestMsgDialogNative.cpp @@ -10,6 +10,9 @@ int APS5_VABI sceMsgDialogUpdateStatus(void); int APS5_VABI sceMsgDialogGetResult(MsgDialogResult* result); int APS5_VABI sceMsgDialogClose(void); int APS5_VABI sceMsgDialogTerminate(void); +int APS5_VABI sceMsgDialogProgressBarInc(int target, std::uint32_t delta); +int APS5_VABI sceMsgDialogProgressBarSetMsg(int target, const char* msg); +int APS5_VABI sceMsgDialogProgressBarSetValue(int target, std::uint32_t rate); } namespace { @@ -33,8 +36,12 @@ int main() { Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusInitialized); + Require(sceMsgDialogProgressBarSetValue(0, 50) == kErrNotRunning); Require(sceMsgDialogOpen(param) == 0); Require(sceMsgDialogUpdateStatus() == kStatusFinished); + Require(sceMsgDialogProgressBarInc(0, 10) == kErrNotRunning); + Require(sceMsgDialogProgressBarSetMsg(0, "progress") == kErrNotRunning); + Require(sceMsgDialogProgressBarSetValue(0, 100) == kErrNotRunning); Require(sceMsgDialogClose() == kErrNotRunning); Require(sceMsgDialogUpdateStatus() == kStatusFinished); diff --git a/core/libs/tests/GuestPad.cpp b/core/libs/tests/GuestPad.cpp index 8abd4f04f..522d99171 100644 --- a/core/libs/tests/GuestPad.cpp +++ b/core/libs/tests/GuestPad.cpp @@ -1,4 +1,5 @@ #include "prx/libScePad/include/Pad.hpp" +#include "prx/libScePad/include/PadInputTypes.hpp" #include "prx/libScePad/include/PadState.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include @@ -42,6 +43,20 @@ static void CheckTiltCorrection(int handle) { PadPublishInput_nid_postfix(PadInputState{}); } +static void CheckTouchContact() { + PadInputState touch; + touch.buttons = static_cast(Pad::PadButton::TouchPad); + touch.touch[0] = {true, 960, 471, 0}; + PadPublishInput_nid_postfix(touch); + const auto data = Pad::ReadState(); + Require((data.buttons & static_cast(Pad::PadButton::TouchPad)) != 0); + Require(data.touch_data_touch_num == 1); + Require(data.touch_data_touch0_x == 960); + Require(data.touch_data_touch0_y == 471); + PadPublishInput_nid_postfix(PadInputState{}); + Pad::ReadState(); +} + int main() { constexpr int noHandle = static_cast(0x80920008); constexpr int user = 0x10000000; @@ -55,6 +70,7 @@ int main() { Require(scePadGetHandle(user, 0, 0) == handle); Require(scePadGetHandle(user, 2, 0) == handle); CheckTiltCorrection(handle); + CheckTouchContact(); Require(scePadGetHandle(0xff, 16, 0) == handle); Require(scePadGetHandle(user, 16, 0) == noHandle); Require(scePadGetHandle(user, 0, 1) == noHandle); diff --git a/core/libs/tests/GuestSceNet.cpp b/core/libs/tests/GuestSceNet.cpp index aae168ecc..48fcdcea1 100644 --- a/core/libs/tests/GuestSceNet.cpp +++ b/core/libs/tests/GuestSceNet.cpp @@ -1,9 +1,11 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include "SceTypes.hpp" #include +#include #include #include #include +#include extern "C" { int APS5_VABI sceNetInit_nid_postfix(void); @@ -18,6 +20,8 @@ std::int64_t APS5_VABI sceNetRecv(int, void*, std::size_t, int); std::int64_t APS5_VABI sceNetSendto(int, const void*, std::size_t, int, const void*, std::uint32_t); std::int64_t APS5_VABI sceNetRecvfrom(int, void*, std::size_t, int, void*, std::uint32_t*); int APS5_VABI sceNetSocketClose(int); +int APS5_VABI sceNetSetsockopt(int, int, int, const void*, std::uint32_t); +int* APS5_VABI sceNetErrnoLoc(void); int APS5_VABI sceNetEpollCreate(const char*, int); int APS5_VABI sceNetEpollControl(int, int, int, const NetEpollEvent*); int APS5_VABI sceNetEpollWait(int, NetEpollEvent*, int, int); @@ -53,6 +57,10 @@ int main() { address_size = peer.size(); const int accepted = sceNetAccept(listener, peer.data(), &address_size); Require(accepted >= 0 && address_size == 16); + const int nonblocking = 1; + Require(sceNetSetsockopt(accepted, 0xffff, 0x1200, &nonblocking, sizeof(nonblocking)) == 0); + char pending = 0; + Require(sceNetRecv(accepted, &pending, sizeof(pending), 0) == static_cast(0x80410123) && *sceNetErrnoLoc() == 35); const int epoll = sceNetEpollCreate("guest-sce-net", 0); Require(epoll >= 0); @@ -71,8 +79,15 @@ int main() { Require(std::strcmp(request, response) == 0); Require(sceNetEpollDestroy(epoll) == 0); Require(sceNetSocketClose(accepted) == 0); + bool send_failed = false; + for (int attempt = 0; attempt < 100 && !send_failed; ++attempt) { + send_failed = sceNetSend(client, request, sizeof(request), 0) < 0; + if (!send_failed) std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + Require(send_failed); Require(sceNetSocketClose(client) == 0); Require(sceNetSocketClose(listener) == 0); + Require(sceNetSocketClose(listener) == static_cast(0x80410109) && *sceNetErrnoLoc() == 9); const int udp_receiver = sceNetSocket(nullptr, 2, 2, 17); const int udp_sender = sceNetSocket(nullptr, 2, 2, 17); diff --git a/core/libs/tests/GuestShare.cpp b/core/libs/tests/GuestShare.cpp index 353b80bb4..f45247047 100644 --- a/core/libs/tests/GuestShare.cpp +++ b/core/libs/tests/GuestShare.cpp @@ -1,9 +1,15 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include +#include extern "C" { int APS5_VABI sceShareCaptureScreenshotExtended(const void* extended_param, std::int32_t* req_id); +int APS5_VABI sceShareCaptureScreenshot(const void* param, std::int32_t* req_id); +int APS5_VABI sceShareCaptureVideoClip(const void* param, std::int32_t* req_id); +int APS5_VABI sceShareGetCurrentStatus(std::uint32_t feature_flag, void* status); +int APS5_VABI sceShareOpenMenuForContent(const void* content_id); } namespace { @@ -26,5 +32,32 @@ int main() { Require(sceShareCaptureScreenshotExtended(param, nullptr) == notSupported); Require(sceShareCaptureScreenshotExtended(nullptr, nullptr) == notSupported); + + using Capture = int (APS5_VABI*)(const void*, std::int32_t*); + for (Capture capture : {sceShareCaptureScreenshot, sceShareCaptureVideoClip}) { + reqId = 7; + Require(capture(param, &reqId) == notSupported); + Require(reqId == -1); + reqId = 7; + Require(capture(nullptr, &reqId) == notSupported); + Require(reqId == -1); + Require(capture(param, nullptr) == notSupported); + } + + Require(sceShareOpenMenuForContent(param) == notSupported); + Require(sceShareOpenMenuForContent(nullptr) == notSupported); + + constexpr std::int32_t invalidParam = static_cast(0x81960002); + std::uint8_t status[18]; + std::memset(status, 0x5a, sizeof(status)); + Require(sceShareGetCurrentStatus(1, status) == 0); + for (std::size_t index = 0; index < sizeof(status); ++index) Require(status[index] == (index < 16 ? 0 : 0x5a)); + std::memset(status, 0x5a, sizeof(status)); + Require(sceShareGetCurrentStatus(0xffffffffu, status) == 0); + Require(status[0] == 0 && status[15] == 0 && status[16] == 0x5a); + std::memset(status, 0x5a, sizeof(status)); + Require(sceShareGetCurrentStatus(0, status) == invalidParam); + Require(status[0] == 0x5a); + Require(sceShareGetCurrentStatus(1, nullptr) == invalidParam); return 0; } diff --git a/core/libs/tests/GuestStrings.cpp b/core/libs/tests/GuestStrings.cpp index a743df2ac..808f69407 100644 --- a/core/libs/tests/GuestStrings.cpp +++ b/core/libs/tests/GuestStrings.cpp @@ -8,6 +8,7 @@ extern "C" { char* APS5_VABI basename_nid_postfix(const char*); int* APS5_VABI __error_nid_postfix(); std::size_t APS5_VABI strnlen_nid_postfix(const char*, std::size_t); +std::size_t APS5_VABI strnlen_s_nid_postfix(const char*, std::size_t); char* APS5_VABI strncat_nid_postfix(char*, const char*, std::size_t); char* APS5_VABI strpbrk_nid_postfix(const char*, const char*); std::size_t APS5_VABI strcspn_nid_postfix(const char*, const char*); @@ -88,6 +89,8 @@ int main() { Require(strnlen_nid_postfix(bounded, 0) == 0); Require(strnlen_nid_postfix(bounded, sizeof(bounded)) == 3); Require(strnlen_nid_postfix("a", 8) == 1); + Require(strnlen_s_nid_postfix(nullptr, 8) == 0); + Require(strnlen_s_nid_postfix("abc", 8) == 3 && strnlen_s_nid_postfix("abcdef", 4) == 4); char truncated[] = "abXX"; Require(strlcat_nid_postfix(truncated, "cd", 2) == 4); Require(std::strcmp(truncated, "abXX") == 0); diff --git a/core/libs/tests/GuestSystemFont.cpp b/core/libs/tests/GuestSystemFont.cpp new file mode 100644 index 000000000..53f205e5d --- /dev/null +++ b/core/libs/tests/GuestSystemFont.cpp @@ -0,0 +1,218 @@ +#include "prx/libSceFont/include/FontTypes.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceFontMemoryInit(FontMemory*, void*, std::uint32_t, const FontMemoryInterface*, void*, FontMemoryDestroyFunction, void*); +int APS5_VABI sceFontCreateLibrary(const FontMemory*, const void*, FontLibrary*); +int APS5_VABI sceFontDestroyLibrary(FontLibrary*); +int APS5_VABI sceFontSupportSystemFonts(FontLibrary); +int APS5_VABI sceFontSupportExternalFonts(FontLibrary, std::uint32_t, std::uint32_t); +int APS5_VABI sceFontOpenFontSet(FontLibrary, std::uint32_t, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontOpenFontMemory(FontLibrary, const void*, std::uint32_t, const FontOpenDetail*, FontHandle*); +int APS5_VABI sceFontOpenFontInstance(FontHandle, FontHandle, FontHandle*); +int APS5_VABI sceFontCloseFont(FontHandle); +int APS5_VABI sceFontSetScalePixel(FontHandle, float, float); +int APS5_VABI sceFontGetCharGlyphMetrics(FontHandle, std::uint32_t, FontGlyphMetrics*); +int APS5_VABI sceFontGetHorizontalLayout(FontHandle, FontHorizontalLayout*); +const void* APS5_VABI sceFontSelectLibraryFt(int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "System font check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static int allocations = 0; +static void* APS5_VABI Allocate(void*, std::uint32_t size) { + ++allocations; + return std::malloc(size); +} +static void APS5_VABI Release(void*, void* pointer) { + if (pointer) --allocations; + std::free(pointer); +} + +static void Put16(std::vector& out, std::uint32_t value) { + out.push_back(static_cast(value >> 8)); + out.push_back(static_cast(value)); +} + +static void Put32(std::vector& out, std::uint32_t value) { + Put16(out, value >> 16); + Put16(out, value & 0xFFFFu); +} + +static void PutAll16(std::vector& out, std::initializer_list values) { + for (const int value : values) Put16(out, static_cast(static_cast(value))); +} + +static std::vector SquareGlyphFont(std::uint32_t codepoint, int width) { + std::vector head; + Put32(head, 0x00010000u); + Put32(head, 0x00010000u); + Put32(head, 0); + Put32(head, 0x5F0F3CF5u); + PutAll16(head, {0x000B, 1000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, width, 800, 0, 8, 2, 0, 0}); + std::vector hhea; + Put32(hhea, 0x00010000u); + PutAll16(hhea, {800, -200, 0, width, 0, 0, width, 1, 0, 0, 0, 0, 0, 0, 0, 2}); + std::vector maxp; + Put32(maxp, 0x00010000u); + PutAll16(maxp, {2, 4, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0}); + std::vector hmtx; + PutAll16(hmtx, {width, 0, width, 100}); + std::vector glyf; + PutAll16(glyf, {1, 100, 0, width - 100, 700, 3, 0}); + glyf.insert(glyf.end(), {1, 1, 1, 1}); + PutAll16(glyf, {100, 0, width - 200, 0, 0, 700, 0, -700}); + std::vector loca; + PutAll16(loca, {0, 0, static_cast(glyf.size() / 2)}); + std::vector cmap; + PutAll16(cmap, {0, 1, 3, 1}); + Put32(cmap, 12); + PutAll16(cmap, {4, 32, 0, 4, 4, 1, 0, static_cast(codepoint), 0xFFFF, 0, static_cast(codepoint), 0xFFFF, 1 - static_cast(codepoint), 1, 0, 0}); + const std::vector*>> tables = { + {"cmap", &cmap}, {"glyf", &glyf}, {"head", &head}, {"hhea", &hhea}, {"hmtx", &hmtx}, {"loca", &loca}, {"maxp", &maxp}}; + std::vector font; + Put32(font, 0x00010000u); + PutAll16(font, {static_cast(tables.size()), 64, 2, static_cast(tables.size()) * 16 - 64}); + std::uint32_t offset = 12 + static_cast(tables.size()) * 16; + for (const auto& [tag, data] : tables) { + font.insert(font.end(), tag, tag + 4); + Put32(font, 0); + Put32(font, offset); + Put32(font, static_cast(data->size())); + offset += (static_cast(data->size()) + 3u) & ~3u; + } + for (const auto& [tag, data] : tables) { + font.insert(font.end(), data->begin(), data->end()); + font.resize((font.size() + 3u) & ~std::size_t{3}); + } + return font; +} + +static 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())); + Require(static_cast(file)); +} + +static void SetFontDirectory(const std::filesystem::path& directory) { +#ifdef _WIN32 + Require(_putenv_s("ANYPS5_SYSTEM_FONTS", directory.string().c_str()) == 0); +#else + Require(::setenv("ANYPS5_SYSTEM_FONTS", directory.string().c_str(), 1) == 0); +#endif +} + +static float AdvanceOf(FontHandle font, std::uint32_t code) { + FontGlyphMetrics metrics{}; + Require(sceFontGetCharGlyphMetrics(font, code, &metrics) == SCE_FONT_OK); + return metrics.Horizontal.advance; +} + +int main() { + constexpr std::uint32_t EuropeanLight = 0x180700C3u; + constexpr std::uint32_t EuropeanBold = 0x180700C7u; + constexpr std::uint32_t EuropeanItalic = 0x18170044u; + constexpr std::uint32_t ThaiMedium = 0x18071055u; + constexpr std::uint32_t VietnameseBold = 0x18070057u; + constexpr std::uint32_t JapaneseJg2Light = 0x1A0835D3u; + constexpr std::uint32_t ChineseGb = 0x180CB0D4u; + const std::filesystem::path root = std::filesystem::temp_directory_path() / "anyps5_guest_system_font"; + std::filesystem::remove_all(root); + const std::filesystem::path empty = root / "empty"; + const std::filesystem::path fonts = root / "fonts"; + std::filesystem::create_directories(empty); + std::filesystem::create_directories(fonts); + WriteFile(fonts / "SST-Bold.otf", SquareGlyphFont('A', 700)); + WriteFile(fonts / "NotoSans-Bold.ttf", SquareGlyphFont('A', 300)); + WriteFile(fonts / "NotoSans-Light.ttf", SquareGlyphFont('A', 500)); + WriteFile(fonts / "SST-Italic.otf", SquareGlyphFont('A', 520)); + WriteFile(fonts / "NotoSansThai-Medium.ttf", SquareGlyphFont(0x0E01, 540)); + WriteFile(fonts / "SSTVietnamese-Bold.otf", SquareGlyphFont(0x1EA0, 560)); + WriteFile(fonts / "NotoSansCJK-Light.ttc", SquareGlyphFont(0x3042, 580)); + + const FontMemoryInterface iface{Allocate, Release, nullptr, nullptr, nullptr, nullptr}; + FontMemory memory{}; + Require(sceFontMemoryInit(&memory, nullptr, 0, &iface, nullptr, nullptr, nullptr) == SCE_FONT_OK); + FontLibrary library = nullptr; + Require(sceFontCreateLibrary(&memory, sceFontSelectLibraryFt(0), &library) == SCE_FONT_OK); + Require(sceFontSupportSystemFonts(library) == SCE_FONT_OK); + + FontHandle font = nullptr; + SetFontDirectory(empty); + Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_ERROR_FONT_OPEN_FAILED && font == nullptr); + Require(sceFontOpenFontSet(library, 0x18070046u, 1, nullptr, &font) == SCE_FONT_ERROR_NO_SUPPORT_FONTSET && font == nullptr); + SetFontDirectory(root / "missing"); + bool threw = false; + try { + sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw); + + SetFontDirectory(fonts); + const int beforeOpen = allocations; + Require(sceFontOpenFontSet(library, EuropeanBold, 1, nullptr, &font) == SCE_FONT_OK && font != nullptr); + Require(allocations - beforeOpen == 3); + Require(sceFontSetScalePixel(font, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(font, 'A') == 70.0f); + FontGlyphMetrics metrics{}; + Require(sceFontGetCharGlyphMetrics(font, 'B', &metrics) == SCE_FONT_ERROR_NO_SUPPORT_GLYPH); + FontHorizontalLayout layout{}; + Require(sceFontGetHorizontalLayout(font, &layout) == SCE_FONT_OK && layout.baselineOffset > 0.0f && layout.lineAdvance >= layout.baselineOffset); + + FontHandle again = nullptr; + Require(sceFontOpenFontSet(library, EuropeanBold, 2, nullptr, &again) == SCE_FONT_OK && again != font); + FontHandle instance = nullptr; + Require(sceFontOpenFontInstance(font, nullptr, &instance) == SCE_FONT_OK && instance != nullptr); + Require(sceFontGetHorizontalLayout(instance, &layout) == SCE_FONT_OK && layout.baselineOffset > 0.0f); + + const std::vector>> expected = { + {EuropeanLight, {'A', 50.0f}}, {EuropeanItalic, {'A', 52.0f}}, {ThaiMedium, {0x0E01, 54.0f}}, + {VietnameseBold, {0x1EA0, 56.0f}}, {JapaneseJg2Light, {0x3042, 58.0f}}}; + for (const auto& [type, glyph] : expected) { + FontHandle set = nullptr; + Require(sceFontOpenFontSet(library, type, 1, nullptr, &set) == SCE_FONT_OK); + Require(sceFontSetScalePixel(set, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(set, glyph.first) == glyph.second); + Require(sceFontCloseFont(set) == SCE_FONT_OK); + } + Require(sceFontCloseFont(again) == SCE_FONT_OK); + FontHandle missing = nullptr; + Require(sceFontOpenFontSet(library, ChineseGb, 1, nullptr, &missing) == SCE_FONT_ERROR_FONT_OPEN_FAILED && missing == nullptr); + + std::filesystem::remove(fonts / "SST-Bold.otf"); + FontHandle substitute = nullptr; + Require(sceFontOpenFontSet(library, EuropeanBold, 3, nullptr, &substitute) == SCE_FONT_OK); + Require(sceFontSetScalePixel(substitute, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(substitute, 'A') == 30.0f); + Require(AdvanceOf(font, 'A') == 70.0f); + + Require(sceFontSupportExternalFonts(library, 2, 0x52) == SCE_FONT_OK); + const std::vector external = SquareGlyphFont('A', 900); + FontHandle memoryFont = nullptr; + Require(sceFontOpenFontMemory(library, external.data(), static_cast(external.size()), nullptr, &memoryFont) == SCE_FONT_OK); + Require(sceFontSetScalePixel(memoryFont, 100.0f, 100.0f) == SCE_FONT_OK); + Require(AdvanceOf(memoryFont, 'A') == 90.0f); + Require(sceFontGetHorizontalLayout(memoryFont, &layout) == SCE_FONT_OK); + + for (FontHandle handle : {font, instance, substitute, memoryFont}) Require(sceFontCloseFont(handle) == SCE_FONT_OK); + Require(sceFontDestroyLibrary(&library) == SCE_FONT_OK); + Require(allocations == 0); + std::filesystem::remove_all(root); +} diff --git a/core/libs/tests/GuestSystemService.cpp b/core/libs/tests/GuestSystemService.cpp index fe937fc57..cd875318c 100644 --- a/core/libs/tests/GuestSystemService.cpp +++ b/core/libs/tests/GuestSystemService.cpp @@ -23,7 +23,15 @@ 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); + int main() { + Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); + Require(sceSystemServicePowerTick() == SYSTEM_SERVICE_OK); + Require(sceSystemServiceReportAbnormalTermination(nullptr) == SYSTEM_SERVICE_OK); + int info = 0; + Require(sceSystemServiceReportAbnormalTermination(&info) == 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/GuestTcp.cpp b/core/libs/tests/GuestTcp.cpp index 2c6927336..0b9b42f2a 100644 --- a/core/libs/tests/GuestTcp.cpp +++ b/core/libs/tests/GuestTcp.cpp @@ -27,6 +27,7 @@ int main() { Require(listener >= 0); std::array address{16, 2, 0, 0, 127, 0, 0, 1}; Require(bind_nid_postfix(listener, address.data(), address.size()) == 0); + Require(bind_nid_postfix(listener, address.data(), address.size()) == -1 && *__error_nid_postfix() == 22); Require(listen_nid_postfix(listener, 4) == 0); std::uint32_t address_size = address.size(); Require(getsockname_nid_postfix(listener, address.data(), &address_size) == 0); @@ -34,6 +35,7 @@ int main() { const int client = socket_nid_postfix(2, 1, 0); Require(client >= 0); + Require(accept_nid_postfix(client, nullptr, nullptr) == -1 && *__error_nid_postfix() == 22); Require(connect_nid_postfix(client, address.data(), address.size()) == 0); std::array peer{}; address_size = peer.size(); diff --git a/core/libs/tests/GuestThreadSelf.cpp b/core/libs/tests/GuestThreadSelf.cpp index 585745cbd..4290ec703 100644 --- a/core/libs/tests/GuestThreadSelf.cpp +++ b/core/libs/tests/GuestThreadSelf.cpp @@ -15,6 +15,23 @@ 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 scePthreadAttrInit(PthreadAttr* attr); +int APS5_VABI scePthreadAttrDestroy(PthreadAttr* attr); +int APS5_VABI scePthreadAttrGet(Pthread thread, PthreadAttr* attr); +int APS5_VABI scePthreadAttrGetstackaddr(const PthreadAttr* attr, void** stack_addr); +int APS5_VABI scePthreadAttrGetstacksize(const PthreadAttr* attr, size_t* stack_size); +} + +static bool StackContains(Pthread thread, const void* address) { + PthreadAttr attr = nullptr; + if (scePthreadAttrInit(&attr) != 0 || scePthreadAttrGet(thread, &attr) != 0) return false; + void* stack = nullptr; + size_t size = 0; + const bool queried = scePthreadAttrGetstackaddr(&attr, &stack) == 0 && scePthreadAttrGetstacksize(&attr, &size) == 0; + scePthreadAttrDestroy(&attr); + const auto begin = reinterpret_cast(stack); + const auto value = reinterpret_cast(address); + return queried && stack != nullptr && size != 0 && value >= begin && value - begin < size; } static constexpr int SCE_OK = 0; @@ -28,6 +45,7 @@ static void Require(bool value) { if (!value) std::abort(); } struct WorkerContext { Pthread thread = nullptr; Pthread selfFromWorker = nullptr; + bool workerStackReported = false; PthreadMutex* mutex = nullptr; int unlockResult = 0; }; @@ -35,6 +53,8 @@ struct WorkerContext { static void* APS5_VABI Worker(void* arg) { auto& context = *static_cast(arg); context.selfFromWorker = scePthreadSelf(); + int local = 0; + context.workerStackReported = StackContains(context.selfFromWorker, &local); context.unlockResult = scePthreadMutexUnlock(context.mutex); scePthreadExit(reinterpret_cast(WorkerRetval)); return nullptr; @@ -44,6 +64,8 @@ int main() { const Pthread mainSelf = scePthreadSelf(); Require(mainSelf != nullptr); Require(scePthreadSelf() == mainSelf); + int local = 0; + Require(StackContains(mainSelf, &local)); Require(scePthreadJoin(mainSelf, nullptr) == SCE_KERNEL_ERROR_EINVAL); Require(scePthreadDetach(mainSelf) == SCE_KERNEL_ERROR_EINVAL); @@ -69,6 +91,7 @@ int main() { Require(context.selfFromWorker == context.thread); Require(context.selfFromWorker != mainSelf); Require(context.unlockResult == SCE_KERNEL_ERROR_EPERM); + Require(context.workerStackReported); Require(scePthreadMutexUnlock(&mutex) == SCE_OK); Require(scePthreadMutexDestroy(&mutex) == SCE_OK); diff --git a/core/libs/tests/GuestVoiceQoS.cpp b/core/libs/tests/GuestVoiceQoS.cpp new file mode 100644 index 000000000..9f9e668a6 --- /dev/null +++ b/core/libs/tests/GuestVoiceQoS.cpp @@ -0,0 +1,30 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceVoiceQoSInit(void*, std::uint32_t, std::int32_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static bool Throws(TFunction function) { + try { + function(); + } catch (const TException&) { + return true; + } + return false; +} + +int main() { + std::vector memory(0x40000); + Require(Throws([&] { sceVoiceQoSInit(nullptr, 0x40000, 0x20000000); })); + Require(Throws([&] { sceVoiceQoSInit(memory.data(), 0, 0x20000000); })); + Require(Throws([&] { sceVoiceQoSInit(memory.data(), 0x40000, 0); })); + Require(sceVoiceQoSInit(memory.data(), 0x40000, 0x20000000) == 0); + Require(Throws([&] { sceVoiceQoSInit(memory.data(), 0x40000, 0x20000000); })); +} diff --git a/core/libs/tests/GuestWcsrtombsS.cpp b/core/libs/tests/GuestWcsrtombsS.cpp new file mode 100644 index 000000000..758ff5d33 --- /dev/null +++ b/core/libs/tests/GuestWcsrtombsS.cpp @@ -0,0 +1,78 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" int APS5_VABI wcsrtombs_s_nid_postfix( + std::size_t*, char*, std::size_t, const std::uint16_t**, std::size_t, void*); +static void Require(bool value) { if (!value) std::abort(); } + +constexpr auto Failed = static_cast(-1); +constexpr auto Huge = std::size_t{1} << 63; +static const std::uint16_t text[] = {'a', 'b', 'c', 0}; +static const std::uint16_t wide[] = {'a', 0x100, 'b', 0}; + +struct Run { + int status; + std::size_t result = 7; + const std::uint16_t* source; + char out[8]; +}; + +static Run Convert(const std::uint16_t* input, std::size_t capacity, std::size_t limit, bool toBuffer = true) { + Run run{}; + std::memset(run.out, 'x', sizeof(run.out)); + run.source = input; + std::uint64_t state[2] = {}; + run.status = wcsrtombs_s_nid_postfix(&run.result, toBuffer ? run.out : nullptr, capacity, &run.source, limit, state); + return run; +} + +int main() { + auto run = Convert(text, 0, 0, false); + Require(run.status == 0 && run.result == 3 && run.source == text); + run = Convert(wide, 0, Huge, false); + Require(run.status == 86 && run.result == Failed && run.source == wide); + run = Convert(text, 5, 1, false); + Require(run.status == 22 && run.result == Failed && run.source == text); + + run = Convert(text, 8, 8); + Require(run.status == 0 && run.result == 3 && run.source == nullptr && std::memcmp(run.out, "abc\0x", 5) == 0); + run = Convert(text, 4, 4); + Require(run.status == 0 && run.result == 3 && run.source == nullptr && std::memcmp(run.out, "abc\0x", 5) == 0); + run = Convert(text, 8, 2); + Require(run.status == 0 && run.result == 2 && run.source == text + 2 && std::memcmp(run.out, "ab\0x", 4) == 0); + run = Convert(text, 8, 0); + Require(run.status == 0 && run.result == 0 && run.source == text && std::memcmp(run.out, "\0x", 2) == 0); + + run = Convert(text, 3, 8); + Require(run.status == 34 && run.result == Failed && run.source == text + 3 && std::memcmp(run.out, "\0bcx", 4) == 0); + run = Convert(text, 3, 3); + Require(run.status == 34 && run.result == Failed && run.source == text + 3 && std::memcmp(run.out, "\0bcx", 4) == 0); + run = Convert(text, 1, 8); + Require(run.status == 34 && run.result == Failed && run.source == text + 1 && std::memcmp(run.out, "\0x", 2) == 0); + + run = Convert(wide, 8, 8); + Require(run.status == 86 && run.result == Failed && run.source == wide && std::memcmp(run.out, "a\0xx", 4) == 0); + + run = Convert(text, 0, 8); + Require(run.status == 22 && run.result == Failed && run.source == text && run.out[0] == 'x'); + run = Convert(text, Huge, 8); + Require(run.status == 22 && run.result == Failed && run.out[0] == 'x'); + run = Convert(text, 8, Huge); + Require(run.status == 22 && run.result == Failed && run.source == text && run.out[0] == '\0'); + run = Convert(nullptr, 8, 8); + Require(run.status == 22 && run.result == Failed && run.out[0] == '\0'); + + char out[4] = {'x', 'x', 'x', 'x'}; + std::size_t result = 7; + const std::uint16_t* source = text; + std::uint64_t state[2] = {}; + Require(wcsrtombs_s_nid_postfix(nullptr, out, 4, &source, 4, state) == 22 && out[0] == '\0' && source == text); + out[0] = 'x'; + Require(wcsrtombs_s_nid_postfix(&result, out, 4, nullptr, 4, state) == 22 && result == Failed && out[0] == '\0'); + out[0] = 'x'; + result = 7; + Require(wcsrtombs_s_nid_postfix(&result, out, 4, &source, 4, nullptr) == 22 && result == Failed && out[0] == '\0'); + Require(source == text && std::memcmp(out, "\0xxx", 4) == 0); +} diff --git a/core/libs/tests/ParamJsonParser.cpp b/core/libs/tests/ParamJsonParser.cpp index fc3c06cb0..88684685d 100644 --- a/core/libs/tests/ParamJsonParser.cpp +++ b/core/libs/tests/ParamJsonParser.cpp @@ -24,6 +24,37 @@ static ParsedParamJson ParseSize(const std::filesystem::path& path, const std::s return parseParamJson(path); } +static ParsedParamJson ParseText(const std::filesystem::path& path, const std::string& text) { + { + std::ofstream file(path, std::ios::binary); + file << text; + Require(static_cast(file), "Cannot write param.json fixture"); + } + return parseParamJson(path); +} + +static void CheckLanguages(const std::filesystem::path& path) { + const auto nested = ParseText(path, "{\r\n \"localizedParameters\": {\r\n \"defaultLanguage\": \"en-GB\",\r\n" + " \"en-GB\": { \"titleName\": \"British\" }\r\n },\r\n \"titleId\": \"PPSA00001\"\r\n}\r\n"); + Require(nested.title == "British" && nested.titleId == "PPSA00001", "Nested defaultLanguage without en-US"); + const auto selected = ParseText(path, R"({"titleId":"PPSA00002","localizedParameters":{"defaultLanguage":"fr-FR",)" + R"("en-US":{"titleName":"American"},"fr-FR":{"titleName":"French"}}})"); + Require(selected.title == "French", "Nested defaultLanguage not selected"); + const auto american = ParseText(path, R"({"titleId":"PPSA00003","localizedParameters":{"defaultLanguage":"de-DE",)" + R"("ja-JP":{"titleName":"Japanese"},"en-US":{"titleName":"American"}}})"); + Require(american.title == "American", "Missing defaultLanguage entry does not fall back to en-US"); + const auto root = ParseText(path, R"({"titleId":"PPSA00004","defaultLanguage":"ja-JP","localizedParameters":{)" + R"("en-US":{"titleName":"American"},"ja-JP":{"titleName":"Japanese"}}})"); + Require(root.title == "Japanese", "Root defaultLanguage not selected"); + bool rejected = false; + try { + ParseText(path, R"({"titleId":"PPSA00005","localizedParameters":{"defaultLanguage":"en-GB"}})"); + } catch (const std::exception&) { + rejected = true; + } + Require(rejected, "Accepted localizedParameters without a language entry"); +} + int main() { const auto path = std::filesystem::temp_directory_path() / ("anyps5-param-json-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json"); @@ -47,6 +78,7 @@ int main() { try { ParseSize(path, text); } catch (const std::exception&) { rejected = true; } Require(rejected, std::string("Accepted invalid download size: ") + text); } + CheckLanguages(path); } catch (const std::exception& error) { std::cerr << error.what() << '\n'; result = 1; diff --git a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp index eebb504ca..cef7ff2cf 100644 --- a/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp +++ b/core/relinker/relinker/src/guest/GuestModuleBuilder.cpp @@ -49,6 +49,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (lazyBinding) throw Domain::RelinkerException("Guest modules require eager binding; --lazy-binding is incompatible"); std::vector images; std::map exports; + std::map> sharedExports; std::set outputNames; Io::FileReader reader; for (const auto& path : paths) { @@ -65,7 +66,11 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path for (const auto& symbol : image.Symbols) { if (symbol.Section == 0 || (symbol.Info >> 4) == 0 || symbol.Visibility == 1 || symbol.Visibility == 2) continue; const auto [existing, inserted] = exports.emplace(symbol.Name, images.size()); - if (!inserted) throw Domain::RelinkerException("Duplicate guest export after stripping #: " + symbol.Name + " in " + images.at(existing->second).SourcePath.string() + " and " + path.string()); + 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()); } std::vector codeHeaders; for (const auto& header : image.Headers) if (header.Type == 1 && (header.Flags & 1) != 0) codeHeaders.push_back(header); @@ -88,6 +93,24 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path if (!inserted && found->second != index) throw Domain::RelinkerException("Ambiguous guest dependency name: " + name); } } + const auto rejectSharedImport = [&](const std::string& name, const std::string& importer) { + const auto shared = sharedExports.find(name); + if (shared == sharedExports.end()) return; + std::string providers; + for (const auto provider : shared->second) providers += " " + images[provider].SourcePath.string(); + throw Domain::RelinkerException("Ambiguous guest import " + name + " in " + importer + ": exported by" + providers); + }; + if (dynamic.DynSymData.size() % 24 != 0) throw Domain::RelinkerException("Invalid executable symbol table"); + for (std::size_t offset = 0; offset < dynamic.DynSymData.size(); offset += 24) { + if (Io::ReadU16(dynamic.DynSymData, offset + 6) != 0) continue; + const auto nameOffset = Io::ReadU32(dynamic.DynSymData, offset); + if (nameOffset >= dynamic.DynStrData.size()) throw Domain::RelinkerException("Invalid executable symbol name offset"); + const auto start = dynamic.DynStrData.begin() + nameOffset; + const auto end = std::find(start, dynamic.DynStrData.end(), 0); + if (end == dynamic.DynStrData.end()) throw Domain::RelinkerException("Unterminated executable symbol name"); + const std::string name(start, end); + rejectSharedImport(name.substr(0, name.find('#')), inputPath.string()); + } std::vector> dependencies(images.size()); for (auto& image : images) image.UsePlatformTlsResolver = !exports.contains("vNe1w4diLCs"); for (std::size_t index = 0; index < images.size(); ++index) { @@ -97,6 +120,7 @@ std::vector GuestModuleBuilder::Build(const std::filesystem::path } for (const auto& symbol : images[index].Symbols) { if (symbol.Section != 0 || symbol.Name.empty()) continue; + rejectSharedImport(symbol.Name, images[index].SourcePath.string()); const auto found = exports.find(symbol.Name); if (found != exports.end()) { const auto& provider = images[found->second]; diff --git a/core/relinker/relinker/tests/test_guest_module_directories.py b/core/relinker/relinker/tests/test_guest_module_directories.py index 5d0389cb2..992f89abf 100644 --- a/core/relinker/relinker/tests/test_guest_module_directories.py +++ b/core/relinker/relinker/tests/test_guest_module_directories.py @@ -122,7 +122,21 @@ def main(): (case / name).mkdir(parents=True) (case / name / f"{name}.prx").write_bytes(module_with_symbol(True)) result, output = convert(case, windows) - assert result.returncode == 2 and "Duplicate guest export" in result.stderr, result.stderr + if windows: + assert result.returncode == 2 and "Duplicate guest export" in result.stderr, result.stderr + assert not output.exists() and not (case / "app0").exists(), output + else: + assert result.returncode == 0 and output.exists(), result.stderr + assert {path.name for path in (case / "app0").rglob("*.guest.prx")} == {"sce_module.prx.guest.prx", "prx.prx.guest.prx"} + + case = work / f"{windows}-ambiguous-import" + for name in ("sce_module", "prx"): + (case / name).mkdir(parents=True) + (case / name / f"{name}.prx").write_bytes(module_with_symbol(True)) + (case / "prx" / "consumer.prx").write_bytes(module_with_symbol(False)) + result, output = convert(case, windows) + expected = "Duplicate guest export" if windows else "Ambiguous guest import shared" + assert result.returncode == 2 and expected in result.stderr, result.stderr assert not output.exists() and not (case / "app0").exists(), output print("Guest module directory integration tests passed") diff --git a/core/shader/recompiler/CacheKey.hpp b/core/shader/recompiler/CacheKey.hpp index 79fee172b..800fdbbbd 100644 --- a/core/shader/recompiler/CacheKey.hpp +++ b/core/shader/recompiler/CacheKey.hpp @@ -140,6 +140,7 @@ private: append(key, value.sampleMaskExportEnable); append(key, value.earlyZ); append(key, value.executeOnNoop); + append(key, value.conservativeZExport); append(key, value.targetOutputMode); append(key, value.targetExportMapping); } diff --git a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp index 2c5ae693d..cfcb917ff 100644 --- a/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp +++ b/core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -110,10 +111,59 @@ bool isImmediate(const RdnaOperand& operand, std::uint32_t& value) { return false; } +constexpr std::uint32_t NoScalarRegister = UINT32_MAX; + +std::uint32_t scalarIndex(const RdnaOperand& operand) { + switch (operand.kind) { + case RdnaOperandKind::ScalarRegister: return operand.reg; + case RdnaOperandKind::VccLo: return 106u; + case RdnaOperandKind::VccHi: return 107u; + default: return NoScalarRegister; + } +} + +bool isScalar(const RdnaOperand& operand, std::uint32_t index) { + return index != NoScalarRegister && scalarIndex(operand) == index; +} + +bool addsImmediateTo(const RdnaInstruction& instruction, std::uint32_t index, std::uint32_t& immediate) { + return isScalar(instruction.destination, index) && + ((isScalar(instruction.source0, index) && isImmediate(instruction.source1, immediate)) || + (isScalar(instruction.source1, index) && isImmediate(instruction.source0, immediate))); +} + +bool subtractsImmediateFrom(const RdnaInstruction& instruction, std::uint32_t index, std::uint32_t& immediate) { + return isScalar(instruction.destination, index) && isScalar(instruction.source0, index) && isImmediate(instruction.source1, immediate); +} + +bool resolveLongSetpcTarget(const RdnaProgram& program, std::uint32_t setpcIndex, std::uint32_t& target) { + if (setpcIndex < 3u) return false; + const auto& setpc = program.instructions[setpcIndex]; + const auto& pc = program.instructions[setpcIndex - 3u]; + const auto& low = program.instructions[setpcIndex - 2u]; + const auto& high = program.instructions[setpcIndex - 1u]; + const auto pcRegister = scalarIndex(setpc.source0); + const bool adds = low.op == RdnaOpcode::SAddU32 && high.op == RdnaOpcode::SAddcU32; + const bool subtracts = low.op == RdnaOpcode::SSubU32 && high.op == RdnaOpcode::SSubbU32; + std::uint32_t lowImmediate = 0, highImmediate = 0; + if (setpc.op != RdnaOpcode::SSetpcB64 || pcRegister == NoScalarRegister || pcRegister % 2u != 0u || pc.op != RdnaOpcode::SGetpcB64 || !isScalar(pc.destination, pcRegister)) return false; + if (adds ? !addsImmediateTo(low, pcRegister, lowImmediate) || !addsImmediateTo(high, pcRegister + 1u, highImmediate) + : !subtracts || !subtractsImmediateFrom(low, pcRegister, lowImmediate) || !subtractsImmediateFrom(high, pcRegister + 1u, highImmediate)) return false; + const auto offset = (static_cast(highImmediate) << 32u) | lowImmediate; + const auto base = static_cast(instructionEndProgramCounter(pc)); + const auto destination = adds ? base + offset : base - offset; + if (destination > UINT32_MAX || (destination & 3u) != 0u) return false; + target = static_cast(destination); + return true; +} + bool resolveSetpcTarget(const RdnaProgram& program, std::uint32_t setpcIndex, std::uint32_t& target) { if (setpcIndex >= program.instructions.size()) { return false; } + if (resolveLongSetpcTarget(program, setpcIndex, target)) { + return true; + } const auto& setpc = program.instructions[setpcIndex]; if (setpc.op != RdnaOpcode::SSetpcB64 || setpc.source0.kind != RdnaOperandKind::ScalarRegister) { @@ -233,6 +283,115 @@ bool resolveJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJu return false; } +bool writesScalar(const RdnaInstruction& instruction, std::uint32_t index) { + std::uint32_t count = instruction.dataDwordCount; + switch (instruction.family) { + case RdnaInstructionFamily::SOP1: + case RdnaInstructionFamily::SOP2: + case RdnaInstructionFamily::SOPK: + if (instruction.op == RdnaOpcode::SMovreldB32 || instruction.op == RdnaOpcode::SMovreldB64 || instruction.op == RdnaOpcode::SMovrelsd2B32) return true; + break; + case RdnaInstructionFamily::SMEM: + if (instruction.op == RdnaOpcode::SMemtime || instruction.op == RdnaOpcode::SMemrealtime) count = 2u; + break; + case RdnaInstructionFamily::VOP1: + case RdnaInstructionFamily::VOP2: + case RdnaInstructionFamily::VOP3: + case RdnaInstructionFamily::VOP3P: + case RdnaInstructionFamily::VOPC: + count = 2u; + break; + case RdnaInstructionFamily::SOPC: + case RdnaInstructionFamily::SOPP: + case RdnaInstructionFamily::VINTRP: + case RdnaInstructionFamily::MUBUF: + case RdnaInstructionFamily::MTBUF: + case RdnaInstructionFamily::FLAT: + case RdnaInstructionFamily::DS: + case RdnaInstructionFamily::MIMG: + case RdnaInstructionFamily::EXP: return false; + case RdnaInstructionFamily::Unknown: return true; + } + const auto covers = [&](const RdnaOperand& operand) { + const auto first = scalarIndex(operand); + return first != NoScalarRegister && index >= first && index - first < count; + }; + return covers(instruction.destination) || covers(instruction.destination2); +} + +bool findLastWriter(const RdnaProgram& program, std::uint32_t end, std::initializer_list indices, std::uint32_t& writer) { + for (auto position = end; position-- > 0u;) { + const auto& instruction = program.instructions[position]; + if (IsDirectBranchOpcode(instruction.op) || instruction.op == RdnaOpcode::SSetpcB64 || instruction.op == RdnaOpcode::SEndpgm) return false; + if (std::ranges::any_of(indices, [&](std::uint32_t index) { return writesScalar(instruction, index); })) { + writer = position; + return true; + } + } + return false; +} + +bool resolveDwordJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJumpTable& result) { + if (index < 2u) return false; + const auto& branch = program.instructions[index]; + const auto& low = program.instructions[index - 2u]; + const auto& high = program.instructions[index - 1u]; + if (branch.source0.kind != RdnaOperandKind::ScalarRegister || branch.source0.reg % 2u != 0u) return false; + const auto pcReg = branch.source0.reg; + const auto entryReg = scalarIndex(low.source1); + std::uint32_t borrow = 0; + if (low.op != RdnaOpcode::SSubU32 || !isScalar(low.destination, pcReg) || !isScalar(low.source0, pcReg) || + entryReg == NoScalarRegister || entryReg == pcReg || entryReg == pcReg + 1u || + high.op != RdnaOpcode::SSubbU32 || !subtractsImmediateFrom(high, pcReg + 1u, borrow) || borrow != 0u) return false; + std::uint32_t loadIndex = 0, shiftIndex = 0, boundIndex = 0, baseIndex = 0; + if (!findLastWriter(program, index - 2u, {entryReg, pcReg, pcReg + 1u}, loadIndex)) return false; + const auto& load = program.instructions[loadIndex]; + const auto offsetReg = scalarIndex(load.source1); + if (load.op != RdnaOpcode::SLoadDword || !isScalar(load.destination, entryReg) || writesScalar(load, pcReg) || writesScalar(load, pcReg + 1u) || + !isScalar(load.source0, pcReg) || offsetReg == NoScalarRegister || offsetReg == pcReg || offsetReg == pcReg + 1u) return false; + if (!findLastWriter(program, loadIndex, {offsetReg}, shiftIndex) || !findLastWriter(program, shiftIndex, {offsetReg}, boundIndex) || + !findLastWriter(program, loadIndex, {pcReg, pcReg + 1u}, baseIndex) || baseIndex < 2u) return false; + const auto& shift = program.instructions[shiftIndex]; + const auto& bound = program.instructions[boundIndex]; + const auto& basePc = program.instructions[baseIndex - 2u]; + const auto& baseLow = program.instructions[baseIndex - 1u]; + const auto& baseHigh = program.instructions[baseIndex]; + std::uint32_t shiftAmount = 0, maximum = 0, displacement = 0, carry = 0; + if (shift.op != RdnaOpcode::SLshlB32 || !isScalar(shift.destination, offsetReg) || !isScalar(shift.source0, offsetReg) || + !isImmediate(shift.source1, shiftAmount) || shiftAmount != 2u || + bound.op != RdnaOpcode::SMinU32 || !isScalar(bound.destination, offsetReg) || + !(isImmediate(bound.source1, maximum) || isImmediate(bound.source0, maximum)) || maximum > 255u || + basePc.op != RdnaOpcode::SGetpcB64 || !isScalar(basePc.destination, pcReg) || + baseLow.op != RdnaOpcode::SAddU32 || !addsImmediateTo(baseLow, pcReg, displacement) || + baseHigh.op != RdnaOpcode::SAddcU32 || !addsImmediateTo(baseHigh, pcReg + 1u, carry) || carry != 0u) return false; + const auto& first = program.instructions[std::min(boundIndex, baseIndex - 2u)]; + for (const auto& instruction : program.instructions) { + if (IsDirectBranchOpcode(instruction.op) && instruction.branchTarget > first.programCounter && instruction.branchTarget <= branch.programCounter) return false; + } + const auto base = static_cast(instructionEndProgramCounter(basePc)) + displacement; + const auto table = base + static_cast(load.memoryOffset); + const auto byteCount = (static_cast(maximum) + 1) * 4; + if (table < 0 || (table & 3) != 0 || table + byteCount > static_cast(program.code.size_bytes())) return false; + BoundedJumpTable resolved; + resolved.load.programCounter = load.programCounter; + resolved.firstProgramCounter = first.programCounter; + for (std::uint32_t entry = 0; entry <= maximum; ++entry) { + const auto value = program.code[static_cast(table / 4) + entry]; + const auto target = base - static_cast(value); + if (target < static_cast(instructionEndProgramCounter(branch)) || target > UINT32_MAX || (target & 3) != 0) return false; + resolved.load.values.push_back(value); + resolved.targets.push_back(static_cast(target)); + } + result = std::move(resolved); + return true; +} + +bool resolveBoundedJumpTable(const RdnaProgram& program, std::uint32_t index, BoundedJumpTable& result) { + if (resolveJumpTable(program, index, result)) return true; + result = BoundedJumpTable{}; + return resolveDwordJumpTable(program, index, result); +} + BranchCondition conditionForOpcode(RdnaOpcode opcode) { switch (opcode) { case RdnaOpcode::SBranch: return BranchCondition::Always; @@ -346,7 +505,7 @@ std::vector GraphBuilder::splitIntoBlocks(const RdnaProgram& program std::uint32_t target = 0; if (!resolveSetpcTarget(program, index, target)) { BoundedJumpTable table; - if (!resolveJumpTable(program, index, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(instruction.programCounter)); + if (!resolveBoundedJumpTable(program, index, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(instruction.programCounter)); for (const auto tableTarget : table.targets) { if (!instructionProgramCounters.contains(tableTarget)) throw std::invalid_argument("jump table targets invalid instruction boundary " + toHexString(tableTarget)); labels.insert(tableTarget); @@ -413,7 +572,7 @@ void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram std::uint32_t target = 0; if (!resolveSetpcTarget(program, block.instructionEnd - 1u, target)) { BoundedJumpTable table; - if (!resolveJumpTable(program, block.instructionEnd - 1u, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(last.programCounter)); + if (!resolveBoundedJumpTable(program, block.instructionEnd - 1u, table)) throw std::invalid_argument("unsupported dynamic s_setpc_b64 at program counter " + toHexString(last.programCounter)); block.terminator.kind = TerminatorKind::IndirectBranch; block.terminator.indirectPcSgpr = last.source0.reg; sortUnique(table.targets); @@ -424,6 +583,7 @@ void GraphBuilder::linkBlocks(std::vector& blocks, const RdnaProgram } continue; } + if (block.instructionEnd - block.instructionBegin < 4u && resolveLongSetpcTarget(program, block.instructionEnd - 1u, target)) throw std::invalid_argument("long branch at program counter " + toHexString(last.programCounter) + " does not start in its block"); block.terminator.kind = TerminatorKind::Branch; block.terminator.condition = BranchCondition::Always; block.terminator.trueBlock = programCounterToBlock.at(target); @@ -485,7 +645,7 @@ ControlFlowGraph GraphBuilder::Build(const RdnaProgram& program) const { const auto term = graph.blocks[id].terminator; if (term.kind != TerminatorKind::IndirectBranch) continue; BoundedJumpTable table; - if (!resolveJumpTable(program, graph.blocks[id].instructionEnd - 1u, table)) throw std::logic_error("jump table resolution changed"); + if (!resolveBoundedJumpTable(program, graph.blocks[id].instructionEnd - 1u, table)) throw std::logic_error("jump table resolution changed"); if (table.firstProgramCounter < graph.blocks[id].startProgramCounter) throw std::invalid_argument("jump table bound does not dominate its branch in the same block"); graph.codeTableLoadProgramCounters.push_back(table.load.programCounter); graph.codeTableLoads.push_back(std::move(table.load)); diff --git a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp index 21ac3eddf..0e22804d0 100644 --- a/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp +++ b/core/shader/recompiler/ControlFlow/src/RequestSerializer.cpp @@ -227,6 +227,14 @@ ProgramRole readProgramRole(Reader& reader) { return static_cast(value); } +ConservativeZExport readConservativeZExport(Reader& reader) { + const auto value = reader.ReadU8(); + if (value > static_cast(ConservativeZExport::GreaterThanZ)) { + throw std::runtime_error("RequestSerializer: invalid ConservativeZExport value"); + } + return static_cast(value); +} + void writeShaderBinary(Writer& writer, const ShaderBinary& binary) { writer.WriteU8(static_cast(binary.stage)); writer.WriteU64(binary.codeAddress); @@ -296,6 +304,7 @@ void writePixelInfo(Writer& writer, const ShaderPixelStageInfo& info) { writer.WriteBool(info.sampleMaskExportEnable); writer.WriteBool(info.earlyZ); writer.WriteBool(info.executeOnNoop); + writer.WriteU8(static_cast(info.conservativeZExport)); for (const std::uint8_t value : info.targetOutputMode) { writer.WriteU8(value); } @@ -328,6 +337,7 @@ ShaderPixelStageInfo readPixelInfo(Reader& reader, std::uint32_t version) { info.sampleMaskExportEnable = reader.ReadBool(); info.earlyZ = reader.ReadBool(); info.executeOnNoop = reader.ReadBool(); + if (version >= 7u) info.conservativeZExport = readConservativeZExport(reader); for (std::uint8_t& value : info.targetOutputMode) { value = reader.ReadU8(); } @@ -677,7 +687,7 @@ std::string RequestSerializer::Serialize(const RecompileRequest& request) const std::string buffer; Writer writer(buffer); writer.WriteU32(0x41505335u); - writer.WriteU32(6u); + writer.WriteU32(7u); writeShaderBinary(writer, request.shader); writeGuestContext(writer, request.context); writeSpirvTarget(writer, request.target); @@ -701,7 +711,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 > 6u) throw std::runtime_error("unsupported recompile request serialization version"); + if (version < 1u || version > 7u) 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); diff --git a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp index 805a366f9..b0ced9a19 100644 --- a/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp +++ b/core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrOpcode.hpp @@ -261,6 +261,7 @@ enum class IrOpcode : std::uint16_t { FPRsq64, FPSqrt64, FPTrigPreop64, + FPDot2F32F16, ConvertF32F64, ConvertF64F32, ConvertF64S32, diff --git a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp index 7057ea930..bede0e6e5 100644 --- a/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp +++ b/core/shader/recompiler/IntermediateRepresentation/src/IrOpcode.cpp @@ -302,6 +302,7 @@ constexpr std::array(IrOpcode::Count)> Meta makeMeta("FPRsq64", U64, U64), makeMeta("FPSqrt64", U64, U64), makeMeta("FPTrigPreop64", U64, U64, U32), + makeMeta("FPDot2F32F16", U32, U32, U32, U32), makeMeta("ConvertF32F64", F32, U64), makeMeta("ConvertF64F32", U64, F32), makeMeta("ConvertF64S32", U64, U32), diff --git a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp index 3d9c39d7f..befe1cce7 100644 --- a/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp +++ b/core/shader/recompiler/Optimization/include/Optimization/ShaderStageInputInfo.hpp @@ -2,6 +2,7 @@ #define CORE_SHADER_RECOMPILIER_OPTIMIZATION_INCLUDE_OPTIMIZATION_SHADERSTAGEINPUTINFO_HPP #include "IntermediateRepresentation/IrMetadata.hpp" +#include "Recompiler.hpp" #include #include #include @@ -183,6 +184,7 @@ struct ShaderPixelInputInfo { bool psSampleShading = false; bool psEarlyZ = false; bool psExecuteOnNoop = false; + ConservativeZExport psConservativeZExport = ConservativeZExport::AnyZ; ShaderStageRuntime stage; bool HasPositionInput() const { diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp index 2544e10eb..a30ed3d3a 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaMemoryOpDecoder.cpp @@ -434,6 +434,9 @@ RdnaOperand scalarDestination(std::uint32_t code) { RdnaOperand scalarDescriptorBase(std::uint32_t reg, std::uint32_t registerCount, const char* reason) { const auto operand = scalarSource(reg); + if (registerCount == 2u && operand.kind == RdnaOperandKind::VccLo) { + return operand; + } if (operand.kind != RdnaOperandKind::ScalarRegister || reg + (registerCount - 1u) > 105u) { throw std::runtime_error(reason); } diff --git a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp index 09ef291c1..de3c85f62 100644 --- a/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp +++ b/core/shader/recompiler/RdnaDecoder/src/RdnaScalarOpDecoder.cpp @@ -328,7 +328,7 @@ RdnaInstruction DecodeRdnaSop1(std::uint32_t programCounter, std::span interpolatorSettings; @@ -119,6 +125,7 @@ struct ShaderPixelStageInfo { bool sampleMaskExportEnable; bool earlyZ; bool executeOnNoop; + ConservativeZExport conservativeZExport; std::array targetOutputMode; std::array targetExportMapping; }; diff --git a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp index b0e61840a..6080354b9 100644 --- a/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp +++ b/core/shader/recompiler/SpirvBackend/include/SpirvBackend/SpirvEmitterInstructions.hpp @@ -195,6 +195,7 @@ std::uint32_t EmitFPRcp64(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPRsq64(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPSqrt64(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitFPTrigPreop64(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1); +std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2); std::uint32_t EmitConvertF32F64(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertF64F32(SpirvEmitterState& state, std::uint32_t arg0); std::uint32_t EmitConvertF64S32(SpirvEmitterState& state, std::uint32_t arg0); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp index c2090afca..1dec262eb 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvAlu/SpirvAluEmitterFloat64.cpp @@ -442,6 +442,96 @@ std::uint32_t EmitFPTrigPreop64(SpirvEmitterState& state, std::uint32_t arg0, st return FromF64(state, Exact(state, spv::OpFMul, Exact(state, spv::OpFMul, mantissa, scale), ConstantF64(state, 0x1a70000000000000ull))); } +std::uint32_t EmitFPDot2F32F16(SpirvEmitterState& state, std::uint32_t arg0, std::uint32_t arg1, std::uint32_t arg2) { + const auto u32 = TypeU32(state); + const auto op = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, u32, lhs, ConstantU32(state, value)); }; + const auto test = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t value) { return Binary(state, opcode, TypeBool(state), lhs, ConstantU32(state, value)); }; + const auto logical = [&](spv::Op opcode, std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, opcode, TypeBool(state), lhs, rhs); }; + const auto differ = [&](std::uint32_t lhs, std::uint32_t rhs) { return Binary(state, spv::OpINotEqual, TypeBool(state), lhs, rhs); }; + const auto pick = [&](std::uint32_t condition, std::uint32_t trueValue, std::uint32_t falseValue) { return Select(state, u32, condition, trueValue, falseValue); }; + struct Half { + std::uint32_t bits = 0; + std::uint32_t sign = 0; + std::uint32_t nan = 0; + std::uint32_t infinite = 0; + std::uint32_t zero = 0; + std::uint32_t value = 0; + }; + const auto half = [&](std::uint32_t packed, std::uint32_t shift) { + Half result; + result.bits = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, packed, shift), 0xffffu); + const auto magnitude = op(spv::OpBitwiseAnd, result.bits, 0x7fffu); + result.sign = op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, result.bits, 0x8000u), 16u); + result.nan = test(spv::OpUGreaterThan, magnitude, 0x7c00u); + result.infinite = test(spv::OpIEqual, magnitude, 0x7c00u); + result.zero = test(spv::OpIEqual, magnitude, 0u); + const auto subnormal = Exact(state, spv::OpFMul, Unary(state, spv::OpConvertUToF, TypeF64(state), magnitude), ConstantF64(state, 0x3e70000000000000ull)); + const auto normal = ToF64(state, Join(state, {ConstantU32(state, 0u), op(spv::OpIAdd, op(spv::OpShiftLeftLogical, magnitude, 10u), 1008u << 20u)})); + const auto unsignedBits = Split(state, FromF64(state, Select(state, TypeF64(state), test(spv::OpULessThan, magnitude, 0x400u), subnormal, normal))); + result.value = ToF64(state, Join(state, {unsignedBits.low, Binary(state, spv::OpBitwiseOr, u32, unsignedBits.high, result.sign)})); + return result; + }; + const auto aLow = half(arg0, 0u); + const auto bLow = half(arg1, 0u); + const auto aHigh = half(arg0, 16u); + const auto bHigh = half(arg1, 16u); + + const auto magnitudeC = op(spv::OpBitwiseAnd, arg2, 0x7fffffffu); + const auto signC = op(spv::OpBitwiseAnd, arg2, 0x80000000u); + const auto nanC = test(spv::OpUGreaterThan, magnitudeC, 0x7f800000u); + const auto infiniteC = test(spv::OpIEqual, magnitudeC, 0x7f800000u); + const auto flushedC = test(spv::OpULessThan, magnitudeC, 0x00800000u); + const auto highC = pick(flushedC, ConstantU32(state, 0u), op(spv::OpIAdd, op(spv::OpShiftRightLogical, magnitudeC, 3u), 896u << 20u)); + const auto lowC = pick(flushedC, ConstantU32(state, 0u), op(spv::OpShiftLeftLogical, magnitudeC, 29u)); + const auto valueC = ToF64(state, Join(state, {lowC, Binary(state, spv::OpBitwiseOr, u32, highC, signC)})); + + const auto productLow = Exact(state, spv::OpFMul, aLow.value, bLow.value); + const auto productHigh = Exact(state, spv::OpFMul, aHigh.value, bHigh.value); + const auto upper = TwoSum(state, productHigh, valueC); + const auto total = TwoSum(state, productLow, upper.high); + const auto sum = Split(state, FromF64(state, AddRoundToOdd(state, total.high, AddRoundToOdd(state, total.low, upper.low)))); + const auto exponent = op(spv::OpBitwiseAnd, op(spv::OpShiftRightLogical, sum.high, 20u), 0x7ffu); + const auto mantissa = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, sum.high, 0x000fffffu), 3u), op(spv::OpShiftRightLogical, sum.low, 29u)); + const auto base = Binary(state, spv::OpIAdd, u32, op(spv::OpShiftLeftLogical, op(spv::OpISub, exponent, 896u), 23u), mantissa); + const auto rest = op(spv::OpBitwiseAnd, sum.low, 0x1fffffffu); + const auto tie = logical(spv::OpLogicalAnd, test(spv::OpIEqual, rest, 0x10000000u), test(spv::OpINotEqual, op(spv::OpBitwiseAnd, base, 1u), 0u)); + const auto roundUp = logical(spv::OpLogicalOr, test(spv::OpUGreaterThan, rest, 0x10000000u), tie); + auto result = Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseAnd, sum.high, 0x80000000u), Binary(state, spv::OpIAdd, u32, base, pick(roundUp, ConstantU32(state, 1u), ConstantU32(state, 0u)))); + + const auto signLow = Binary(state, spv::OpBitwiseXor, u32, aLow.sign, bLow.sign); + const auto signHigh = Binary(state, spv::OpBitwiseXor, u32, aHigh.sign, bHigh.sign); + const auto zeroSign = Binary(state, spv::OpBitwiseAnd, u32, Binary(state, spv::OpBitwiseAnd, u32, signLow, signHigh), signC); + const auto zero = test(spv::OpIEqual, Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseAnd, sum.high, 0x7fffffffu), sum.low), 0u); + result = pick(zero, zeroSign, result); + + const auto notZero = [&](const Half& value) { return Unary(state, spv::OpLogicalNot, TypeBool(state), value.zero); }; + const auto infiniteProduct = [&](const Half& lhs, const Half& rhs) { + return logical(spv::OpLogicalOr, logical(spv::OpLogicalAnd, lhs.infinite, notZero(rhs)), logical(spv::OpLogicalAnd, rhs.infinite, notZero(lhs))); + }; + const auto invalidProduct = [&](const Half& lhs, const Half& rhs) { + return logical(spv::OpLogicalOr, logical(spv::OpLogicalAnd, lhs.infinite, rhs.zero), logical(spv::OpLogicalAnd, rhs.infinite, lhs.zero)); + }; + const auto infiniteLow = infiniteProduct(aLow, bLow); + const auto infiniteHigh = infiniteProduct(aHigh, bHigh); + auto invalid = logical(spv::OpLogicalOr, invalidProduct(aLow, bLow), invalidProduct(aHigh, bHigh)); + invalid = logical(spv::OpLogicalOr, invalid, logical(spv::OpLogicalAnd, logical(spv::OpLogicalAnd, infiniteLow, infiniteHigh), differ(signLow, signHigh))); + invalid = logical(spv::OpLogicalOr, invalid, logical(spv::OpLogicalAnd, logical(spv::OpLogicalAnd, infiniteLow, infiniteC), differ(signLow, signC))); + invalid = logical(spv::OpLogicalOr, invalid, logical(spv::OpLogicalAnd, logical(spv::OpLogicalAnd, infiniteHigh, infiniteC), differ(signHigh, signC))); + const auto infinite = logical(spv::OpLogicalOr, logical(spv::OpLogicalOr, infiniteLow, infiniteHigh), infiniteC); + const auto infiniteSign = pick(infiniteLow, signLow, pick(infiniteHigh, signHigh, signC)); + result = pick(infinite, op(spv::OpBitwiseOr, infiniteSign, 0x7f800000u), result); + result = pick(invalid, ConstantU32(state, 0xffc00000u), result); + + const auto quietHalf = [&](const Half& value) { + return Binary(state, spv::OpBitwiseOr, u32, op(spv::OpBitwiseOr, value.sign, 0x7fc00000u), op(spv::OpShiftLeftLogical, op(spv::OpBitwiseAnd, value.bits, 0x3ffu), 13u)); + }; + result = pick(nanC, op(spv::OpBitwiseOr, arg2, 0x00400000u), result); + result = pick(bHigh.nan, quietHalf(bHigh), result); + result = pick(aHigh.nan, quietHalf(aHigh), result); + result = pick(bLow.nan, quietHalf(bLow), result); + return pick(aLow.nan, quietHalf(aLow), result); +} + std::uint32_t EmitConvertF32F64(SpirvEmitterState& state, std::uint32_t arg0) { const auto converted = Unary(state, spv::OpBitcast, TypeU32(state), Unary(state, spv::OpFConvert, TypeF32(state), ToF64(state, arg0))); const auto cls = Classify(state, arg0); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp index c05dcf3df..7c55d68b8 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvFlowEmitter.cpp @@ -466,6 +466,7 @@ void EmitDirectInstruction(SpirvValueEmitContext& ctx, const IrValue& inst) { case IrOpcode::FPRsq64: return Invoke(EmitFPRsq64, ctx, inst); case IrOpcode::FPSqrt64: return Invoke(EmitFPSqrt64, ctx, inst); case IrOpcode::FPTrigPreop64: return Invoke(EmitFPTrigPreop64, ctx, inst); + case IrOpcode::FPDot2F32F16: return Invoke(EmitFPDot2F32F16, ctx, inst); case IrOpcode::ConvertF32F64: return Invoke(EmitConvertF32F64, ctx, inst); case IrOpcode::ConvertF64F32: return Invoke(EmitConvertF64F32, ctx, inst); case IrOpcode::ConvertF64S32: return Invoke(EmitConvertF64S32, ctx, inst); diff --git a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp index 9b56e98ae..9100ce94f 100644 --- a/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp +++ b/core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvModuleSetup.cpp @@ -197,6 +197,12 @@ void DefineModule(SpirvEmitterState& state) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeOriginUpperLeft); if (state.depthVariable != 0) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthReplacing); + if (pixel.psConservativeZExport == ConservativeZExport::LessThanZ) { + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthLess); + } + if (pixel.psConservativeZExport == ConservativeZExport::GreaterThanZ) { + state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeDepthGreater); + } } if (pixel.psEarlyZ && !pixel.psPixelKillEnable && !pixel.psDepthExportEnable && !pixel.psSampleMaskExportEnable) { state.module.AddExecutionMode(state.mainFunc, spv::ExecutionModeEarlyFragmentTests); diff --git a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp index 3bf343733..4200d7197 100644 --- a/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp +++ b/core/shader/recompiler/Translation/include/Translation/TranslationContext.hpp @@ -185,6 +185,7 @@ private: bool vDivFixupF64(const RdnaInstruction& inst); bool vDot2cF32F16(const RdnaInstruction& inst); bool vDot2F32F16(const RdnaInstruction& inst); + bool float16Dot2(const RdnaInstruction& inst, const RdnaOperand& lhs, const RdnaOperand& rhs, IrU32 accumulator); bool vCubeidF32(const RdnaInstruction& inst); bool vCubescF32(const RdnaInstruction& inst); bool vCubetcF32(const RdnaInstruction& inst); diff --git a/core/shader/recompiler/Translation/src/FloatInstructions.cpp b/core/shader/recompiler/Translation/src/FloatInstructions.cpp index 958edd283..c29acfb00 100644 --- a/core/shader/recompiler/Translation/src/FloatInstructions.cpp +++ b/core/shader/recompiler/Translation/src/FloatInstructions.cpp @@ -541,32 +541,38 @@ bool TranslationContext::vLdexpF32(const RdnaInstruction& inst) { } bool TranslationContext::vDot2cF32F16(const RdnaInstruction& inst) { - RdnaOperand a = sourceAt(inst, 0u); - a.opSel = false; - a.opSelHi = true; - RdnaOperand b = sourceAt(inst, 1u); - b.opSel = false; - b.opSelHi = true; - const IrF32 aLow = readF16LaneAsF32(a, false); - const IrF32 aHigh = readF16LaneAsF32(a, true); - const IrF32 bLow = readF16LaneAsF32(b, false); - const IrF32 bHigh = readF16LaneAsF32(b, true); - const IrF32 accumulator = readMixF32(inst.destination); - const IrF32 low(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&aLow.Value(), &bLow.Value(), &accumulator.Value()})); - const IrF32 result(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&aHigh.Value(), &bHigh.Value(), &low.Value()})); - writeOperand(inst.destination, &result.Value()); - return true; + RdnaOperand lhs = sourceAt(inst, 0u); + lhs.opSel = false; + lhs.opSelHi = true; + RdnaOperand rhs = sourceAt(inst, 1u); + rhs.opSel = false; + rhs.opSelHi = true; + return float16Dot2(inst, lhs, rhs, readU32(accumulatorOperand(inst))); } bool TranslationContext::vDot2F32F16(const RdnaInstruction& inst) { - const IrF32 aLow = readF16LaneAsF32(sourceAt(inst, 0u), false, true); - const IrF32 aHigh = readF16LaneAsF32(sourceAt(inst, 0u), true, true); - const IrF32 bLow = readF16LaneAsF32(sourceAt(inst, 1u), false, true); - const IrF32 bHigh = readF16LaneAsF32(sourceAt(inst, 1u), true, true); - IrValue* accumulator = readOperand(sourceAt(inst, 2u), IrType::F32); - const IrF32 low(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&aLow.Value(), &bLow.Value(), accumulator})); - const IrF32 result(ir.Emit(IrOpcode::FPFma32, IrType::F32, {&aHigh.Value(), &bHigh.Value(), &low.Value()})); - writeOperand(inst.destination, &result.Value()); + return float16Dot2(inst, sourceAt(inst, 0u), sourceAt(inst, 1u), readU32(sourceAt(inst, 2u))); +} + +bool TranslationContext::float16Dot2(const RdnaInstruction& inst, const RdnaOperand& lhs, const RdnaOperand& rhs, IrU32 accumulator) { + const auto packed = [&](const RdnaOperand& operand) { + const IrU32 source = readF16SourceBits(operand); + const auto half = [&](bool highLane) { + const bool selectHigh = highLane ? operand.opSelHi : operand.opSel; + IrU32 bits(selectHigh ? ir.ShiftRightLogical(source.Value(), ir.Constant(16u)) : ir.BitwiseAnd(source.Value(), ir.Constant(0xffffu))); + if (highLane ? operand.negateHi : operand.negate) { + bits = IrU32(ir.BitwiseXor(bits.Value(), ir.Constant(0x8000u))); + } + return bits; + }; + return IrU32(ir.BitwiseOr(half(false).Value(), ir.ShiftLeftLogical(half(true).Value(), ir.Constant(16u)))); + }; + const IrU32 a = packed(lhs); + const IrU32 b = packed(rhs); + const IrU32 result(ir.Emit(IrOpcode::FPDot2F32F16, IrType::U32, {&a.Value(), &b.Value(), &accumulator.Value()})); + RdnaOperand destination = inst.destination; + destination.clamp = false; + writeOperand(destination, &ir.BitCastF32(result.Value())); return true; } diff --git a/core/shader/recompiler/Translation/src/MemoryInstructions/ScalarBufferInstructions.cpp b/core/shader/recompiler/Translation/src/MemoryInstructions/ScalarBufferInstructions.cpp index d97478e6b..5ae223245 100644 --- a/core/shader/recompiler/Translation/src/MemoryInstructions/ScalarBufferInstructions.cpp +++ b/core/shader/recompiler/Translation/src/MemoryInstructions/ScalarBufferInstructions.cpp @@ -58,9 +58,13 @@ void TranslationContext::TranslateCodeTableLoad(const RdnaInstruction& instructi IrU32 low(ir.Constant(static_cast(table.values.back()))); IrU32 high(ir.Constant(static_cast(table.values.back() >> 32u))); for (std::size_t entry = table.values.size() - 1u; entry-- > 0u;) { - auto& matches = ir.IEqual(index.Value(), ir.Constant(static_cast(entry * 8u))); + auto& matches = ir.IEqual(index.Value(), ir.Constant(static_cast(entry * instruction.dataDwordCount * 4u))); low = IrU32(ir.Select(matches, ir.Constant(static_cast(table.values[entry])), low.Value())); - high = IrU32(ir.Select(matches, ir.Constant(static_cast(table.values[entry] >> 32u)), high.Value())); + if (instruction.dataDwordCount == 2u) high = IrU32(ir.Select(matches, ir.Constant(static_cast(table.values[entry] >> 32u)), high.Value())); + } + if (instruction.dataDwordCount == 1u) { + writeRawU32(instruction.destination, low); + return; } writeU32Pair(instruction.destination, {low, high}); } diff --git a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp index 32887b3bc..81327c49f 100644 --- a/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp +++ b/core/shader/recompiler/Translation/src/ShaderInputInfoBuilder.cpp @@ -137,6 +137,7 @@ ShaderStageInputInfo BuildShaderStageInputInfo(ShaderStageKind stage, const Gues pixelStorage.psSampleShading = pixel.sampleShading; pixelStorage.psEarlyZ = pixel.earlyZ; pixelStorage.psExecuteOnNoop = pixel.executeOnNoop; + pixelStorage.psConservativeZExport = pixel.conservativeZExport; ShaderStageInputInfo result; result.pixel = &pixelStorage; return result; diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 7f378e259..1a9a9572a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -18,8 +18,10 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 - [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. @@ -29,14 +31,20 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 +- [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 - [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 +- [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 - [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` - [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` - [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 - [sceAgcSetSubmitMode](../../core/libs/prx/libSceAgc/Misc/src/Suspend.cpp) (libSceAgc) - mode values unknown; only 0 is accepted +- [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 - [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 @@ -44,6 +52,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 - [sceKernelInternalMemoryGetModuleSegmentInfo](../../core/libs/prx/libkernel/Module/src/Module.cpp) (libkernel) - unknown signature @@ -65,20 +74,24 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [sceAudioOutGetLastOutputTime](../../core/libs/prx/libSceAudioOut/src/AudioOut.cpp) (libSceAudioOut) - signature and behaviour taken from the [shadPS4](https://github.com/shadps4-emu/shadPS4) and [fpPS4](https://github.com/red-prig/fpPS4) reimplementations of the PS4 library, not confirmed on a PS5 title: the time is the process time at which `sceAudioOutOutput` or `sceAudioOutOutputs` accepted the port's last block, not the time the block reached the device -- [AOWqIYsgVHs](../../core/libs/prx/libSceContentExport/Export.cpp) (libSceContentExport) - unknown name, signature - [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 +- [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 (the value PPSA14632 passes) and a second initialization throw. The minimum memory size is unknown and the block is not used, as no endpoint is implemented - AudioIn, NpSessionSignaling and PlayerInvitationDialog 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 - [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 - [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 - [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 -- [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 +- [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 -- [sceHttpParseResponseHeader](../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - unknown signature +- [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 - [sceHttp2GetMemoryPoolStats](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) - unknown signature - [sceNetGetMemoryPoolStats](../../core/libs/prx/libSceNet/Export.cpp) (libSceNet) - unknown signature @@ -87,6 +100,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 - [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 @@ -94,6 +108,7 @@ Throughout the project, every function at every stage either **does exactly what - [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. - [_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) ### Functional @@ -115,15 +130,20 @@ Throughout the project, every function at every stage either **does exactly what - [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. +- [DB_SHADER_CONTROL.CONSERVATIVE_Z_EXPORT](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderInputState.cpp) (libSceAgcDriver) becomes `DepthLess`/`DepthGreater`. For a pixel shader that breaks the promise, the host driver decides whether its interpolated Z is depth-tested before shading; what the console does then was not measured. - The Linux placement of the `--to-intel` stubs in the [ELF patcher](../../core/relinker/elfpatcher/src/linux/LinuxElfPatcher.cpp) is covered only by a synthetic test. - The libc [SSE4a trap emulation](../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it. +- [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. - [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. - [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): reading it from the frame's Xing/Info, VBRI or Fraunhofer data is not implemented. +- [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. - [libScePlayerInvitationDialog](../../core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp) simulates dialog completion without displaying UI or sending invitations; its parameter ABI remains unverified. - [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. +- [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 - [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. diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 7cb429298..999714177 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -85,6 +85,10 @@ Windows PowerShell: .\app.exe ``` +### System fonts + +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. + ## 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.