Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 43 additions & 27 deletions core/libs/prx/libc/src/Time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,24 +107,41 @@ bool isFreeBsdConversion(char specifier) {
return specifier == 'k' || specifier == 'l' || specifier == 's' || specifier == 'v' || specifier == '+';
}

bool isModifiedPercent(const char* afterPercent) {
return (afterPercent[0] == 'E' || afterPercent[0] == 'O') && afterPercent[1] == '%';
}

std::size_t freeBsdConversionLength(const char* afterPercent, char& conversion) {
struct FormatConversion {
std::size_t length;
char specifier;
bool literal;
bool padded;
};

FormatConversion parseConversion(const char* afterPercent) {
const char* cursor = afterPercent;
if ((*cursor == 'E' || *cursor == 'O') && isFreeBsdConversion(cursor[1])) ++cursor;
if (!isFreeBsdConversion(*cursor)) return 0;
conversion = *cursor;
return static_cast<std::size_t>(cursor - afterPercent) + 1;
bool alternative = false;
bool padded = false;
while (*cursor != '\0') {
if (*cursor == 'E' || *cursor == 'O') {
if (alternative) break;
alternative = true;
} else if (*cursor == '-' || *cursor == '_' || *cursor == '0') {
if (padded) break;
padded = true;
} else {
break;
}
++cursor;
}
if (*cursor == '\0')
return {static_cast<std::size_t>(cursor - afterPercent), cursor[-1], true, padded};
const bool literal = *cursor == '%' || std::strchr("aAbBcCdDeFgGhHIjklmMnprRsStTuUVvWwXxyYzZ+", *cursor) == nullptr;
return {static_cast<std::size_t>(cursor - afterPercent) + 1, *cursor, literal, padded};
}

bool hasFreeBsdConversion(const char* format) {
bool requiresFreeBsdExpansion(const char* format) {
while (*format != '\0') {
if (*format++ != '%' || *format == '\0') continue;
char conversion = 0;
if (freeBsdConversionLength(format, conversion) != 0) return true;
format += isModifiedPercent(format) ? 2 : 1;
if (*format++ != '%') continue;
const auto conversion = parseConversion(format);
if (conversion.literal || (!conversion.padded && isFreeBsdConversion(conversion.specifier))) return true;
format += conversion.length;
}
return false;
}
Expand All @@ -142,26 +159,25 @@ std::string expandFreeBsdConversions(const char* format, const GuestTm& guest) {
std::string result;
while (*format != '\0') {
const char next = *format++;
if (next != '%' || *format == '\0') {
if (next != '%') {
result += next;
continue;
}
if (isModifiedPercent(format)) {
result += '%';
result += *format++;
result += *format++;
const auto conversion = parseConversion(format);
if (conversion.literal) {
if (conversion.specifier == '%') result += '%';
result += conversion.specifier;
format += conversion.length;
continue;
}
char conversion = 0;
const std::size_t length = freeBsdConversionLength(format, conversion);
if (length == 0) {
result += '%';
result += *format++;
if (conversion.padded || !isFreeBsdConversion(conversion.specifier)) {
result.append(format - 1, conversion.length + 1);
format += conversion.length;
continue;
}
format += length;
format += conversion.length;
char text[32];
switch (conversion) {
switch (conversion.specifier) {
case 'k':
std::snprintf(text, sizeof(text), "%2d", guest.tm_hour);
result += text;
Expand Down Expand Up @@ -302,7 +318,7 @@ int64_t APS5_VABI mktime_nid_postfix(GuestTm* timeptr) {

size_t APS5_VABI libc_strftime_nid_postfix(char* str, size_t count, const char* format, const GuestTm* timeptr) {
const std::tm host = toHostTm(*timeptr);
if (!hasFreeBsdConversion(format)) return std::strftime(str, count, format, &host);
if (!requiresFreeBsdExpansion(format)) return std::strftime(str, count, format, &host);
const std::string expanded = expandFreeBsdConversions(format, *timeptr);
return std::strftime(str, count, expanded.c_str(), &host);
}
Expand Down
48 changes: 48 additions & 0 deletions core/libs/tests/GuestTimeConversion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,53 @@ static void CheckFreeBsdConversions() {
Require(strftime_nid_postfix(tooSmall, sizeof(tooSmall), "%k:%l", &guest) == 0, "strftime accepted an undersized buffer");
}

static void CheckLiteralConversions() {
const std::int64_t timer = 1700000000;
GuestTm guest{};
Require(gmtime_s_nid_postfix(&timer, &guest) == &guest, "UTC conversion failed");
struct FormatCase {
const char* format;
const char* expected;
};
constexpr FormatCase cases[]{
{"%Q", "Q"},
{"%P|%f|%N|%1|%#Y", "P|f|N|1|#Y"},
{"[%Q] %Y-%m-%d", "[Q] 2023-11-14"},
{"%EQ|%OQ|%_Q|%-Q|%0Q", "Q|Q|Q|Q|Q"},
{"%E_Q|%_EQ|%O0Q|%-OQ", "Q|Q|Q|Q"},
{"%EEY|%EOY|%OEY|%OOY", "EY|OY|EY|OY"},
{"%EE%Y|%OO%k", "E2023|O22"},
{"%--Y|%__Y|%00Y|%_0Y|%0-Y", "-Y|_Y|0Y|0Y|-Y"},
{"%E_EY|%_EOY|%O0OY", "EY|OY|OY"},
{"%", "%"},
{"text%", "text%"},
{"%E", "E"},
{"%O", "O"},
{"%_", "_"},
{"%-", "-"},
{"%0", "0"},
{"%E_", "_"},
{"%_E", "E"},
{"%Q%", "Q%"},
{"%k%", "22%"},
{"%%Q|%%%Q|%%%%Q", "%Q|%Q|%%Q"},
{"%E%Q|%O%Q|%_%Q|%0%Q|%-%Q", "%Q|%Q|%Q|%Q|%Q"},
{"%E%%Q|%O%%%Q", "%Q|%%Q"},
{"%Q|%Ek|%Ol|%Ev|%Y", "Q|22|10|14-Nov-2023|2023"},
{"%\xc3\xa5", "\xc3\xa5"},
{"%%", "%"},
{"literal text", "literal text"},
};
for (const auto& test : cases) {
CheckFormat(guest, test.format, test.expected);
CheckBufferSize(guest, test.format, std::strlen(test.expected));
}
CheckFormat(guest, "", "");
char buffer[2]{'x', 'y'};
Require(strftime_nid_postfix(buffer, 0, "%Q", &guest) == 0, "strftime accepted a zero-size buffer");
Require(buffer[0] == 'x' && buffer[1] == 'y', "strftime wrote to a zero-size buffer");
}

int main() {
#ifdef _WIN32
_putenv_s("TZ", "UTC-2");
Expand Down Expand Up @@ -233,6 +280,7 @@ int main() {
&& std::strcmp(formatted, "1970-01-01 00:00:00") == 0, "strftime did not read the guest tm");

CheckFreeBsdConversions();
CheckLiteralConversions();

CheckConcurrent(libc_gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent libc_gmtime returned another thread's date");
CheckConcurrent(gmtime_nid_postfix, gmtime_s_nid_postfix, "Concurrent gmtime returned another thread's date");
Expand Down
2 changes: 1 addition & 1 deletion docs/dev/TechnicalDebt.md
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ Throughout the project, every function at every stage either **does exactly what
- [Temporary app-content storage](../../core/libs/prx/libSceAppContent/Export.cpp) tracks one `/temp0` mount, preserves its files on unmount, and rejects new path resolutions until remount. Mount, format, space-query and unmount errors follow [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_path.pas); the pointer ABI is also declared by [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/main/src/SharpProspero/Interop/AppContent/AppContent.cs). Existing open host descriptors remain usable after unmount; console behaviour with open files is unverified. Host filesystem failures still throw.
- [Guest path resolution](../../core/libs/prx/libc/src/General.cpp) falls back to ASCII case-insensitive component lookup, following [shadPS4's host filesystem](https://github.com/shadps4-emu/shadPS4/blob/945dbc3cc3eee80ac3e053b438502ed936fa6bb2/src/core/file_sys/backends/host_fs.cpp). PPSA03747 requests `Skyrim.INI`, `Data/Video` and lowercase archive/plugin names that differ from the dump's filenames. Exact host names take precedence; multiple fallback matches throw instead of selecting an arbitrary file. Existing parent names are resolved when creating a new file, and guest mount aliases use the same ASCII comparison. Results are not cached, so renames and newly created files remain visible. Unicode case folding and PS5 behaviour for case-colliding host entries are unverified; host roots are used as supplied.
- [localtime, mktime](../../core/libs/prx/libc/src/Time.cpp) (libc) on Windows convert through the CRT's `localtime_s` and `mktime`, which accept about 1970 to January 3001 UTC and apply the CRT's daylight-saving rule; outside that range `localtime` returns null and `mktime` returns -1, where the console converts. `gmtime` converts every time whose year fits an `int` on both hosts.
- [strftime](../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does, and passes every other conversion to the host's `strftime`. On Windows the conversions the CRT lacks (`%P` and the `%-`, `%_` and `%0` padding flags) still produce nothing, and the padding flags are not supported on the five new conversions at all (`%-k`, `%_k` and `%0k` go to the host), an unknown conversion, a doubled `E` or `O` modifier (`%EE`) or a trailing lone `%` returns 0 where FreeBSD prints `%` and the character, and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null.
- [strftime](../../core/libs/prx/libc/src/Time.cpp) (libc) expands FreeBSD's `%k`, `%l`, `%s`, `%v` and `%+` itself, ignoring a leading `E` or `O` as FreeBSD does. Unknown conversions, repeated modifiers and incomplete conversions emit literals as in FreeBSD 11; recognized conversions otherwise go to the host's `strftime`. On Windows the `%-`, `%_` and `%0` padding flags still produce nothing, including on the five expanded conversions (`%-k`, `%_k` and `%0k` go to the host), and a standalone `%z` or `%Z` uses the host's zone instead of the broken-down time's `tm_zone`. `%s` follows FreeBSD 9 and 11 and is the local-time `mktime` of the broken-down time (current FreeBSD uses `tm_gmtoff`), which is -1 on Windows outside the range above. `%+` is the C locale's `%a %b %e %H:%M:%S %Z %Y` with `%Z` taken from `tm_zone`, or from `tzname[tm_isdst != 0]` when it is null.
- [LINE_STIPPLE_TEX](../../core/shader/recompiler/Translation/src/InstructionTranslator.cpp) (recompiler) reads as 0: line stipple is not emulated. RADV enables the input only as the required pixel input of a shader that reads no interpolants, because it initializes the fewest VGPRs ([Mesa](https://gitlab.freedesktop.org/mesa/mesa/-/blob/main/src/amd/vulkan/radv_shader.c) `radv_compute_spi_ps_input`), and never reads it
- [Draw snapshot buffer descriptors](../../core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp) rewrite each affected array from element zero, preserving unchanged descriptors. MoltenVK 1.4.2 writes auxiliary buffer lengths at index `i` instead of `start + i` for partial array updates ([MVKDescriptorSet.mm, BufferAuxSize](https://github.com/KhronosGroup/MoltenVK/blob/v1.4.2/MoltenVK/MoltenVK/GPUObjects/MVKDescriptorSet.mm#L1051)). Per-element writes therefore let one buffer length replace another even when Vulkan buffer handles and ranges are correct.
- [Static scalar calls](../../core/shader/recompiler/ControlFlow/src/GraphBuilder.cpp) (`s_call_b64` and `s_swappc_b64`) require an ordinary even-aligned SGPR link pair (s0:s1 through s104:s105) and a later `s_setpc_b64` return reading that exact pair. A pair serves one call, or several calls whose regions are disjoint, each closed by the first return reading the pair after its target. Outside every region of its calls the pair may only be overwritten whole, by `s_mov_b32`, `s_mov_b64`, an SOP2 result or a scalar load, as the caller of a user-data callee does before each call. Regions must nest or be disjoint; recursion, other shared links, escaping link values (including overlapping tuples and resource descriptors), invalid targets, interleaved regions, cross-region branches or fall-through, relative SGPR reads and dynamic `s_setpc_b64` in a program with calls throw. The analysis checks all decoded instructions, including unreachable code. Arbitrary indirect calls and general call frames are not implemented.
Expand Down
Loading