diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 575971f71..d019b3bf7 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -232,6 +232,19 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_collate_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestCollate.cpp) +target_include_directories(guest_collate_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_collate_tests PRIVATE libc) +configure_windows_unwind(guest_collate_tests) +set_target_properties(guest_collate_tests PROPERTIES CXX_EXTENSIONS OFF) +if(BUILD_TESTING) + set_target_properties(guest_collate_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_collate COMMAND guest_collate_tests) + if(WIN32) + set_tests_properties(guest_collate PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + add_test_executable(application_heap_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/ApplicationHeap.cpp) target_include_directories(application_heap_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(application_heap_tests PRIVATE libc) diff --git a/core/libs/prx/libc/include/GuestLocale.hpp b/core/libs/prx/libc/include/GuestLocale.hpp index 1e46c9210..45e29df3f 100644 --- a/core/libs/prx/libc/include/GuestLocale.hpp +++ b/core/libs/prx/libc/include/GuestLocale.hpp @@ -35,6 +35,29 @@ struct Implementation { const char* name; }; +struct String { + std::uint64_t reserved; + union { + char buffer[16]; + char* pointer; + }; + std::uint64_t size; + std::uint64_t capacity; +}; + +struct CollateFacet { + Facet base; + const void* collation; + const void* wideCollation; +}; + +struct CollateVtable { + FacetVtable facet; + int (APS5_VABI *compare)(const CollateFacet* self, const char* first1, const char* last1, const char* first2, const char* last2); + String* (APS5_VABI *transform)(String* result, const CollateFacet* self, const char* first, const char* last); + std::int64_t (APS5_VABI *hash)(const CollateFacet* self, const char* first, const char* last); +}; + struct IosBase { const void* vtable; std::int64_t standardStream; @@ -59,6 +82,13 @@ static_assert(offsetof(IosBase, storage) == 0x28); static_assert(offsetof(IosBase, callbacks) == 0x30); static_assert(offsetof(IosBase, locale) == 0x38); static_assert(sizeof(IosBase) == 0x40); +static_assert(offsetof(String, buffer) == 0x8); +static_assert(offsetof(String, size) == 0x18); +static_assert(sizeof(String) == 0x28); +static_assert(offsetof(CollateFacet, collation) == 0x10); +static_assert(sizeof(CollateFacet) == 0x20); +static_assert(offsetof(CollateVtable, compare) == 0x20); +static_assert(offsetof(CollateVtable, hash) == 0x30); static_assert(alignof(LocinfoStorage) == 8); } diff --git a/core/libs/prx/libc/src/LocaleSupport.cpp b/core/libs/prx/libc/src/LocaleSupport.cpp index abc4717b0..941df32b3 100644 --- a/core/libs/prx/libc/src/LocaleSupport.cpp +++ b/core/libs/prx/libc/src/LocaleSupport.cpp @@ -13,12 +13,19 @@ #include #include #include +#include +#include #include "prx/libc/include/General.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/GuestLocale.hpp" #include "SceTypes.hpp" +extern "C" { +void* APS5_VABI _Znwm_nid_postfix(std::size_t size); +void APS5_VABI _ZdlPv_nid_postfix(void* pointer); +} + namespace { std::mutex g_localeInitMutex; @@ -71,6 +78,71 @@ const GuestLocale::FacetVtable g_copiedLocaleVtable{DestroyCopiedLocale, Destroy GuestLocale::Facet* g_classicFacets[1]{}; GuestLocale::Implementation g_classicLocale{{&g_localeVtable, 1, 0}, g_classicFacets, 1, 0, false, "C"}; +constexpr std::size_t GuestStringInlineCapacity = 15; +constexpr std::size_t GuestStringLargeAllocation = 0x1000; + +void ValidateCharacterRange(const char* first, const char* last, const char* function) { + if ((first == nullptr) != (last == nullptr) || last < first) throw std::invalid_argument(std::string(function) + ": invalid character range"); +} + +void APS5_VABI DestroyCollate(GuestLocale::Facet*) {} + +void APS5_VABI DeleteCollate(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("collate delete: null facet"); + _ZdlPv_nid_postfix(self); +} + +void APS5_VABI RetainFacet(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("facet retain: null facet"); + std::atomic_ref(self->references).fetch_add(1); +} + +GuestLocale::Facet* APS5_VABI ReleaseFacet(GuestLocale::Facet* self) { + if (self == nullptr) throw std::invalid_argument("facet release: null facet"); + return std::atomic_ref(self->references).fetch_sub(1) == 1 ? self : nullptr; +} + +int APS5_VABI CollateCompare(const GuestLocale::CollateFacet*, const char* first1, const char* last1, const char* first2, const char* last2) { + ValidateCharacterRange(first1, last1, "collate::do_compare"); + ValidateCharacterRange(first2, last2, "collate::do_compare"); + const auto length1 = static_cast(last1 - first1); + const auto length2 = static_cast(last2 - first2); + const auto common = std::min(length1, length2); + const int order = common == 0 ? 0 : std::memcmp(first1, first2, common); + if (order != 0) return order < 0 ? -1 : 1; + if (length1 == length2) return 0; + return length1 < length2 ? -1 : 1; +} + +GuestLocale::String* APS5_VABI CollateTransform(GuestLocale::String* result, const GuestLocale::CollateFacet*, const char* first, const char* last) { + if (result == nullptr) throw std::invalid_argument("collate::do_transform: null result"); + ValidateCharacterRange(first, last, "collate::do_transform"); + const auto length = static_cast(last - first); + if (length != 0 && std::memchr(first, 0, length) != nullptr) throw std::runtime_error("collate::do_transform: embedded null characters are not supported"); + if (length + 1 >= GuestStringLargeAllocation) throw std::runtime_error("collate::do_transform: results of 4095 characters or more are not supported"); + char* data = result->buffer; + std::size_t capacity = GuestStringInlineCapacity; + if (length > GuestStringInlineCapacity) { + capacity = length; + data = static_cast(_Znwm_nid_postfix(capacity + 1)); + } + if (length != 0) std::memcpy(data, first, length); + data[length] = 0; + if (data != result->buffer) result->pointer = data; + result->size = length; + result->capacity = capacity; + return result; +} + +std::int64_t APS5_VABI CollateHash(const GuestLocale::CollateFacet*, const char* first, const char* last) { + ValidateCharacterRange(first, last, "collate::do_hash"); + std::uint64_t hash = 0xcbf29ce484222325; + for (; first != last; ++first) hash = (hash ^ static_cast(*first)) * 0x100000001b3; + return static_cast(hash); +} + +const GuestLocale::CollateVtable g_collateVtable{{DestroyCollate, DeleteCollate, RetainFacet, ReleaseFacet}, CollateCompare, CollateTransform, CollateHash}; + constexpr std::array MakeClassificationTable() { std::array table{}; for (unsigned int value = 0; value < 256; ++value) { @@ -189,8 +261,14 @@ void APS5_VABI _ZNSt7collateIwE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(Gu NotImplemented_nid_no_patch(__func__); } -void APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet**, const GuestLocale::Implementation*) { - NotImplemented_nid_no_patch(__func__); +std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale) { + if (facet != nullptr && *facet == nullptr) { + if (locale == nullptr || *locale == nullptr || (*locale)->name == nullptr || std::strcmp((*locale)->name, "C") != 0) throw std::invalid_argument("collate::_Getcat: only the C locale is supported"); + auto* collate = static_cast(_Znwm_nid_postfix(sizeof(GuestLocale::CollateFacet))); + *collate = {{&g_collateVtable.facet, 0, 0}, nullptr, nullptr}; + *facet = &collate->base; + } + return 1; } void APS5_VABI _ZNSt8_LocinfoC1EPKc_nid_postfix(GuestLocale::LocinfoStorage* self, const char* localeName) { diff --git a/core/libs/tests/GuestCollate.cpp b/core/libs/tests/GuestCollate.cpp new file mode 100644 index 000000000..ed4b1976d --- /dev/null +++ b/core/libs/tests/GuestCollate.cpp @@ -0,0 +1,156 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/GuestLocale.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +extern GuestLocale::Implementation* _ZSt21_sceLibcClassicLocale_nid_postfix; +std::size_t APS5_VABI _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(GuestLocale::Facet** facet, const GuestLocale::Implementation* const* locale); +} + +namespace { + +std::size_t allocations = 0; +std::size_t frees = 0; +std::size_t lastSize = 0; +void* lastAllocation = nullptr; +void* lastFree = nullptr; + +void require(bool condition) { + if (!condition) std::abort(); +} + +template +void reject(TAction action) { + bool rejected = false; + try { action(); } catch (const std::exception&) { rejected = true; } + require(rejected); +} + +void* APS5_VABI allocate(std::size_t bytes) { + ++allocations; + lastSize = bytes; + lastAllocation = std::malloc(bytes); + return lastAllocation; +} + +void APS5_VABI release(void* pointer) { + ++frees; + lastFree = pointer; + std::free(pointer); +} + +void* APS5_VABI allocateZeroed(std::size_t, std::size_t) { std::abort(); } +void* APS5_VABI reallocate(void*, std::size_t) { std::abort(); } +void* APS5_VABI align(std::size_t, std::size_t) { std::abort(); } +void* APS5_VABI realign(void*, std::size_t, std::size_t) { std::abort(); } +int APS5_VABI posixAlign(void**, std::size_t, std::size_t) { std::abort(); } + +const GuestLocale::CollateVtable& Vtable(const GuestLocale::Facet* facet) { + return *reinterpret_cast(facet->vtable); +} + +int Compare(const GuestLocale::CollateFacet* facet, const std::string& left, const std::string& right) { + return Vtable(&facet->base).compare(facet, left.data(), left.data() + left.size(), right.data(), right.data() + right.size()); +} + +std::uint64_t Hash(const GuestLocale::CollateFacet* facet, const std::string& text) { + return static_cast(Vtable(&facet->base).hash(facet, text.data(), text.data() + text.size())); +} + +} + +int main() { + const std::array api{reinterpret_cast(&allocate), reinterpret_cast(&release), reinterpret_cast(&allocateZeroed), reinterpret_cast(&reallocate), reinterpret_cast(&align), reinterpret_cast(&realign), reinterpret_cast(&posixAlign)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + + const GuestLocale::Implementation* const* classic = &_ZSt21_sceLibcClassicLocale_nid_postfix; + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(nullptr, classic) == 1); + GuestLocale::Facet existing{}; + GuestLocale::Facet* facet = &existing; + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet == &existing && allocations == 0); + + GuestLocale::Implementation french = *_ZSt21_sceLibcClassicLocale_nid_postfix; + french.name = "fr_FR"; + const GuestLocale::Implementation* frenchPointer = &french; + facet = nullptr; + reject([&] { _ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, &frenchPointer); }); + require(facet == nullptr && allocations == 0); + + require(_ZNSt7collateIcE7_GetcatEPPKNSt6locale5facetEPKS1__nid_postfix(&facet, classic) == 1); + require(facet != nullptr && allocations == 1 && lastSize == sizeof(GuestLocale::CollateFacet) && lastAllocation == facet); + const auto* collate = reinterpret_cast(facet); + require(facet->references == 0 && collate->collation == nullptr && collate->wideCollation == nullptr); + + require(Compare(collate, "abc", "abd") == -1); + require(Compare(collate, "abd", "abc") == 1); + require(Compare(collate, "a", "c") == -1); + require(Compare(collate, "abc", "abc") == 0); + require(Compare(collate, "ab", "abc") == -1); + require(Compare(collate, "abc", "ab") == 1); + require(Compare(collate, "", "") == 0); + require(Compare(collate, "", "a") == -1); + require(Compare(collate, "\x80", "a") == 1); + require(Compare(collate, std::string("a\0", 2), "a") == 1); + require(Compare(collate, std::string("a\0b", 3), std::string("a\0c", 3)) == -1); + require(Vtable(facet).compare(collate, nullptr, nullptr, nullptr, nullptr) == 0); + const char text[] = "abc"; + reject([&] { Vtable(facet).compare(collate, text + 2, text, text, text + 1); }); + reject([&] { Vtable(facet).compare(collate, nullptr, text, text, text + 1); }); + + require(Hash(collate, "") == 0xcbf29ce484222325ull); + require(Hash(collate, "a") == 0xaf63dc4c8601ec8cull); + require(Hash(collate, "foobar") == 0x85944171f73967e8ull); + require(Hash(collate, "\xff") == ((0xcbf29ce484222325ull ^ 0xffull) * 0x100000001b3ull)); + reject([&] { Vtable(facet).hash(collate, text + 1, text); }); + + GuestLocale::String result; + std::memset(&result, 0xcd, sizeof(result)); + const std::string shortText = "hello"; + require(Vtable(facet).transform(&result, collate, shortText.data(), shortText.data() + shortText.size()) == &result); + require(result.reserved == 0xcdcdcdcdcdcdcdcdull && result.size == 5 && result.capacity == 15 && std::strcmp(result.buffer, "hello") == 0); + require(allocations == 1); + + std::memset(&result, 0xcd, sizeof(result)); + require(Vtable(facet).transform(&result, collate, nullptr, nullptr) == &result); + require(result.size == 0 && result.capacity == 15 && result.buffer[0] == 0); + + const std::string fifteen(15, 'q'); + require(Vtable(facet).transform(&result, collate, fifteen.data(), fifteen.data() + fifteen.size()) == &result); + require(result.size == 15 && result.capacity == 15 && std::string(result.buffer) == fifteen && allocations == 1); + + const std::string longText = "collation of a longer text"; + require(Vtable(facet).transform(&result, collate, longText.data(), longText.data() + longText.size()) == &result); + require(allocations == 2 && lastSize == longText.size() + 1 && result.pointer == lastAllocation); + require(result.size == longText.size() && result.capacity == longText.size() && std::string(result.pointer) == longText); + std::free(result.pointer); + + const std::string largest(4094, 'z'); + require(Vtable(facet).transform(&result, collate, largest.data(), largest.data() + largest.size()) == &result); + require(allocations == 3 && lastSize == 4095 && result.capacity == 4094 && std::string(result.pointer) == largest); + std::free(result.pointer); + + const std::string tooLong(4095, 'z'); + reject([&] { Vtable(facet).transform(&result, collate, tooLong.data(), tooLong.data() + tooLong.size()); }); + const std::string embedded("a\0b", 3); + reject([&] { Vtable(facet).transform(&result, collate, embedded.data(), embedded.data() + embedded.size()); }); + reject([&] { Vtable(facet).transform(nullptr, collate, text, text + 1); }); + require(allocations == 3); + + Vtable(facet).facet.retain(facet); + Vtable(facet).facet.retain(facet); + require(facet->references == 2); + require(Vtable(facet).facet.release(facet) == nullptr && facet->references == 1); + require(Vtable(facet).facet.release(facet) == facet && facet->references == 0); + Vtable(facet).facet.destroy(facet); + require(frees == 0); + Vtable(facet).facet.deleteObject(facet); + require(frees == 1 && lastFree == collate); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 645d976a4..8a521f867 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -169,6 +169,7 @@ Throughout the project, every function at every stage either **does exactly what - [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 +- [collate::_Getcat](../../core/libs/prx/libc/src/LocaleSupport.cpp) (libc) builds the facet for the C locale only and throws for any other locale name. The console's `libc.prx` stores pointers to its collation state tables at offsets 0x10 and 0x18 of the facet; ours leaves them null, as only the facet's own functions read them. `do_transform` throws for input with embedded null characters, which the console drops depending on the output buffer size, and for results of 4095 characters or more, whose string storage uses the guest's over-aligned large allocation. - [Thread exit destructors](../../core/libs/prx/libSceLibcInternal/Export.cpp) (libSceLibcInternal) run when a libkernel thread finishes; on the main or a host thread only `_sceLibcInternalThreadDtors` runs them, as `exit` does, else they are dropped - [sceFontOpenFontSet](../../core/libs/prx/libSceFont/src/SystemFont.cpp) (libSceFont) opens only the primary file of a system font set. Characters the console takes from the set's other files (Hangul, Thai and Arabic in Japanese and Chinese sets, Arabic in European sets) are reported as unsupported glyphs. The console file names are those of the PS4 firmware; the names of the PS5-only JG2 sets (`0x1A......`) are unknown, so they use the SSTJpPro files. When a console file is missing, an openly licensed substitute (Noto Sans, Noto Sans Mono, Noto Sans Thai, Noto Sans CJK) is loaded instead. It is not the system font: its metrics differ, so text width, line height and wrapping differ from the console. System font faces are loaded by FreeType in host memory, not in the font memory the title passes to the library: FreeType needs about 1 MiB per CJK face, which the title's font memory is not sized for. - [Font writing lines](../../core/libs/prx/libSceFont/src/WritingLine.cpp) (libSceFont) - `sceFontCreateWritingLine`, `sceFontWritingLineWritesOrder`, `sceFontWritingLineRefersRenderStep`, `sceFontWritingLineGetRenderMetrics` and `sceFontWritingLineClear` follow [KytyPS5](https://github.com/KytyPS5/KytyPS5/blob/main/src/libs/libFont.cpp): runs are placed left to right, each at the line's advance so far, with no spacing or adjustment. Only horizontal and left-to-right lines, writing attribute 0 and non-null run metrics are modelled; other values, the metrics of an empty line and unknown or null handles throw, since no reference gives their result or error code. `sceFontWritingLineGetOrderingSpace` still throws (KytyPS5's values are placeholders). Lines are allocated on the host, not in the font memory