diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 24d3d6794..a76a7dbfe 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -106,6 +106,8 @@ add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) add_executable(application_heap_tests EXCLUDE_FROM_ALL tests/ApplicationHeap.cpp prx/libc/src/ApplicationHeap.cpp) target_include_directories(application_heap_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) set_target_properties(application_heap_tests PROPERTIES CXX_EXTENSIONS OFF) +target_link_libraries(application_heap_tests PRIVATE libc) +configure_windows_unwind(application_heap_tests) add_executable(guest_locale_tests EXCLUDE_FROM_ALL tests/GuestLocale.cpp) target_include_directories(guest_locale_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) @@ -113,6 +115,11 @@ set_target_properties(guest_locale_tests PROPERTIES CXX_EXTENSIONS OFF) target_link_libraries(guest_locale_tests PRIVATE libc) configure_windows_unwind(guest_locale_tests) +add_executable(guest_rtc_tests EXCLUDE_FROM_ALL tests/GuestRtc.cpp) +target_include_directories(guest_rtc_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +set_target_properties(guest_rtc_tests PROPERTIES CXX_EXTENSIONS OFF) +target_link_libraries(guest_rtc_tests PRIVATE libSceRtc) + if(MINGW) foreach(runtimeLinkFlag IN ITEMS -static-libgcc -static-libstdc++ -static) string(REPLACE "${runtimeLinkFlag}" "" CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS}") diff --git a/core/libs/prx/libSceImeDialog/Export.cpp b/core/libs/prx/libSceImeDialog/Export.cpp index ebd440cf2..84fe1c7e2 100644 --- a/core/libs/prx/libSceImeDialog/Export.cpp +++ b/core/libs/prx/libSceImeDialog/Export.cpp @@ -1,59 +1,97 @@ +#include #include #include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int StatusNone = 0; +constexpr int StatusRunning = 1; +constexpr int StatusFinished = 2; +constexpr std::uint32_t EndStatusOk = 0; +constexpr std::uint32_t EndStatusAborted = 2; +constexpr int ErrorNotOpened = static_cast(0x80bc0002u); + +std::atomic status{StatusNone}; +std::atomic polls{0}; +std::atomic endStatus{EndStatusOk}; + +} + extern "C" { int APS5_VABI sceImeDialogAbort(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (status.load() != StatusRunning) return ErrorNotOpened; + endStatus = EndStatusAborted; + status = StatusFinished; + return 0; } int APS5_VABI sceImeDialogGetPanelPositionAndForm(PositionAndForm* form) { - (void)form; - NotImplemented_nid_no_patch(__func__); - return 0; + if (form == nullptr) return ErrorNotOpened; + *form = PositionAndForm{}; + form->width = 1920; + form->height = 1080; + return 0; } int APS5_VABI sceImeDialogGetPanelSize(const Param* param, uint32_t* width, uint32_t* height) { - (void)param; - (void)width; - (void)height; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)param; + if (width != nullptr) *width = 1920; + if (height != nullptr) *height = 1080; + return 0; } int APS5_VABI sceImeDialogGetPanelSizeExtended(const Param* param, const ExtendedParam* extended, uint32_t* width, uint32_t* height) { - (void)param; - (void)extended; - (void)width; - (void)height; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)extended; + return sceImeDialogGetPanelSize(param, width, height); } int APS5_VABI sceImeDialogGetResult(Result* result) { - (void)result; - NotImplemented_nid_no_patch(__func__); - return 0; + std::fprintf(stderr, "[ime] GetResult (status %d)\n", status.load()); + if (result == nullptr || status.load() != StatusFinished) return ErrorNotOpened; + *result = Result{}; + result->endstatus = endStatus.load(); + return 0; } int APS5_VABI sceImeDialogGetStatus(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (status.load() == StatusRunning && ++polls > 1) status = StatusFinished; + static std::atomic traced{0}; + const int count = traced.fetch_add(1); + if (count < 8 || count % 2000 == 0) std::fprintf(stderr, "[ime] GetStatus #%d -> %d\n", count, status.load()); + return status.load(); } int APS5_VABI sceImeDialogInit(const Param* param, const ExtendedParam* extended) { - (void)param; - (void)extended; - NotImplemented_nid_no_patch(__func__); - return 0; + (void)extended; + static std::atomic reported{0}; + bool filled = false; + if (param != nullptr && param->input_text_buffer != nullptr && param->max_text_length != 0 && param->input_text_buffer[0] == u'\0') { + static constexpr char16_t defaultText[] = u"Slayer"; + std::uint32_t length = 0; + while (defaultText[length] != u'\0' && length < param->max_text_length) { + param->input_text_buffer[length] = defaultText[length]; + ++length; + } + param->input_text_buffer[length] = u'\0'; + filled = true; + } + std::fprintf(stderr, "[ime] Init #%d\n", reported.load()); + if (reported.fetch_add(1) == 0) std::fprintf(stderr, "[ime] dialog requested (max text length %u): finishing at once with %s\n", param != nullptr ? param->max_text_length : 0u, filled ? "a default text" : "the text unchanged"); + polls = 0; + endStatus = EndStatusOk; + status = StatusRunning; + return 0; } int APS5_VABI sceImeDialogTerm(void) { - NotImplemented_nid_no_patch(__func__); - return 0; + std::fprintf(stderr, "[ime] Term (status %d)\n", status.load()); + if (status.load() == StatusNone) return ErrorNotOpened; + status = StatusNone; + return 0; } } diff --git a/core/libs/prx/libSceRtc/Export.cpp b/core/libs/prx/libSceRtc/Export.cpp index 4065bf0bc..827c70395 100644 --- a/core/libs/prx/libSceRtc/Export.cpp +++ b/core/libs/prx/libSceRtc/Export.cpp @@ -1,185 +1,339 @@ +#include #include #include +#include +#include +#include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +namespace { + +constexpr int SCE_RTC_ERROR_INVALID_POINTER = static_cast(0x80B50002); +constexpr int SCE_RTC_ERROR_INVALID_VALUE = static_cast(0x80B50003); +constexpr int SCE_RTC_ERROR_BAD_PARSE = static_cast(0x80B50007); +constexpr int SCE_RTC_ERROR_INVALID_YEAR = static_cast(0x80B50008); +constexpr int SCE_RTC_ERROR_INVALID_MONTH = static_cast(0x80B50009); +constexpr int SCE_RTC_ERROR_INVALID_DAY = static_cast(0x80B5000A); +constexpr int SCE_RTC_ERROR_INVALID_HOUR = static_cast(0x80B5000B); +constexpr int SCE_RTC_ERROR_INVALID_MINUTE = static_cast(0x80B5000C); +constexpr int SCE_RTC_ERROR_INVALID_SECOND = static_cast(0x80B5000D); +constexpr int SCE_RTC_ERROR_INVALID_MICROSECOND = static_cast(0x80B5000E); + +constexpr std::int64_t TICKS_PER_SECOND = 1000000; +constexpr std::int64_t TICKS_PER_MINUTE = 60 * TICKS_PER_SECOND; +constexpr std::int64_t TICKS_PER_HOUR = 60 * TICKS_PER_MINUTE; +constexpr std::int64_t TICKS_PER_DAY = 24 * TICKS_PER_HOUR; +constexpr std::int64_t UNIX_EPOCH_DAYS = 719162; +constexpr std::int64_t WIN32_EPOCH_DAYS = 584388; +constexpr std::uint64_t UNIX_EPOCH_TICK = UNIX_EPOCH_DAYS * TICKS_PER_DAY; +constexpr std::uint64_t WIN32_EPOCH_TICK = WIN32_EPOCH_DAYS * TICKS_PER_DAY; +constexpr std::uint64_t MAX_TICK = 3652059ull * TICKS_PER_DAY - 1; + +bool isLeapYear(std::int64_t year) { + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; +} + +int daysInMonth(std::int64_t year, int month) { + static constexpr int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + return month == 2 && isLeapYear(year) ? 29 : days[month - 1]; +} + +std::int64_t daysFromCivil(std::int64_t year, unsigned month, unsigned day) { + year -= month <= 2; + const std::int64_t era = (year >= 0 ? year : year - 399) / 400; + const auto yearOfEra = static_cast(year - era * 400); + const unsigned dayOfYear = (153 * (month > 2 ? month - 3 : month + 9) + 2) / 5 + day - 1; + const unsigned dayOfEra = yearOfEra * 365 + yearOfEra / 4 - yearOfEra / 100 + dayOfYear; + return era * 146097 + static_cast(dayOfEra) - 719468 + UNIX_EPOCH_DAYS; +} + +void civilFromDays(std::int64_t days, std::int64_t& year, unsigned& month, unsigned& day) { + days -= UNIX_EPOCH_DAYS - 719468; + const std::int64_t era = (days >= 0 ? days : days - 146096) / 146097; + const auto dayOfEra = static_cast(days - era * 146097); + const unsigned yearOfEra = (dayOfEra - dayOfEra / 1460 + dayOfEra / 36524 - dayOfEra / 146096) / 365; + const unsigned dayOfYear = dayOfEra - (365 * yearOfEra + yearOfEra / 4 - yearOfEra / 100); + const unsigned shiftedMonth = (5 * dayOfYear + 2) / 153; + day = dayOfYear - (153 * shiftedMonth + 2) / 5 + 1; + month = shiftedMonth < 10 ? shiftedMonth + 3 : shiftedMonth - 9; + year = static_cast(yearOfEra) + era * 400 + (month <= 2); +} + +int validate(const RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (time->year < 1 || time->year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + if (time->month < 1 || time->month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + if (time->day < 1 || time->day > daysInMonth(time->year, time->month)) return SCE_RTC_ERROR_INVALID_DAY; + if (time->hour > 23) return SCE_RTC_ERROR_INVALID_HOUR; + if (time->minute > 59) return SCE_RTC_ERROR_INVALID_MINUTE; + if (time->second > 59) return SCE_RTC_ERROR_INVALID_SECOND; + if (time->microsecond >= TICKS_PER_SECOND) return SCE_RTC_ERROR_INVALID_MICROSECOND; + return 0; +} + +std::uint64_t toTick(const RtcDateTime& time) { + return static_cast(daysFromCivil(time.year, time.month, time.day)) * TICKS_PER_DAY + + time.hour * TICKS_PER_HOUR + time.minute * TICKS_PER_MINUTE + time.second * TICKS_PER_SECOND + time.microsecond; +} + +RtcDateTime fromTick(std::uint64_t tick) { + std::int64_t year = 0; + unsigned month = 0; + unsigned day = 0; + civilFromDays(static_cast(tick / TICKS_PER_DAY), year, month, day); + const std::uint64_t timeOfDay = tick % TICKS_PER_DAY; + return RtcDateTime{ + static_cast(year), static_cast(month), static_cast(day), + static_cast(timeOfDay / TICKS_PER_HOUR), static_cast(timeOfDay % TICKS_PER_HOUR / TICKS_PER_MINUTE), + static_cast(timeOfDay % TICKS_PER_MINUTE / TICKS_PER_SECOND), static_cast(timeOfDay % TICKS_PER_SECOND)}; +} + +std::uint64_t currentTick() { + const auto now = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + return UNIX_EPOCH_TICK + static_cast(now); +} + +std::int64_t localOffsetSeconds(std::uint64_t utcTick) { + const auto seconds = static_cast((static_cast(utcTick) - static_cast(UNIX_EPOCH_TICK)) / TICKS_PER_SECOND); + std::tm local{}; +#ifdef _WIN32 + if (localtime_s(&local, &seconds) != 0) return 0; +#else + if (!localtime_r(&seconds, &local)) return 0; +#endif + const std::int64_t localSeconds = (daysFromCivil(local.tm_year + 1900, static_cast(local.tm_mon + 1), static_cast(local.tm_mday)) - UNIX_EPOCH_DAYS) * 86400 + + local.tm_hour * 3600 + local.tm_min * 60 + local.tm_sec; + return localSeconds - static_cast(seconds); +} + +int addTicks(RtcTick* dst, const RtcTick* src, std::int64_t count, std::int64_t unit) { + if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; + if (count != 0 && (count > std::numeric_limits::max() / unit || count < std::numeric_limits::min() / unit)) return SCE_RTC_ERROR_INVALID_VALUE; + const std::int64_t delta = count * unit; + if (delta < 0 ? src->tick < static_cast(-(delta + 1)) + 1 : MAX_TICK - src->tick < static_cast(delta)) return SCE_RTC_ERROR_INVALID_VALUE; + dst->tick = src->tick + static_cast(delta); + return 0; +} + +int addMonths(RtcTick* dst, const RtcTick* src, std::int64_t months) { + if (!dst || !src) return SCE_RTC_ERROR_INVALID_POINTER; + RtcDateTime time = fromTick(src->tick); + const std::int64_t monthIndex = time.year * 12 + (time.month - 1) + months; + const std::int64_t year = monthIndex / 12; + if (monthIndex < 12 || year > 9999) return SCE_RTC_ERROR_INVALID_VALUE; + time.year = static_cast(year); + time.month = static_cast(monthIndex % 12 + 1); + const int lastDay = daysInMonth(time.year, time.month); + if (time.day > lastDay) time.day = static_cast(lastDay); + dst->tick = toTick(time); + return 0; +} + +bool parseDigits(const char*& cursor, int count, int& value) { + value = 0; + for (int index = 0; index < count; ++index, ++cursor) { + if (*cursor < '0' || *cursor > '9') return false; + value = value * 10 + (*cursor - '0'); + } + return true; +} + +} + extern "C" { int APS5_VABI sceRtcCheckValid(const RtcDateTime* time) { - (void)time; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick* local_time, RtcTick* utc) { - (void)local_time; - (void)utc; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick* utc, RtcTick* local_time) { - (void)utc; - (void)local_time; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { - (void)date_time; - (void)utc; - (void)time_zone_minutes; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { - (void)time; - (void)time_zone_minutes; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { - (void)time; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick) { - (void)tick; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { - (void)tick; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day) { - (void)year; - (void)month; - (void)day; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetTick(const RtcDateTime* time, RtcTick* tick) { - (void)time; - (void)tick; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetTickResolution(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { - (void)time; - (void)seconds; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32_time) { - (void)time; - (void)win32_time; - NotImplemented_nid_no_patch(__func__); - return 0; + return validate(time); } int APS5_VABI sceRtcIsLeapYear(int year) { - (void)year; - NotImplemented_nid_no_patch(__func__); - return 0; + if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; + return isLeapYear(year) ? 1 : 0; } -int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { - (void)utc; - (void)date_time; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRtcGetDaysInMonth(int year, int month) { + if (year < 1) return SCE_RTC_ERROR_INVALID_YEAR; + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + return daysInMonth(year, month); +} + +int APS5_VABI sceRtcGetDayOfWeek(int year, int month, int day) { + if (year < 1 || year > 9999) return SCE_RTC_ERROR_INVALID_YEAR; + if (month < 1 || month > 12) return SCE_RTC_ERROR_INVALID_MONTH; + if (day < 1 || day > daysInMonth(year, month)) return SCE_RTC_ERROR_INVALID_DAY; + return static_cast((daysFromCivil(year, static_cast(month), static_cast(day)) + 1) % 7); +} + +int APS5_VABI sceRtcGetTickResolution(void) { + return static_cast(TICKS_PER_SECOND); +} + +int APS5_VABI sceRtcGetTick(const RtcDateTime* time, RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + if (const int result = validate(time); result != 0) return result; + tick->tick = toTick(*time); + return 0; } int APS5_VABI sceRtcSetTick(RtcDateTime* time, const RtcTick* tick) { - (void)time; - (void)tick; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!time || !tick) return SCE_RTC_ERROR_INVALID_POINTER; + if (tick->tick > MAX_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + *time = fromTick(tick->tick); + return 0; +} + +int APS5_VABI sceRtcGetCurrentTick(RtcTick* tick) { + if (!tick) return SCE_RTC_ERROR_INVALID_POINTER; + tick->tick = currentTick(); + return 0; +} + +int APS5_VABI sceRtcGetCurrentNetworkTick(RtcTick* tick) { + return sceRtcGetCurrentTick(tick); +} + +int APS5_VABI sceRtcGetCurrentClock(RtcDateTime* time, int time_zone_minutes) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick tick{currentTick()}; + if (const int result = addTicks(&tick, &tick, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + *time = fromTick(tick.tick); + return 0; +} + +int APS5_VABI sceRtcGetCurrentClockLocalTime(RtcDateTime* time) { + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + const std::uint64_t utc = currentTick(); + *time = fromTick(utc + static_cast(localOffsetSeconds(utc) * TICKS_PER_SECOND)); + return 0; +} + +int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick* utc, RtcTick* local_time) { + if (!utc || !local_time) return SCE_RTC_ERROR_INVALID_POINTER; + return addTicks(local_time, utc, localOffsetSeconds(utc->tick), TICKS_PER_SECOND); +} + +int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick* local_time, RtcTick* utc) { + if (!local_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + const std::int64_t offset = localOffsetSeconds(local_time->tick - static_cast(localOffsetSeconds(local_time->tick) * TICKS_PER_SECOND)); + return addTicks(utc, local_time, -offset, TICKS_PER_SECOND); +} + +int APS5_VABI sceRtcGetTime_t(const RtcDateTime* time, int64_t* seconds) { + if (!seconds) return SCE_RTC_ERROR_INVALID_POINTER; + if (const int result = validate(time); result != 0) return result; + const std::uint64_t tick = toTick(*time); + if (tick < UNIX_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + *seconds = static_cast((tick - UNIX_EPOCH_TICK) / TICKS_PER_SECOND); + return 0; } int APS5_VABI sceRtcSetTime_t(RtcDateTime* time, int64_t seconds) { - (void)time; - (void)seconds; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (seconds < 0 || static_cast(seconds) > (MAX_TICK - UNIX_EPOCH_TICK) / TICKS_PER_SECOND) return SCE_RTC_ERROR_INVALID_VALUE; + *time = fromTick(UNIX_EPOCH_TICK + static_cast(seconds) * TICKS_PER_SECOND); + return 0; +} + +int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime* time, uint64_t* win32_time) { + if (!win32_time) return SCE_RTC_ERROR_INVALID_POINTER; + if (const int result = validate(time); result != 0) return result; + const std::uint64_t tick = toTick(*time); + if (tick < WIN32_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + *win32_time = (tick - WIN32_EPOCH_TICK) * 10; + return 0; } int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime* time, uint64_t win32_time) { - (void)time; - (void)win32_time; - NotImplemented_nid_no_patch(__func__); - return 0; + if (!time) return SCE_RTC_ERROR_INVALID_POINTER; + if (win32_time / 10 > MAX_TICK - WIN32_EPOCH_TICK) return SCE_RTC_ERROR_INVALID_VALUE; + *time = fromTick(WIN32_EPOCH_TICK + win32_time / 10); + return 0; } -int APS5_VABI sceRtcTickAddDays(RtcTick* dst, const RtcTick* src, int32_t days) { - (void)dst; - (void)src; - (void)days; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRtcFormatRFC3339(char* date_time, const RtcTick* utc, int time_zone_minutes) { + if (!date_time || !utc) return SCE_RTC_ERROR_INVALID_POINTER; + RtcTick local{}; + if (const int result = addTicks(&local, utc, time_zone_minutes, TICKS_PER_MINUTE); result != 0) return result; + const RtcDateTime time = fromTick(local.tick); + int written = std::snprintf(date_time, 23, "%04u-%02u-%02uT%02u:%02u:%02u.%02u", time.year, time.month, time.day, time.hour, time.minute, time.second, time.microsecond / 10000); + if (time_zone_minutes == 0) { + std::snprintf(date_time + written, 2, "Z"); + } else { + const int offset = time_zone_minutes < 0 ? -time_zone_minutes : time_zone_minutes; + std::snprintf(date_time + written, 7, "%c%02d:%02d", time_zone_minutes < 0 ? '-' : '+', offset / 60, offset % 60); + } + return 0; } -int APS5_VABI sceRtcTickAddHours(RtcTick* dst, const RtcTick* src, int32_t hours) { - (void)dst; - (void)src; - (void)hours; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcTickAddMicroseconds(RtcTick* dst, const RtcTick* src, int64_t usec) { - (void)dst; - (void)src; - (void)usec; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcTickAddMinutes(RtcTick* dst, const RtcTick* src, int64_t minutes) { - (void)dst; - (void)src; - (void)minutes; - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI sceRtcTickAddSeconds(RtcTick* dst, const RtcTick* src, int64_t seconds) { - (void)dst; - (void)src; - (void)seconds; - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI sceRtcParseRFC3339(RtcTick* utc, const char* date_time) { + if (!utc || !date_time) return SCE_RTC_ERROR_INVALID_POINTER; + const char* cursor = date_time; + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!parseDigits(cursor, 4, year) || *cursor++ != '-' || !parseDigits(cursor, 2, month) || *cursor++ != '-' || !parseDigits(cursor, 2, day)) return SCE_RTC_ERROR_BAD_PARSE; + if (*cursor != 'T' && *cursor != 't' && *cursor != ' ') return SCE_RTC_ERROR_BAD_PARSE; + ++cursor; + if (!parseDigits(cursor, 2, hour) || *cursor++ != ':' || !parseDigits(cursor, 2, minute) || *cursor++ != ':' || !parseDigits(cursor, 2, second)) return SCE_RTC_ERROR_BAD_PARSE; + std::uint32_t microsecond = 0; + if (*cursor == '.') { + ++cursor; + std::uint32_t scale = 100000; + if (*cursor < '0' || *cursor > '9') return SCE_RTC_ERROR_BAD_PARSE; + for (; *cursor >= '0' && *cursor <= '9'; ++cursor, scale /= 10) microsecond += static_cast(*cursor - '0') * scale; + } + std::int64_t offsetMinutes = 0; + if (*cursor == 'Z' || *cursor == 'z') { + ++cursor; + } else if (*cursor == '+' || *cursor == '-') { + const int sign = *cursor++ == '-' ? -1 : 1; + int offsetHours = 0, offsetMinute = 0; + if (!parseDigits(cursor, 2, offsetHours) || *cursor++ != ':' || !parseDigits(cursor, 2, offsetMinute)) return SCE_RTC_ERROR_BAD_PARSE; + offsetMinutes = sign * (offsetHours * 60 + offsetMinute); + } else { + return SCE_RTC_ERROR_BAD_PARSE; + } + if (*cursor != 0) return SCE_RTC_ERROR_BAD_PARSE; + const RtcDateTime time{static_cast(year), static_cast(month), static_cast(day), + static_cast(hour), static_cast(minute), static_cast(second), microsecond}; + if (const int result = validate(&time); result != 0) return result; + const RtcTick local{toTick(time)}; + return addTicks(utc, &local, -offsetMinutes, TICKS_PER_MINUTE); } int APS5_VABI sceRtcTickAddTicks(RtcTick* dst, const RtcTick* src, int64_t ticks) { - (void)dst; - (void)src; - (void)ticks; - NotImplemented_nid_no_patch(__func__); - return 0; + return addTicks(dst, src, ticks, 1); +} + +int APS5_VABI sceRtcTickAddMicroseconds(RtcTick* dst, const RtcTick* src, int64_t usec) { + return addTicks(dst, src, usec, 1); +} + +int APS5_VABI sceRtcTickAddSeconds(RtcTick* dst, const RtcTick* src, int64_t seconds) { + return addTicks(dst, src, seconds, TICKS_PER_SECOND); +} + +int APS5_VABI sceRtcTickAddMinutes(RtcTick* dst, const RtcTick* src, int64_t minutes) { + return addTicks(dst, src, minutes, TICKS_PER_MINUTE); +} + +int APS5_VABI sceRtcTickAddHours(RtcTick* dst, const RtcTick* src, int32_t hours) { + return addTicks(dst, src, hours, TICKS_PER_HOUR); +} + +int APS5_VABI sceRtcTickAddDays(RtcTick* dst, const RtcTick* src, int32_t days) { + return addTicks(dst, src, days, TICKS_PER_DAY); } int APS5_VABI sceRtcTickAddWeeks(RtcTick* dst, const RtcTick* src, int32_t weeks) { - (void)dst; - (void)src; - (void)weeks; - NotImplemented_nid_no_patch(__func__); - return 0; + return addTicks(dst, src, weeks, 7 * TICKS_PER_DAY); +} + +int APS5_VABI sceRtcTickAddMonths(RtcTick* dst, const RtcTick* src, int32_t months) { + return addMonths(dst, src, months); +} + +int APS5_VABI sceRtcTickAddYears(RtcTick* dst, const RtcTick* src, int16_t years) { + return addMonths(dst, src, static_cast(years) * 12); } } diff --git a/core/libs/prx/libSceVideoOut/src/PadInput.cpp b/core/libs/prx/libSceVideoOut/src/PadInput.cpp index 66fde9da7..6631fb5a9 100644 --- a/core/libs/prx/libSceVideoOut/src/PadInput.cpp +++ b/core/libs/prx/libSceVideoOut/src/PadInput.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include @@ -27,6 +28,10 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { publish(); return; } + // APS5_NO_PAD_INPUT=1 keeps a measurement run from reacting to keys or mouse buttons that reach + // its window (a stray press advances the title into another stage). + static const bool ignoreInput = std::getenv("APS5_NO_PAD_INPUT") != nullptr; + if (ignoreInput && event.type == SDL_MOUSEWHEEL) return; if (event.type == SDL_MOUSEWHEEL) { int direction = (event.wheel.y > 0) - (event.wheel.y < 0); if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) direction = -direction; @@ -44,6 +49,7 @@ void PadInput::HandleEvent(const SDL_Event& event, DisplayWindow& window) { const bool keyboard = event.type == SDL_KEYDOWN || event.type == SDL_KEYUP; const bool mouse = event.type == SDL_MOUSEBUTTONDOWN || event.type == SDL_MOUSEBUTTONUP; if (!keyboard && !mouse) return; + if (ignoreInput) return; if (keyboard && event.key.repeat != 0) return; const bool down = event.type == SDL_KEYDOWN || event.type == SDL_MOUSEBUTTONDOWN; for (std::size_t index = 0; index < Pad::InputMapping.size(); ++index) { diff --git a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp index e892c5828..c0036f286 100644 --- a/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp +++ b/core/libs/prx/libSceVideoOut/src/VideoOutDriver.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include @@ -42,6 +44,16 @@ public: request->outputHandle = info.handle; request->flipMode = static_cast(info.mode); request->flipArg = info.argument; + // Debug aid: APS5_FLIP_QUEUE_WAIT=1 waits for room instead of failing the flip, so a + // presenter stall keeps the process alive for a debugger (reported every 5 s). + static const bool waitWhenFull = std::getenv("APS5_FLIP_QUEUE_WAIT") != nullptr; + if (waitWhenFull) { + int waited = 0; + while (queue->reservations.load() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + if (++waited % 500 == 0) std::fprintf(stderr, "[flip] queue full for %d s; waiting (APS5_FLIP_QUEUE_WAIT)\n", waited / 100); + } + } std::lock_guard queueLock(queue->mutex); if (queue->failure) std::rethrow_exception(queue->failure); require(!queue->stopping, "flip during shutdown"); @@ -123,8 +135,10 @@ void FlipRequest::GpuReady(const std::shared_ptr& frameT queue->changed.notify_all(); // The worker is done once the request is queued: hardware does not stall the command processor // on a flip, and the title observes completion through flipPendingNum, which processFlip drops - // after presenting. A presentation failure reaches the worker through ReportFailure at its next - // packet. Debug aid: APS5_SYNC_FLIP=1 parks the worker until the presenter is done, as before. + // once the presentation is queued behind the frame's GPU work (up to APS5_FLIP_INFLIGHT + // presentations may still be executing then, see Driver::Present). A presentation failure + // reaches the worker through ReportFailure at its next packet. Debug aid: APS5_SYNC_FLIP=1 + // parks the worker until the presenter is done, as before. static const bool syncFlip = std::getenv("APS5_SYNC_FLIP") != nullptr; if (!syncFlip) return; std::unique_lock lock(cfg->mutex); diff --git a/core/libs/prx/libSceVideoOut/tests/Flip.cpp b/core/libs/prx/libSceVideoOut/tests/Flip.cpp index e67aa6a13..99af68044 100644 --- a/core/libs/prx/libSceVideoOut/tests/Flip.cpp +++ b/core/libs/prx/libSceVideoOut/tests/Flip.cpp @@ -238,6 +238,34 @@ void testPresentation(bool expectUnavailable) { 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() { + const int handle = sceVideoOutOpen(255, 0, 0, nullptr); + auto cfg = VideoOutDriver::Get().GetConfig(handle); + std::array, 2> allocations{std::vector(6 * 65536 + 65535), std::vector(6 * 65536 + 65535)}; + std::array buffers{}; + for (std::size_t i = 0; i < buffers.size(); ++i) { + const auto storage = alignedBuffer(allocations[i]); + fillBuffer(storage, 259, 137); + buffers[i] = {storage.data(), nullptr, {nullptr, nullptr}}; + } + VideoOutBufferAttribute2 attribute{}; + sceVideoOutSetBufferAttribute2(&attribute, 0x8000000000000000ull, 0, 259, 137, 0, 0, 0); + sceVideoOutRegisterBuffers2(handle, 0, 0, buffers.data(), 2, &attribute, 0, nullptr); + int64_t argument = 1000; + for (int index : {0, 1, 0, 1}) sceVideoOutSubmitFlip(handle, index, 1, ++argument); + std::unique_lock lock(cfg->mutex); + check(cfg->vblankCond.wait_for(lock, std::chrono::seconds(15), [&] { return cfg->failure || cfg->flipStatus.count == 4; }), "back-to-back presentations did not complete"); + if (cfg->failure) std::rethrow_exception(cfg->failure); + check(cfg->flipStatus.flipPendingNum == 0 && cfg->bufferPending[0] == 0 && cfg->bufferPending[1] == 0, "back-to-back flips left pending counts"); + check(cfg->flipStatus.flipArg == argument && cfg->flipStatus.currentBuffer == 1, "back-to-back flip status is wrong"); + lock.unlock(); + sceVideoOutUnregisterBuffers(handle, 0); + sceVideoOutClose(handle); + LibcRunShutdown_nid_postfix(); +} + } int main(int argc, char** argv) { @@ -245,6 +273,7 @@ int main(int argc, char** argv) { if (argc == 2 && std::string(argv[1]) == "decode") testDecode(); 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]) == "unavailable") testPresentation(true); else testLifetime(argc == 2 && std::string(argv[1]) == "reopen"); std::puts("VideoOut flip tests passed"); diff --git a/core/libs/prx/libc/include/CpuTopology.hpp b/core/libs/prx/libc/include/CpuTopology.hpp new file mode 100644 index 000000000..3150ed400 --- /dev/null +++ b/core/libs/prx/libc/include/CpuTopology.hpp @@ -0,0 +1,119 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_CPUTOPOLOGY_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_CPUTOPOLOGY_HPP + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#endif + +// Hybrid CPU layout (Intel P/E cores) from the OS: which logical processors belong to the lowest and +// the highest efficiency class. Non-hybrid machines report no layout and every pin is a no-op unless +// a mask is given explicitly. Header-only so libkernel and the AGC driver share it without a link. +namespace CpuTopology { + +struct Layout { + std::uint64_t process = 0; + std::uint64_t efficient = 0; + std::uint64_t performant = 0; + bool hybrid = false; +}; + +inline const Layout& Get() { + static const Layout layout = [] { + Layout result{}; +#ifdef _WIN32 + DWORD_PTR processMask = 0; + DWORD_PTR systemMask = 0; + if (GetProcessAffinityMask(GetCurrentProcess(), &processMask, &systemMask)) result.process = static_cast(processMask); + DWORD length = 0; + GetLogicalProcessorInformationEx(RelationProcessorCore, nullptr, &length); + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER || length == 0) return result; + std::vector buffer(length); + if (!GetLogicalProcessorInformationEx(RelationProcessorCore, reinterpret_cast(buffer.data()), &length)) return result; + std::uint64_t byClass[256] = {}; + bool seen[256] = {}; + for (DWORD offset = 0; offset + 8 <= length;) { + const auto* entry = reinterpret_cast(buffer.data() + offset); + if (entry->Size == 0 || offset + entry->Size > length) break; + if (entry->Relationship == RelationProcessorCore && entry->Processor.GroupCount >= 1 && entry->Processor.GroupMask[0].Group == 0) { + const unsigned cls = entry->Processor.EfficiencyClass; + byClass[cls] |= static_cast(entry->Processor.GroupMask[0].Mask); + seen[cls] = true; + } + offset += entry->Size; + } + int lowest = -1; + int highest = -1; + for (int cls = 0; cls < 256; ++cls) { + if (!seen[cls]) continue; + if (lowest < 0) lowest = cls; + highest = cls; + } + if (lowest < 0 || lowest == highest) return result; + result.efficient = byClass[lowest]; + result.performant = byClass[highest]; + result.hybrid = result.efficient != 0 && result.performant != 0; +#endif + return result; + }(); + return layout; +} + +// A hex mask from the environment, 0 when unset or empty. +inline std::uint64_t MaskFromEnvironment(const char* name) { + const char* text = std::getenv(name); + if (text == nullptr || *text == '\0') return 0; + return std::strtoull(text, nullptr, 16); +} + +inline bool Trace() { + static const bool trace = std::getenv("APS5_TRACE_AFFINITY") != nullptr; + return trace; +} + +inline unsigned long ThreadId(void* handle) { +#ifdef _WIN32 + return handle != nullptr ? GetThreadId(static_cast(handle)) : GetCurrentThreadId(); +#else + (void)handle; + return 0; +#endif +} + +// Restricts the thread (`nullptr` is the calling thread) to `mask` clipped to the process affinity; +// returns the mask applied, 0 when nothing was. +inline std::uint64_t Pin(void* handle, std::uint64_t mask) { +#ifdef _WIN32 + const auto& layout = Get(); + if (layout.process != 0) mask &= layout.process; + if (mask == 0) return 0; + HANDLE thread = handle != nullptr ? static_cast(handle) : GetCurrentThread(); + return SetThreadAffinityMask(thread, static_cast(mask)) != 0 ? mask : 0; +#else + (void)handle; + (void)mask; + return 0; +#endif +} + +// Pin plus the APS5_TRACE_AFFINITY=1 line naming the thread. +inline std::uint64_t PinTraced(const char* role, void* handle, std::uint64_t mask) { + const auto applied = Pin(handle, mask); + if (Trace()) std::fprintf(stderr, "[affinity] %s tid=%lu mask=0x%llx%s\n", role, ThreadId(handle), static_cast(mask), applied != 0 ? "" : " (not applied)"); + return applied; +} + +// Helper threads (deferred release, mirror refresh) are pinned only when APS5_HELPER_AFFINITY_MASK +// names their cores. +inline void PinHelperThread(const char* role) { + static const std::uint64_t mask = MaskFromEnvironment("APS5_HELPER_AFFINITY_MASK"); + if (mask != 0) PinTraced(role, nullptr, mask); +} + +} + +#endif diff --git a/core/libs/prx/libc/src/ApplicationHeap.cpp b/core/libs/prx/libc/src/ApplicationHeap.cpp index ffd62cc5a..81e17250d 100644 --- a/core/libs/prx/libc/src/ApplicationHeap.cpp +++ b/core/libs/prx/libc/src/ApplicationHeap.cpp @@ -142,7 +142,7 @@ void* ApplicationHeapReallocate_nid_no_patch(void* pointer, std::size_t bytes) { ApplicationHeapFree_nid_no_patch(pointer); return nullptr; } - const auto reallocate = callback(2); + const auto reallocate = callback(3); CallbackScope scope; return requireAllocation(reallocate(pointer, bytes)); } @@ -158,7 +158,7 @@ void* ApplicationHeapAlign_nid_no_patch(std::size_t alignment, std::size_t bytes void* ApplicationHeapCalloc_nid_no_patch(std::size_t count, std::size_t bytes) { if (bytes != 0 && count > std::numeric_limits::max() / bytes) throw std::length_error("application heap: calloc size overflow"); - const auto calloc = callback(3); + const auto calloc = callback(2); CallbackScope scope; return requireAllocation(calloc(count, bytes)); } diff --git a/core/libs/prx/libc/src/exception/Runtime.cpp b/core/libs/prx/libc/src/exception/Runtime.cpp index c26045b6b..c04c5a1cf 100644 --- a/core/libs/prx/libc/src/exception/Runtime.cpp +++ b/core/libs/prx/libc/src/exception/Runtime.cpp @@ -31,17 +31,18 @@ static void RawLog(const char* format, ...) { Header* primary = Primary(globals.caught); const char* typeName = primary->type ? primary->type->name() : nullptr; const char* what = nullptr; - if (primary->adjusted) { + void* exception = primary->adjusted; + if (exception && primary->type && Match(&typeid(std::exception), primary->type, exception)) { struct VtableLayout { std::ptrdiff_t offset; const void* type; void (*destroy)(void*); void (*del)(void*); const char* (*whatFn)(const void*); }; - const void* vtable = *static_cast(primary->adjusted); + const void* vtable = *static_cast(exception); auto* layout = reinterpret_cast(static_cast(vtable) - offsetof(VtableLayout, destroy)); #ifdef _WIN32 if (!primary->_pad) { using GuestWhat = const char* (__attribute__((sysv_abi)) *)(const void*); - what = reinterpret_cast(layout->whatFn)(primary->adjusted); + what = reinterpret_cast(layout->whatFn)(exception); } else #endif - what = layout->whatFn(primary->adjusted); + what = layout->whatFn(exception); } int status = 0; char* demangled = abi::__cxa_demangle(typeName, nullptr, nullptr, &status); diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index cf688c154..fc0208d6b 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -35,6 +35,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Rtld.cpp ${CMAKE_CURRENT_SOURCE_DIR}/System/src/Process.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Time/Time.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/Time/TimedWait.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/AppMetadata.cpp ${CMAKE_CURRENT_SOURCE_DIR}/AppMetadata/src/ParamJsonParser.cpp ) diff --git a/core/libs/prx/libkernel/Equeue/Equeue.cpp b/core/libs/prx/libkernel/Equeue/Equeue.cpp index 4ccab694a..ece17ea7a 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.cpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.cpp @@ -41,7 +41,7 @@ void KernelEqueuePrivate::Close() { } } m_events.clear(); - m_cond.notify_all(); + m_cond.NotifyAll(); } void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t nowNs) { @@ -113,8 +113,7 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros if (m_closed) { return EQUEUE_ERROR_EBADF; } - const auto deadline = std::chrono::steady_clock::now() + - std::chrono::microseconds(micros); + const std::uint64_t deadline = TimedWait::DeadlineNanos(micros); for (;;) { TriggerExpiredTimers(MonotonicNs()); int ret = 0; @@ -152,21 +151,16 @@ int KernelEqueuePrivate::WaitForEvents(KernelEvent* ev, int num, uint32_t micros return EQUEUE_ERROR_EBADF; } if (micros == 0) { - m_cond.wait(lock); + m_cond.Wait(lock); } else { uint32_t timerWait = 0; const bool hasTimer = NextTimerWaitMicros(MonotonicNs(), &timerWait); - const auto now = std::chrono::steady_clock::now(); + const std::uint64_t now = TimedWait::NowNanos(); if (now >= deadline) { return 0; } - const auto remaining = std::chrono::duration_cast( - deadline - now - ).count(); - const auto waitUs = hasTimer - ? std::min(static_cast(remaining), timerWait) - : static_cast(remaining); - m_cond.wait_for(lock, std::chrono::microseconds(waitUs)); + const std::uint64_t timerDeadline = now + static_cast(timerWait) * 1000ULL; + m_cond.WaitUntil(lock, hasTimer ? std::min(deadline, timerDeadline) : deadline); } } } @@ -190,7 +184,7 @@ int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) { } else { m_events.push_back(event); } - m_cond.notify_one(); + m_cond.NotifyOne(); return EQUEUE_OK; } @@ -212,7 +206,7 @@ int KernelEqueuePrivate::TriggerEvent(uintptr_t ident, int16_t filter, void* tri } else { it->triggered = true; } - m_cond.notify_one(); + m_cond.NotifyOne(); return EQUEUE_OK; } diff --git a/core/libs/prx/libkernel/Equeue/Equeue.hpp b/core/libs/prx/libkernel/Equeue/Equeue.hpp index 0082a1edd..cae407344 100644 --- a/core/libs/prx/libkernel/Equeue/Equeue.hpp +++ b/core/libs/prx/libkernel/Equeue/Equeue.hpp @@ -1,7 +1,6 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_EQUEUE_EQUEUE_HPP #define CORE_LIBS_PRX_LIBKERNEL_EQUEUE_EQUEUE_HPP -#include #include #include #include @@ -12,6 +11,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" static constexpr int16_t EVFILT_USER = -11; static constexpr int16_t EVFILT_VIDEO_OUT = -13; @@ -76,7 +76,7 @@ private: std::list m_events; std::mutex m_mutex; - std::condition_variable_any m_cond; + TimedWait::Condition m_cond; std::string m_name; KernelEqueue m_handle; bool m_closed = false; diff --git a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp index 970658765..7d4ebe2ea 100644 --- a/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp +++ b/core/libs/prx/libkernel/EventFlag/src/EventFlag.cpp @@ -3,8 +3,8 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include -#include #include #include #include @@ -29,7 +29,7 @@ static constexpr uint32_t EVF_WAITMODE_CLEAR_PAT = 0x20; struct KernelEventFlagPrivate { std::mutex _mutex; - std::condition_variable _changed; + TimedWait::Condition _changed; std::string _name; uint64_t _pattern = 0; bool _multipleWaiters = false; @@ -83,7 +83,7 @@ int APS5_VABI sceKernelDeleteEventFlag(KernelEventFlag ef) { if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; ef->_deleted = true; if (ef->_waiters != 0) { - ef->_changed.notify_all(); + ef->_changed.NotifyAll(); return SCE_OK; } } @@ -97,7 +97,7 @@ int APS5_VABI sceKernelSetEventFlag(KernelEventFlag ef, uint64_t bit_pattern) { if (ef->_deleted) return SCE_KERNEL_ERROR_ESRCH; ef->_pattern |= bit_pattern; if (TraceSync()) std::fprintf(stderr, "[evf] set %p '%s' |=0x%llx -> 0x%llx\n", static_cast(ef), ef->_name.c_str(), static_cast(bit_pattern), static_cast(ef->_pattern)); - ef->_changed.notify_all(); + ef->_changed.NotifyAll(); return SCE_OK; } @@ -116,7 +116,7 @@ int APS5_VABI sceKernelCancelEventFlag(KernelEventFlag ef, uint64_t set_pattern, if (num_wait_threads) *num_wait_threads = ef->_waiters; ef->_pattern = set_pattern; ++ef->_cancelGeneration; - ef->_changed.notify_all(); + ef->_changed.NotifyAll(); return SCE_OK; } @@ -147,12 +147,11 @@ int APS5_VABI sceKernelWaitEventFlag(KernelEventFlag ef, uint64_t bit_pattern, u bool timedOut = false; const auto waitStart = std::chrono::steady_clock::now(); if (timeout) { - const auto deadline = waitStart + std::chrono::microseconds(*timeout); - timedOut = !ef->_changed.wait_until(lock, deadline, released); - const auto remaining = std::chrono::duration_cast(deadline - std::chrono::steady_clock::now()).count(); - *timeout = remaining > 0 ? static_cast(remaining) : 0; + const std::uint64_t deadline = TimedWait::DeadlineNanos(*timeout); + timedOut = !ef->_changed.WaitUntil(lock, deadline, released); + *timeout = static_cast(TimedWait::RemainingMicros(deadline)); } else { - ef->_changed.wait(lock, released); + ef->_changed.Wait(lock, released); } KernelTraceWait_nid_postfix("evf", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); --ef->_waiters; diff --git a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp index bb76c83d4..8bbe3735e 100644 --- a/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp +++ b/core/libs/prx/libkernel/Pthread/Posix/Sem.cpp @@ -3,16 +3,16 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" #include "Common.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include #include -#include #include #include // The guest sem_t is at least 8 bytes; its first word holds a pointer to the host semaphore. struct PosixSemaphore { std::mutex lock; - std::condition_variable available; + TimedWait::Condition available; unsigned int count; }; @@ -35,9 +35,9 @@ static int WaitFor(PosixSemaphore* semaphore, const KernelUseconds* usec) { std::unique_lock lock(semaphore->lock); const auto ready = [&] { return semaphore->count > 0; }; if (usec) { - if (!semaphore->available.wait_for(lock, std::chrono::microseconds(*usec), ready)) return Fail(PosixThread::GUEST_ETIMEDOUT); + if (!semaphore->available.WaitUntil(lock, TimedWait::DeadlineNanos(*usec), ready)) return Fail(PosixThread::GUEST_ETIMEDOUT); } else { - semaphore->available.wait(lock, ready); + semaphore->available.Wait(lock, ready); } --semaphore->count; return 0; @@ -99,7 +99,7 @@ int APS5_VABI sem_post_nid_postfix(void* sem) { std::lock_guard lock(semaphore->lock); ++semaphore->count; } - semaphore->available.notify_one(); + semaphore->available.NotifyOne(); return 0; } diff --git a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp index c436f91fb..c87009f13 100644 --- a/core/libs/prx/libkernel/Pthread/include/Pthread.hpp +++ b/core/libs/prx/libkernel/Pthread/include/Pthread.hpp @@ -3,6 +3,7 @@ #include #include "SceTypes.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include #include #include @@ -46,7 +47,7 @@ struct PthreadCondattrPrivate { }; struct PthreadCondPrivate { - std::condition_variable_any _cv; + TimedWait::Condition _cv; int _clockid = 0; }; diff --git a/core/libs/prx/libkernel/Pthread/src/Cond.cpp b/core/libs/prx/libkernel/Pthread/src/Cond.cpp index 2f1a2267a..23a7263dc 100644 --- a/core/libs/prx/libkernel/Pthread/src/Cond.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Cond.cpp @@ -1,7 +1,9 @@ #include "../include/Pthread.hpp" #include "prx/libc/include/General.hpp" #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include +#include #include #include #include @@ -10,7 +12,7 @@ static constexpr int SCE_OK = 0; static constexpr int SCE_KERNEL_ERROR_ENOMEM = 0x8002000C; static constexpr int SCE_KERNEL_ERROR_ETIMEDOUT = 0x8002003C; -static int WaitOn(PthreadCondPrivate* cond, PthreadMutexPrivate* mutex, std::optional timeout, const void* caller) { +static int WaitOn(PthreadCondPrivate* cond, PthreadMutexPrivate* mutex, std::optional deadlineNanos, const void* caller) { const auto self = std::this_thread::get_id(); bool timedOut = false; const auto waitStart = std::chrono::steady_clock::now(); @@ -21,13 +23,13 @@ static int WaitOn(PthreadCondPrivate* cond, PthreadMutexPrivate* mutex, std::opt mutex->_owner.store(std::thread::id{}, std::memory_order_relaxed); if (mutex->_type == MutexType::Recursive) { std::unique_lock lock(mutex->_rmtx, std::adopt_lock); - if (timeout) timedOut = cond->_cv.wait_for(lock, *timeout) == std::cv_status::timeout; - else cond->_cv.wait(lock); + if (deadlineNanos) timedOut = !cond->_cv.WaitUntil(lock, *deadlineNanos); + else cond->_cv.Wait(lock); lock.release(); } else { std::unique_lock lock(mutex->_mtx, std::adopt_lock); - if (timeout) timedOut = cond->_cv.wait_for(lock, *timeout) == std::cv_status::timeout; - else cond->_cv.wait(lock); + if (deadlineNanos) timedOut = !cond->_cv.WaitUntil(lock, *deadlineNanos); + else cond->_cv.Wait(lock); lock.release(); } mutex->_owner.store(self, std::memory_order_relaxed); @@ -69,26 +71,26 @@ int APS5_VABI scePthreadCondDestroy(PthreadCond* cond) { int APS5_VABI scePthreadCondSignal(PthreadCond* cond) { if (!cond || !*cond) throw std::runtime_error("scePthreadCondSignal: null cond"); - (*cond)->_cv.notify_one(); + (*cond)->_cv.NotifyOne(); return SCE_OK; } int APS5_VABI scePthreadCondBroadcast(PthreadCond* cond) { if (!cond || !*cond) throw std::runtime_error("scePthreadCondBroadcast: null cond"); - (*cond)->_cv.notify_all(); + (*cond)->_cv.NotifyAll(); return SCE_OK; } int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec) { if (!cond || !*cond || !mutex || !*mutex) throw std::runtime_error("scePthreadCondTimedwait: null arg"); - return WaitOn(*cond, *mutex, std::chrono::microseconds(usec), __builtin_return_address(0)); + return WaitOn(*cond, *mutex, TimedWait::DeadlineNanos(usec), __builtin_return_address(0)); } int APS5_VABI scePthreadCondSignalto(PthreadCond* cond, Pthread thread) { (void)thread; if (!cond || !*cond) throw std::runtime_error("scePthreadCondSignalto: null cond"); - (*cond)->_cv.notify_all(); + (*cond)->_cv.NotifyAll(); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp index baf24a6a9..b88b85a30 100644 --- a/core/libs/prx/libkernel/Pthread/src/Mutex.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Mutex.cpp @@ -1,7 +1,9 @@ #include "../include/Pthread.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include #include +#include #include #include @@ -98,9 +100,10 @@ int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec) if (!mutex || !*mutex) throw std::runtime_error("scePthreadMutexTimedlock: null mutex"); auto* m = *mutex; const auto tid = std::this_thread::get_id(); - const auto timeout = std::chrono::microseconds(usec); + const std::uint64_t deadline = TimedWait::DeadlineNanos(usec); if (m->_type == MutexType::Recursive) { - if (!m->_rmtx.try_lock_for(timeout)) return SCE_KERNEL_ERROR_ETIMEDOUT; + const bool locked = TimedWait::AcquireUntil(deadline, [&] { return m->_rmtx.try_lock(); }, [&](std::uint64_t micros) { return m->_rmtx.try_lock_for(std::chrono::microseconds(micros)); }); + if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; m->_owner.store(tid, std::memory_order_relaxed); ++m->_count; return SCE_OK; @@ -108,7 +111,8 @@ int APS5_VABI scePthreadMutexTimedlock(PthreadMutex* mutex, KernelUseconds usec) if (m->_type == MutexType::ErrorCheck) { if (m->_owner.load(std::memory_order_acquire) == tid) return SCE_KERNEL_ERROR_EDEADLK; } - if (!m->_mtx.try_lock_for(timeout)) return SCE_KERNEL_ERROR_ETIMEDOUT; + const bool locked = TimedWait::AcquireUntil(deadline, [&] { return m->_mtx.try_lock(); }, [&](std::uint64_t micros) { return m->_mtx.try_lock_for(std::chrono::microseconds(micros)); }); + if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; m->_owner.store(tid, std::memory_order_relaxed); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp index 424a889b7..9b4bb1713 100644 --- a/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Rwlock.cpp @@ -1,6 +1,8 @@ #include "../include/Pthread.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include +#include #include static constexpr int SCE_OK = 0; @@ -51,7 +53,8 @@ int APS5_VABI scePthreadRwlockTryrdlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedrdlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; - return lock->_lock.try_lock_shared_for(std::chrono::microseconds(usec)) ? SCE_OK : SCE_KERNEL_ERROR_ETIMEDOUT; + const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock_shared(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_shared_for(std::chrono::microseconds(micros)); }); + return locked ? SCE_OK : SCE_KERNEL_ERROR_ETIMEDOUT; } int APS5_VABI scePthreadRwlockWrlock(PthreadRwlock* rwlock) { @@ -72,7 +75,8 @@ int APS5_VABI scePthreadRwlockTrywrlock(PthreadRwlock* rwlock) { int APS5_VABI scePthreadRwlockTimedwrlock(PthreadRwlock* rwlock, KernelUseconds usec) { auto* lock = RequireRwlock(rwlock, __func__); if (OwnsWrite(lock)) return SCE_KERNEL_ERROR_EDEADLK; - if (!lock->_lock.try_lock_for(std::chrono::microseconds(usec))) return SCE_KERNEL_ERROR_ETIMEDOUT; + const bool locked = TimedWait::AcquireUntil(TimedWait::DeadlineNanos(usec), [&] { return lock->_lock.try_lock(); }, [&](std::uint64_t micros) { return lock->_lock.try_lock_for(std::chrono::microseconds(micros)); }); + if (!locked) return SCE_KERNEL_ERROR_ETIMEDOUT; lock->_writer.store(std::this_thread::get_id(), std::memory_order_release); return SCE_OK; } diff --git a/core/libs/prx/libkernel/Pthread/src/Thread.cpp b/core/libs/prx/libkernel/Pthread/src/Thread.cpp index 2cff28a93..a7cfe767e 100644 --- a/core/libs/prx/libkernel/Pthread/src/Thread.cpp +++ b/core/libs/prx/libkernel/Pthread/src/Thread.cpp @@ -1,5 +1,6 @@ #include "../include/Pthread.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/CpuTopology.hpp" #include #include #include @@ -63,6 +64,36 @@ static void ReleaseThread(PthreadPrivate* thread) { delete thread; } +// The title's job workers ("BPE JobWorkerThread CPU0".."CPU12") spin at full load on one core each. +// Pinning them to the efficiency cores of a hybrid CPU (APS5_JOB_AFFINITY=1, with the driver's +// worker pinning) measured 0 to -9 % at the video stage, so the default leaves them free; +// APS5_JOB_AFFINITY_MASK= picks the cores. +static constexpr const char* JOB_WORKER_PREFIX = "BPE JobWorkerThread"; + +static std::uint64_t JobWorkerMask() { + static const std::uint64_t mask = [] { + if (std::getenv("APS5_NO_JOB_AFFINITY") != nullptr) return std::uint64_t{0}; + const auto requested = CpuTopology::MaskFromEnvironment("APS5_JOB_AFFINITY_MASK"); + if (requested != 0) return requested; + if (std::getenv("APS5_JOB_AFFINITY") == nullptr) return std::uint64_t{0}; + const auto& layout = CpuTopology::Get(); + return layout.hybrid ? layout.efficient : std::uint64_t{0}; + }(); + return mask; +} + +static void ApplyJobAffinity(PthreadPrivate& thread, void* handle) { + if (thread.name.compare(0, std::strlen(JOB_WORKER_PREFIX), JOB_WORKER_PREFIX) != 0) return; + const auto mask = JobWorkerMask(); + if (mask == 0) return; + static std::once_flag summary; + std::call_once(summary, [mask] { + const auto& layout = CpuTopology::Get(); + std::fprintf(stderr, "[affinity] job worker threads -> 0x%llx (hybrid=%d efficient=0x%llx performant=0x%llx process=0x%llx)\n", static_cast(mask), layout.hybrid ? 1 : 0, static_cast(layout.efficient), static_cast(layout.performant), static_cast(layout.process)); + }); + CpuTopology::PinTraced(thread.name.c_str(), handle, mask); +} + struct NativeThreadArgs { std::unique_ptr guest; std::future start; @@ -90,6 +121,7 @@ static unsigned __stdcall StartNativeThread(void* opaque) { if (!self->name.empty()) { const std::wstring description(self->name.begin(), self->name.end()); SetThreadDescription(GetCurrentThread(), description.c_str()); + ApplyJobAffinity(*self, nullptr); } args->initialized.set_value(); } catch (...) { @@ -280,6 +312,7 @@ int APS5_VABI scePthreadRename(Pthread thread, const char* name) { #ifdef _WIN32 const std::wstring description(thread->name.begin(), thread->name.end()); SetThreadDescription(static_cast(thread->nativeHandle), description.c_str()); + ApplyJobAffinity(*thread, thread->nativeHandle); #endif return SCE_OK; } diff --git a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp index e7c6e49d2..631fe181d 100644 --- a/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp +++ b/core/libs/prx/libkernel/Semaphore/include/Semaphore.hpp @@ -2,13 +2,13 @@ #define CORE_LIBS_PRX_LIBKERNEL_SEMAPHORE_SEMAPHORE_HPP #include -#include #include #include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" constexpr int KERNEL_SEMA_OK = 0; constexpr int KERNEL_SEMA_ERROR_EINVAL = static_cast(0x80020016); @@ -19,7 +19,7 @@ struct KernelSemaPrivate { KernelSemaPrivate(std::int32_t initCount, std::int32_t maxCount, std::string name, bool isFifo); std::mutex mutex; - std::condition_variable condition; + TimedWait::Condition condition; std::string name; std::int32_t tokenCount; std::int32_t maxCount; diff --git a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp index 9ac0b16a5..993ccdde2 100644 --- a/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp +++ b/core/libs/prx/libkernel/Semaphore/src/Semaphore.cpp @@ -45,7 +45,7 @@ int APS5_VABI sceKernelSignalSema(KernelSema sem, int count) { return KERNEL_SEMA_ERROR_EINVAL; } sem->tokenCount += count; - sem->condition.notify_all(); + sem->condition.NotifyAll(); return KERNEL_SEMA_OK; } @@ -60,14 +60,13 @@ int APS5_VABI sceKernelWaitSema(KernelSema sem, int need, KernelUseconds* time) KernelTraceWait_nid_postfix("sema", __builtin_return_address(0), static_cast(std::chrono::duration_cast(std::chrono::steady_clock::now() - waitStart).count()), timedOut); }; if (time == nullptr) { - sem->condition.wait(lock, [&] { return sem->tokenCount >= need; }); + sem->condition.Wait(lock, [&] { return sem->tokenCount >= need; }); traceWait(false); sem->tokenCount -= need; return KERNEL_SEMA_OK; } - auto timeout = std::chrono::microseconds(*time); - bool acquired = sem->condition.wait_for(lock, timeout, [&] { return sem->tokenCount >= need; }); + const bool acquired = sem->condition.WaitUntil(lock, TimedWait::DeadlineNanos(*time), [&] { return sem->tokenCount >= need; }); traceWait(!acquired); if (!acquired) { return KERNEL_SEMA_ERROR_ETIMEDOUT; diff --git a/core/libs/prx/libkernel/Time/Time.cpp b/core/libs/prx/libkernel/Time/Time.cpp index 3465b0704..93fd8c725 100644 --- a/core/libs/prx/libkernel/Time/Time.cpp +++ b/core/libs/prx/libkernel/Time/Time.cpp @@ -1,4 +1,5 @@ #include "prx/libkernel/Time/include/Time.hpp" +#include "prx/libkernel/Time/include/TimedWait.hpp" #include "prx/libc/include/General.hpp" #include @@ -6,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -23,14 +25,7 @@ static std::uint64_t RawMonotonicNanos() { #ifdef _WIN32 - static const std::uint64_t freq = [] { - LARGE_INTEGER f{}; - QueryPerformanceFrequency(&f); - return static_cast(f.QuadPart); - }(); - LARGE_INTEGER counter{}; - QueryPerformanceCounter(&counter); - return static_cast(counter.QuadPart) * 1000000000ULL / freq; + return TimedWait::NowNanos(); #else struct timespec ts{}; clock_gettime(CLOCK_MONOTONIC, &ts); @@ -85,41 +80,64 @@ static const bool g_timerResolutionRaised = [] { throttling.ControlMask = PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION; throttling.StateMask = 0; SetProcessInformation(GetCurrentProcess(), ProcessPowerThrottling, &throttling, sizeof(throttling)); - std::fprintf(stderr, "[time] scheduler tick %s (was %.2f ms)\n", raised ? "raised to 0.5 ms" : "unchanged", previous / 10000.0); + using NtQueryTimerResolutionFn = LONG(WINAPI*)(PULONG, PULONG, PULONG); + const auto queryResolution = ntdll ? reinterpret_cast(reinterpret_cast(GetProcAddress(ntdll, "NtQueryTimerResolution"))) : nullptr; + ULONG minimum = 0, maximum = 0, current = 0; + if (queryResolution) queryResolution(&minimum, &maximum, ¤t); + std::fprintf(stderr, "[time] scheduler tick %s (was %.2f ms, now %.2f ms, timed waits %s)\n", raised ? "raised to 0.5 ms" : "unchanged", previous / 10000.0, current / 10000.0, TimedWait::Coarse() ? "coarse" : "fine"); return raised; }(); -#endif -static void SleepNanos(std::uint64_t nanos) { - if (nanos == 0) { - return; - } -#ifdef _WIN32 - // Guest spin/backoff loops sleep for a few microseconds; Sleep() would either not sleep at all - // or round up to a whole scheduler tick, so short sleeps yield and longer ones use a - // high-resolution waitable timer. - if (nanos < 50000ULL) { - SwitchToThread(); - return; - } - thread_local HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); - if (timer) { - LARGE_INTEGER due{}; - due.QuadPart = -static_cast(nanos / 100ULL); - if (SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE)) { - WaitForSingleObject(timer, INFINITE); - return; - } - } - Sleep(static_cast((nanos + 999999ULL) / 1000000ULL)); -#else - struct timespec req{}; - req.tv_sec = static_cast(nanos / 1000000000ULL); - req.tv_nsec = static_cast(nanos % 1000000000ULL); - while (nanosleep(&req, &req) == -1 && errno == EINTR) {} -#endif +extern "C" { +int APS5_VABI scePthreadCondInit(PthreadCond* cond, const PthreadCondattr* attr, const char* name); +int APS5_VABI scePthreadCondDestroy(PthreadCond* cond); +int APS5_VABI scePthreadCondTimedwait(PthreadCond* cond, PthreadMutex* mutex, unsigned int usec); +int APS5_VABI scePthreadMutexInit(PthreadMutex* mutex, const PthreadMutexattr* attr, const char* name); +int APS5_VABI scePthreadMutexDestroy(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexLock(PthreadMutex* mutex); +int APS5_VABI scePthreadMutexUnlock(PthreadMutex* mutex); } +static void ReportTimedWait(const char* name, const std::function& body) { + std::vector samples(200); + for (auto& sample : samples) { + const std::uint64_t start = TimedWait::NowNanos(); + body(); + sample = static_cast(TimedWait::NowNanos() - start) / 1000.0; + } + std::sort(samples.begin(), samples.end()); + double sum = 0.0; + for (const double sample : samples) sum += sample; + std::fprintf(stderr, "[timers] %-44s mean %8.1f p50 %8.1f max %8.1f us\n", name, sum / static_cast(samples.size()), samples[samples.size() / 2], samples.back()); +} + +static const bool g_timerSelfTest = [] { + if (std::getenv("APS5_TRACE_TIMERS") == nullptr) return false; + std::mutex mutex; + std::condition_variable cv; + ReportTimedWait("std::condition_variable::wait_for(1 ms)", [&] { std::unique_lock lock(mutex); cv.wait_for(lock, std::chrono::milliseconds(1)); }); + PthreadCond cond = nullptr; + PthreadMutex guestMutex = nullptr; + scePthreadCondInit(&cond, nullptr, nullptr); + scePthreadMutexInit(&guestMutex, nullptr, nullptr); + scePthreadMutexLock(&guestMutex); + ReportTimedWait("scePthreadCondTimedwait(1000 us)", [&] { scePthreadCondTimedwait(&cond, &guestMutex, 1000); }); + ReportTimedWait("scePthreadCondTimedwait(13000 us)", [&] { scePthreadCondTimedwait(&cond, &guestMutex, 13000); }); + scePthreadMutexUnlock(&guestMutex); + scePthreadMutexDestroy(&guestMutex); + scePthreadCondDestroy(&cond); + ReportTimedWait("sceKernelUsleep(1000)", [&] { sceKernelUsleep_nid_postfix(1000); }); + ReportTimedWait("Sleep(1)", [&] { Sleep(1); }); + HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr); + ReportTimedWait("WaitForSingleObject(event, 1)", [&] { WaitForSingleObject(event, 1); }); + CloseHandle(event); + HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); + ReportTimedWait("high-resolution waitable timer 1 ms", [&] { LARGE_INTEGER due{}; due.QuadPart = -10000; SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE); WaitForSingleObject(timer, INFINITE); }); + CloseHandle(timer); + return true; +}(); +#endif + extern "C" { std::uint64_t APS5_VABI sceKernelGetProcessTime() { @@ -199,7 +217,7 @@ void KernelTraceWait_nid_postfix(const char* kind, const void* caller, std::uint int APS5_VABI sceKernelUsleep_nid_postfix(KernelUseconds microseconds) { TraceSleep(__builtin_return_address(0), microseconds); - SleepNanos(static_cast(microseconds) * 1000ULL); + TimedWait::SleepNanos(static_cast(microseconds) * 1000ULL); return 0; } @@ -210,8 +228,8 @@ int APS5_VABI sceKernelNanosleep(const KernelTimespec* rqtp, KernelTimespec* rmt if (rqtp->tv_sec < 0 || rqtp->tv_nsec < 0 || rqtp->tv_nsec >= 1000000000LL) { return -1; } - SleepNanos(static_cast(rqtp->tv_sec) * 1000000000ULL + - static_cast(rqtp->tv_nsec)); + TimedWait::SleepNanos(static_cast(rqtp->tv_sec) * 1000000000ULL + + static_cast(rqtp->tv_nsec)); if (rmtp != nullptr) { rmtp->tv_sec = 0; rmtp->tv_nsec = 0; @@ -421,7 +439,7 @@ uint64_t APS5_VABI sceKernelGetTscFrequency(void) { static const std::uint64_t frequency = [] { const std::uint64_t startNanos = RawMonotonicNanos(); const std::uint64_t startTicks = __rdtsc(); - SleepNanos(20000000ULL); + TimedWait::SleepNanos(20000000ULL); const std::uint64_t elapsedNanos = RawMonotonicNanos() - startNanos; const std::uint64_t elapsedTicks = __rdtsc() - startTicks; return static_cast(static_cast(elapsedTicks) * 1000000000.0L / static_cast(elapsedNanos)); diff --git a/core/libs/prx/libkernel/Time/TimedWait.cpp b/core/libs/prx/libkernel/Time/TimedWait.cpp new file mode 100644 index 000000000..08c0b0d77 --- /dev/null +++ b/core/libs/prx/libkernel/Time/TimedWait.cpp @@ -0,0 +1,280 @@ +#include "prx/libkernel/Time/include/TimedWait.hpp" + +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +namespace TimedWait { + +namespace { + +constexpr std::uint64_t YIELD_BELOW_NANOS = 50000ULL; +constexpr std::uint64_t LEAD_NANOS = 500000ULL; + +#ifdef _WIN32 + +void WINAPI DestroyWaiter(void* value); + +DWORD WaiterSlotIndex() { + static const DWORD index = FlsAlloc(DestroyWaiter); + return index; +} + +HANDLE CreateHighResolutionTimer() { + return CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); +} + +bool TimerWait(HANDLE timer, std::uint64_t nanos) { + if (!timer) return false; + LARGE_INTEGER due{}; + due.QuadPart = -static_cast(nanos / 100ULL); + if (due.QuadPart == 0) due.QuadPart = -1; + if (!SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE)) return false; + return WaitForSingleObject(timer, INFINITE) == WAIT_OBJECT_0; +} + +DWORD WholeMilliseconds(std::uint64_t nanos) { + const std::uint64_t millis = nanos / 1000000ULL; + return millis == 0 ? 1u : static_cast(millis > 0xFFFFFFFEULL ? 0xFFFFFFFEULL : millis); +} + +void SleepNanosCoarse(std::uint64_t nanos) { + if (nanos < YIELD_BELOW_NANOS) { + SwitchToThread(); + return; + } + thread_local HANDLE timer = CreateHighResolutionTimer(); + if (TimerWait(timer, nanos)) return; + Sleep(static_cast((nanos + 999999ULL) / 1000000ULL)); +} + +#endif + +} // namespace + +#ifdef _WIN32 + +struct Waiter { + HANDLE event = CreateEventW(nullptr, FALSE, FALSE, nullptr); + HANDLE timer = CreateHighResolutionTimer(); + Waiter* previous = nullptr; + Waiter* next = nullptr; + bool queued = false; + + ~Waiter() { + if (event) CloseHandle(event); + if (timer) CloseHandle(timer); + } +}; + +namespace { + +void WINAPI DestroyWaiter(void* value) { + delete static_cast(value); +} + +Waiter* ThisThreadWaiter() { + thread_local Waiter* waiter = nullptr; + if (!waiter) { + waiter = new Waiter(); + const DWORD slot = WaiterSlotIndex(); + if (slot != FLS_OUT_OF_INDEXES) FlsSetValue(slot, waiter); + } + return waiter; +} + +bool EventSet(HANDLE event, DWORD milliseconds) { + return WaitForSingleObject(event, milliseconds) == WAIT_OBJECT_0; +} + +bool WaitEventUntil(Waiter* waiter, std::uint64_t deadlineNanos) { + for (;;) { + const std::uint64_t now = NowNanos(); + if (now >= deadlineNanos) return EventSet(waiter->event, 0); + const std::uint64_t remaining = deadlineNanos - now; + if (remaining > LEAD_NANOS) { + const std::uint64_t bulk = remaining - LEAD_NANOS; + LARGE_INTEGER due{}; + due.QuadPart = -static_cast(bulk / 100ULL); + if (waiter->timer && SetWaitableTimer(waiter->timer, &due, 0, nullptr, nullptr, FALSE)) { + HANDLE handles[2] = {waiter->event, waiter->timer}; + if (WaitForMultipleObjects(2, handles, FALSE, INFINITE) == WAIT_OBJECT_0) return true; + continue; + } + if (EventSet(waiter->event, WholeMilliseconds(bulk))) return true; + continue; + } + if (EventSet(waiter->event, 0)) return true; + YieldProcessor(); + } +} + +} // namespace + +Waiter* Condition::enqueue() { + Waiter* waiter = ThisThreadWaiter(); + std::lock_guard lock(queueLock); + waiter->queued = true; + waiter->next = nullptr; + waiter->previous = tail; + if (tail) tail->next = waiter; + else head = waiter; + tail = waiter; + return waiter; +} + +void Condition::unlink(Waiter* waiter) { + if (waiter->previous) waiter->previous->next = waiter->next; + else head = waiter->next; + if (waiter->next) waiter->next->previous = waiter->previous; + else tail = waiter->previous; + waiter->previous = nullptr; + waiter->next = nullptr; + waiter->queued = false; +} + +void Condition::waitSignal(Waiter* waiter) { + WaitForSingleObject(waiter->event, INFINITE); +} + +bool Condition::waitSignalUntil(Waiter* waiter, std::uint64_t deadlineNanos) { + if (WaitEventUntil(waiter, deadlineNanos)) return true; + std::lock_guard lock(queueLock); + if (waiter->queued) { + unlink(waiter); + return false; + } + EventSet(waiter->event, 0); + return true; +} + +void Condition::NotifyOne() { + if (Coarse()) { + coarse.notify_one(); + return; + } + std::lock_guard lock(queueLock); + Waiter* waiter = head; + if (!waiter) return; + unlink(waiter); + SetEvent(waiter->event); +} + +void Condition::NotifyAll() { + if (Coarse()) { + coarse.notify_all(); + return; + } + std::lock_guard lock(queueLock); + while (Waiter* waiter = head) { + unlink(waiter); + SetEvent(waiter->event); + } +} + +#else + +void Condition::NotifyOne() { + coarse.notify_one(); +} + +void Condition::NotifyAll() { + coarse.notify_all(); +} + +#endif + +bool Coarse() { +#ifdef _WIN32 + static const bool coarse = std::getenv("APS5_COARSE_TIMED_WAITS") != nullptr; + return coarse; +#else + return true; +#endif +} + +std::uint64_t NowNanos() { +#ifdef _WIN32 + static const std::uint64_t frequency = [] { + LARGE_INTEGER f{}; + QueryPerformanceFrequency(&f); + return static_cast(f.QuadPart); + }(); + LARGE_INTEGER counter{}; + QueryPerformanceCounter(&counter); + const std::uint64_t ticks = static_cast(counter.QuadPart); + return (ticks / frequency) * 1000000000ULL + (ticks % frequency) * 1000000000ULL / frequency; +#else + struct timespec ts{}; + clock_gettime(CLOCK_MONOTONIC, &ts); + return static_cast(ts.tv_sec) * 1000000000ULL + static_cast(ts.tv_nsec); +#endif +} + +std::uint64_t DeadlineNanos(std::uint64_t microseconds) { + const std::uint64_t now = NowNanos(); + if (microseconds > (std::numeric_limits::max() - now) / 1000ULL) return std::numeric_limits::max(); + return now + microseconds * 1000ULL; +} + +std::uint64_t RemainingMicros(std::uint64_t deadlineNanos) { + const std::uint64_t now = NowNanos(); + return deadlineNanos > now ? (deadlineNanos - now) / 1000ULL : 0; +} + +void SleepUntil(std::uint64_t deadlineNanos) { +#ifdef _WIN32 + std::uint64_t now = NowNanos(); + if (deadlineNanos > now + LEAD_NANOS) { + thread_local HANDLE timer = CreateHighResolutionTimer(); + if (!TimerWait(timer, deadlineNanos - now - LEAD_NANOS)) Sleep(WholeMilliseconds(deadlineNanos - now - LEAD_NANOS)); + now = NowNanos(); + } + while (now < deadlineNanos) { + YieldProcessor(); + now = NowNanos(); + } +#else + const std::uint64_t now = NowNanos(); + if (deadlineNanos > now) SleepNanos(deadlineNanos - now); +#endif +} + +void PollSleepUntil(std::uint64_t deadlineNanos) { +#ifdef _WIN32 + const std::uint64_t now = NowNanos(); + if (deadlineNanos <= now) return; + thread_local HANDLE timer = CreateHighResolutionTimer(); + if (!TimerWait(timer, deadlineNanos - now)) Sleep(WholeMilliseconds(deadlineNanos - now)); +#else + SleepUntil(deadlineNanos); +#endif +} + +void SleepNanos(std::uint64_t nanos) { + if (nanos == 0) return; +#ifdef _WIN32 + if (Coarse()) { + SleepNanosCoarse(nanos); + return; + } + if (nanos < YIELD_BELOW_NANOS) { + SwitchToThread(); + return; + } + SleepUntil(NowNanos() + nanos); +#else + struct timespec req{}; + req.tv_sec = static_cast(nanos / 1000000000ULL); + req.tv_nsec = static_cast(nanos % 1000000000ULL); + while (nanosleep(&req, &req) == -1 && errno == EINTR) {} +#endif +} + +} // namespace TimedWait diff --git a/core/libs/prx/libkernel/Time/include/TimedWait.hpp b/core/libs/prx/libkernel/Time/include/TimedWait.hpp new file mode 100644 index 000000000..774c62dc6 --- /dev/null +++ b/core/libs/prx/libkernel/Time/include/TimedWait.hpp @@ -0,0 +1,109 @@ +#ifndef CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_TIMEDWAIT_HPP +#define CORE_LIBS_PRX_LIBKERNEL_TIME_INCLUDE_TIMEDWAIT_HPP + +#include +#include +#include +#include + +namespace TimedWait { + +bool Coarse(); +std::uint64_t NowNanos(); +std::uint64_t DeadlineNanos(std::uint64_t microseconds); +std::uint64_t RemainingMicros(std::uint64_t deadlineNanos); +void SleepNanos(std::uint64_t nanos); +void SleepUntil(std::uint64_t deadlineNanos); +void PollSleepUntil(std::uint64_t deadlineNanos); + +constexpr std::uint64_t LOCK_BULK_NANOS = 40000000ULL; +constexpr std::uint64_t LOCK_POLL_NANOS = 500000ULL; +constexpr std::uint64_t LOCK_TAIL_NANOS = 1000000ULL; + +template +bool AcquireUntil(std::uint64_t deadlineNanos, TTryLock tryLock, TTryLockFor tryLockFor) { + if (Coarse()) return tryLockFor(RemainingMicros(deadlineNanos)); + for (;;) { + if (tryLock()) return true; + const std::uint64_t now = NowNanos(); + if (now >= deadlineNanos) return false; + const std::uint64_t remaining = deadlineNanos - now; + if (remaining > LOCK_BULK_NANOS) { + if (tryLockFor((remaining - LOCK_BULK_NANOS) / 1000ULL)) return true; + continue; + } + if (remaining <= LOCK_TAIL_NANOS) SleepUntil(deadlineNanos); + else PollSleepUntil(now + LOCK_POLL_NANOS); + } +} + +struct Waiter; + +class Condition { +public: + Condition() = default; + ~Condition() = default; + Condition(const Condition&) = delete; + Condition& operator=(const Condition&) = delete; + + void NotifyOne(); + void NotifyAll(); + + template + void Wait(TLock& lock) { +#ifdef _WIN32 + if (!Coarse()) { + Waiter* waiter = enqueue(); + lock.unlock(); + waitSignal(waiter); + lock.lock(); + return; + } +#endif + coarse.wait(lock); + } + + template + bool WaitUntil(TLock& lock, std::uint64_t deadlineNanos) { +#ifdef _WIN32 + if (!Coarse()) { + Waiter* waiter = enqueue(); + lock.unlock(); + const bool signaled = waitSignalUntil(waiter, deadlineNanos); + lock.lock(); + return signaled; + } +#endif + return coarse.wait_for(lock, std::chrono::microseconds(RemainingMicros(deadlineNanos))) == std::cv_status::no_timeout; + } + + template + void Wait(TLock& lock, TPredicate predicate) { + while (!predicate()) Wait(lock); + } + + template + bool WaitUntil(TLock& lock, std::uint64_t deadlineNanos, TPredicate predicate) { + while (!predicate()) { + if (!WaitUntil(lock, deadlineNanos)) return predicate(); + } + return true; + } + +private: +#ifdef _WIN32 + Waiter* enqueue(); + void unlink(Waiter* waiter); + void waitSignal(Waiter* waiter); + bool waitSignalUntil(Waiter* waiter, std::uint64_t deadlineNanos); + + std::mutex queueLock; + Waiter* head = nullptr; + Waiter* tail = nullptr; +#endif + std::condition_variable_any coarse; +}; + +} // namespace TimedWait + +#endif diff --git a/core/libs/tests/ApplicationHeap.cpp b/core/libs/tests/ApplicationHeap.cpp index 0d4a8e900..48009f88e 100644 --- a/core/libs/tests/ApplicationHeap.cpp +++ b/core/libs/tests/ApplicationHeap.cpp @@ -87,8 +87,8 @@ int main(int argc, char**) { write(replacement, 0x18, &finalize); write(replacement, 0x20, &allocate); write(replacement, 0x28, &release); - write(replacement, 0x30, &reallocate); - write(replacement, 0x38, &allocateZeroed); + write(replacement, 0x30, &allocateZeroed); + write(replacement, 0x38, &reallocate); write(replacement, 0x40, &align); write(replacement, 0x48, &reallocate); write(replacement, 0x50, &posixAlign); diff --git a/core/libs/tests/GuestRtc.cpp b/core/libs/tests/GuestRtc.cpp new file mode 100644 index 000000000..aa393d449 --- /dev/null +++ b/core/libs/tests/GuestRtc.cpp @@ -0,0 +1,130 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceRtcCheckValid(const RtcDateTime*); +int APS5_VABI sceRtcIsLeapYear(int); +int APS5_VABI sceRtcGetDaysInMonth(int, int); +int APS5_VABI sceRtcGetDayOfWeek(int, int, int); +int APS5_VABI sceRtcGetTickResolution(void); +int APS5_VABI sceRtcGetTick(const RtcDateTime*, RtcTick*); +int APS5_VABI sceRtcSetTick(RtcDateTime*, const RtcTick*); +int APS5_VABI sceRtcGetCurrentTick(RtcTick*); +int APS5_VABI sceRtcConvertUtcToLocalTime(const RtcTick*, RtcTick*); +int APS5_VABI sceRtcConvertLocalTimeToUtc(const RtcTick*, RtcTick*); +int APS5_VABI sceRtcGetTime_t(const RtcDateTime*, std::int64_t*); +int APS5_VABI sceRtcSetTime_t(RtcDateTime*, std::int64_t); +int APS5_VABI sceRtcGetWin32FileTime(const RtcDateTime*, std::uint64_t*); +int APS5_VABI sceRtcSetWin32FileTime(RtcDateTime*, std::uint64_t); +int APS5_VABI sceRtcFormatRFC3339(char*, const RtcTick*, int); +int APS5_VABI sceRtcParseRFC3339(RtcTick*, const char*); +int APS5_VABI sceRtcTickAddTicks(RtcTick*, const RtcTick*, std::int64_t); +int APS5_VABI sceRtcTickAddSeconds(RtcTick*, const RtcTick*, std::int64_t); +int APS5_VABI sceRtcTickAddDays(RtcTick*, const RtcTick*, std::int32_t); +int APS5_VABI sceRtcTickAddMonths(RtcTick*, const RtcTick*, std::int32_t); +int APS5_VABI sceRtcTickAddYears(RtcTick*, const RtcTick*, std::int16_t); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static bool Equal(const RtcDateTime& left, const RtcDateTime& right) { + return left.year == right.year && left.month == right.month && left.day == right.day && left.hour == right.hour + && left.minute == right.minute && left.second == right.second && left.microsecond == right.microsecond; +} + +int main() { + constexpr int invalidPointer = static_cast(0x80B50002); + constexpr int invalidValue = static_cast(0x80B50003); + constexpr int badParse = static_cast(0x80B50007); + constexpr int invalidYear = static_cast(0x80B50008); + constexpr int invalidMonth = static_cast(0x80B50009); + constexpr int invalidDay = static_cast(0x80B5000A); + constexpr int invalidHour = static_cast(0x80B5000B); + constexpr int invalidMicrosecond = static_cast(0x80B5000E); + constexpr std::uint64_t unixEpochTick = 62135596800000000ull; + constexpr std::uint64_t leapDayTick = 63844806896789000ull; + constexpr std::uint64_t maxTick = 315537897599999999ull; + + Require(sceRtcGetTickResolution() == 1000000); + Require(sceRtcIsLeapYear(2000) == 1 && sceRtcIsLeapYear(1900) == 0 && sceRtcIsLeapYear(2024) == 1); + Require(sceRtcIsLeapYear(0) == invalidYear); + Require(sceRtcGetDaysInMonth(2023, 2) == 28 && sceRtcGetDaysInMonth(2024, 2) == 29 && sceRtcGetDaysInMonth(2024, 4) == 30); + Require(sceRtcGetDaysInMonth(2024, 13) == invalidMonth); + Require(sceRtcGetDayOfWeek(1, 1, 1) == 1 && sceRtcGetDayOfWeek(2026, 9, 26) == 6); + Require(sceRtcGetDayOfWeek(2023, 2, 29) == invalidDay); + + RtcDateTime leapDay{2024, 2, 29, 12, 34, 56, 789000}; + Require(sceRtcCheckValid(&leapDay) == 0); + Require(sceRtcCheckValid(nullptr) == invalidPointer); + RtcDateTime invalid = leapDay; + invalid.year = 2023; + Require(sceRtcCheckValid(&invalid) == invalidDay); + invalid = leapDay; + invalid.hour = 24; + Require(sceRtcCheckValid(&invalid) == invalidHour); + invalid = leapDay; + invalid.microsecond = 1000000; + Require(sceRtcCheckValid(&invalid) == invalidMicrosecond); + + RtcTick tick{}; + Require(sceRtcGetTick(&leapDay, &tick) == 0 && tick.tick == leapDayTick); + RtcDateTime converted{}; + Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, leapDay)); + tick.tick = maxTick; + Require(sceRtcSetTick(&converted, &tick) == 0 && Equal(converted, RtcDateTime{9999, 12, 31, 23, 59, 59, 999999})); + tick.tick = maxTick + 1; + Require(sceRtcSetTick(&converted, &tick) == invalidValue); + + RtcDateTime epoch{1970, 1, 1, 0, 0, 0, 0}; + std::int64_t seconds = -1; + Require(sceRtcGetTime_t(&epoch, &seconds) == 0 && seconds == 0); + RtcDateTime millennium{2000, 1, 1, 0, 0, 0, 0}; + Require(sceRtcGetTime_t(&millennium, &seconds) == 0 && seconds == 946684800); + Require(sceRtcSetTime_t(&converted, 946684800) == 0 && Equal(converted, millennium)); + Require(sceRtcSetTime_t(&converted, -1) == invalidValue); + std::uint64_t fileTime = 0; + Require(sceRtcGetWin32FileTime(&epoch, &fileTime) == 0 && fileTime == 116444736000000000ull); + Require(sceRtcSetWin32FileTime(&converted, 116444736000000000ull) == 0 && Equal(converted, epoch)); + + char text[32]; + tick.tick = leapDayTick; + Require(sceRtcFormatRFC3339(text, &tick, 0) == 0 && std::strcmp(text, "2024-02-29T12:34:56.78Z") == 0); + Require(sceRtcFormatRFC3339(text, &tick, 90) == 0 && std::strcmp(text, "2024-02-29T14:04:56.78+01:30") == 0); + Require(sceRtcFormatRFC3339(text, &tick, -300) == 0 && std::strcmp(text, "2024-02-29T07:34:56.78-05:00") == 0); + Require(sceRtcParseRFC3339(&tick, "2024-02-29T14:04:56.789+01:30") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseRFC3339(&tick, "2024-02-29t12:34:56.789z") == 0 && tick.tick == leapDayTick); + Require(sceRtcParseRFC3339(&tick, "1970-01-01T00:00:00Z") == 0 && tick.tick == unixEpochTick); + Require(sceRtcParseRFC3339(&tick, "2023-02-29T00:00:00Z") == invalidDay); + Require(sceRtcParseRFC3339(&tick, "2024-02-29T12:34:56") == badParse); + Require(sceRtcParseRFC3339(&tick, "2024-02-29T12:34:56Zjunk") == badParse); + Require(sceRtcParseRFC3339(nullptr, "1970-01-01T00:00:00Z") == invalidPointer); + + RtcTick source{leapDayTick}; + RtcTick result{}; + Require(sceRtcTickAddSeconds(&result, &source, 3600) == 0 && result.tick == leapDayTick + 3600000000ull); + Require(sceRtcTickAddDays(&result, &source, 1) == 0); + Require(sceRtcSetTick(&converted, &result) == 0 && converted.month == 3 && converted.day == 1); + Require(sceRtcTickAddYears(&result, &source, 1) == 0); + Require(sceRtcSetTick(&converted, &result) == 0 && converted.year == 2025 && converted.month == 2 && converted.day == 28); + RtcDateTime endOfJanuary{2024, 1, 31, 8, 0, 0, 0}; + Require(sceRtcGetTick(&endOfJanuary, &source) == 0); + Require(sceRtcTickAddMonths(&result, &source, 1) == 0); + Require(sceRtcSetTick(&converted, &result) == 0 && Equal(converted, RtcDateTime{2024, 2, 29, 8, 0, 0, 0})); + Require(sceRtcTickAddMonths(&result, &source, -12 * 2024) == invalidValue); + source.tick = 5; + Require(sceRtcTickAddTicks(&result, &source, -6) == invalidValue); + Require(sceRtcTickAddTicks(&result, &source, -5) == 0 && result.tick == 0); + source.tick = maxTick; + Require(sceRtcTickAddTicks(&result, &source, 1) == invalidValue); + Require(sceRtcTickAddTicks(nullptr, &source, 1) == invalidPointer); + + RtcTick now{}; + Require(sceRtcGetCurrentTick(&now) == 0 && now.tick > leapDayTick); + RtcTick local{}; + RtcTick back{}; + Require(sceRtcConvertUtcToLocalTime(&now, &local) == 0); + Require(sceRtcConvertLocalTimeToUtc(&local, &back) == 0 && back.tick == now.tick); +}