Skip to content

fix(libSceSystemService): report the host time zone and summer time - #5084

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
0xRickx:fix/libscesystemservice-host-time-zone
Oct 16, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
0xRickx:fix/libscesystemservice-host-time-zone

Conversation

@0xRickx

@0xRickx 0xRickx commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

sceSystemServiceParamGetInt answered 0 for the time zone (parameter 4) and for summer time (parameter 5), so every title saw a console set to UTC without summer time. libSceRtc and libc convert local time with the host's zone (#4939 is about the same offsets), so a title got two different local times.

Titles compute the local time from these two values as UTC + (time zone + 60 × summer time) minutes. Sources for that rule:

  • Apollo and GoldHEN's frame logger, two homebrew that run on consoles, compute it that way.
  • SharpProspero describes the values as "the time-zone offset from UTC in minutes" and "true when summer time is in effect".

Both values now follow the host's local time zone at the moment of the call:

  • Summer time: tm_isdst of the host's local time.
  • Time zone: the host's current offset east of UTC in minutes, less 60 while summer time is in effect. The offset comes from localtime_s/localtime_r, as libSceRtc's does, so TZ is honoured.

Prospero Explorer (PPSA99109) shows both on its System information > Console tab (#5071). On a Windows host in Central European Summer Time:

  • On main: Time zone UTC+00:00, Summer time no.
  • With this change: UTC+01:00, yes.

Not changed here: libkernel's sceKernelConvertUtcToLocaltime, sceKernelConvertLocaltimeToUtc and sceKernelGettimezone still use UTC. The SceKernelTimesec fields need their own references, so they are left for a follow-up. The debt file records this.

Tested

Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release, on main ae244d5:

  • New guest_system_service_time_zone test (TZ set through the CRT, as GuestRtc does):

    • IST-5:30 gives 330/0, BRT+3 gives -180/0, UTC0 gives 0/0.
    • CET-1CEST gives a time zone of 60 and a summer time that matches the CRT's own tm_isdst at that moment. glibc and the Windows CRT both apply their default summer time rules to it.
    • On Linux, CET-1CEST,0/0,J365/25 (summer time all year) gives 60/1.

    It aborts on main and passes with the change.

  • guest_system_service, guest_system_service_language and patched_libraries pass. Full ctest: 1053/1053 passed (8 skipped on this machine). tools/check_conventions.py --base upstream/main passes.

  • Prospero Explorer (PPSA99109, catalog 01.000.000), relinked: I reached the Console tab with APS5_PAD_INPUT_FILE (steps in Prospero Explorer (PPSA99109) #5071) and captured it with APS5_DUMP_FRAMES. The rows changed as shown above.

Rebased onto main ffa2a6e without conflicts (head 84728d6), then tested again on main c496fd5:

  • Windows (same toolchain), with my eight other open PRs merged as well: guest_system_service, guest_system_service_language and guest_system_service_time_zone pass. Full ctest: 1088/1088 passed (8 skipped on this machine).
  • Linux (Ubuntu 24.04 in WSL, GCC 13.3, Release, the CI's configure options, lavapipe):
    • this branch alone: guest_system_service, guest_system_service_language and guest_system_service_time_zone pass, including the all-year summer time zone.
    • full ctest with my eight other open PRs merged as well: 1067/1070 passed. guest_math, agc_driver_depth_compare_copy and guest_socket_read_write fail the same way on main b868383 without any of them in this setup.

After the review (CI's Windows runner has a UTC system zone): the summer time shift on Windows is now the C runtime's own bias (_get_dstbias), as screamm found and tested. Head b36bbe9 on main ff23657:

  • Windows 11 (26200), same toolchain, W. Europe system zone with summer time in effect: guest_system_service_time_zone, guest_system_service, guest_system_service_language, guest_system_service_path_boundary and patched_libraries pass.
  • The runner's case, reproduced with gdb answering GetTimeZoneInformation with a zeroed structure (UTC, no summer time) and TZ unset at start: the previous head aborts in the CET-1CEST row, this head passes. With TZ at start unset, UTC0, CET-1CEST,M3.5.0,M10.5.0/3 or EST5EDT, the test passes under both zones.
  • Linux (Ubuntu 24.04 in WSL, GCC 13.3, Release, the CI's configure options, lavapipe), merged onto main 92e5041 together with fix(libkernel): refuse a loopback TCP connect at once on Windows #5141: the four tests above and patched_libraries pass. Full ctest: 1263/1273; the ten failures (nine agc_* tests and guest_socket_read_write) fail the same way on main 92e5041 alone in this setup.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: PPSA99109 Prospero Explorer (PPSA99109) #5071
  • AI-assisted: yes

🤖 Generated with Claude Code

@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the diff against the references cited (Apollo, GoldHEN, SharpProspero), built and ran the area's ctest on Linux, and confirmed guest_system_service_time_zone fails without the Export.cpp change and passes with it. Debt line updated and accurate; merging once the full Build is green.

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full Build (Windows) fails on this head: guest_system_service_time_zone aborts with 0xc0000409 (run 38609296408, windows job, ctest #890). A rerun of the job fails the same way, and main does not have this failure. The log does not say which check aborts.

The runner's system time zone is UTC, while this was tested on a CET machine. Could you reproduce it with the system time zone set to UTC and see which Require fails? Likely candidates are the CET-1CEST rows (default summer time rules of the CRT versus tm_isdst) or the offset calculation in HostTimeZone(). Linux is fine on our side: the test fails without the Export.cpp change and passes with it.

@screamm

screamm commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Windows run of this head alone, merged onto main 6239ceffc (clean merge; it also merges cleanly onto the newer 2559e9623, not built). The guest_system_service_time_zone failure from the dispatched build reproduces when the system time zone is UTC. The failing check is line 52, Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_TIME_ZONE) == 60); (the CET-1CEST row), which gets 0.

  • Head 84728d602, base 6239ceffc.
  • Host: Windows 11 Pro 26220, WinLibs MinGW-w64 GCC 15.2.0 r7, Ninja, Release, NVIDIA RTX 4070. System time zone is W. Europe Standard Time, with summer time in effect.
  • Build: the full build and libs pass with 0 errors and no new warnings.
  • Tests, --repeat until-fail:3: guest_system_service_time_zone, guest_system_service, guest_system_service_language and patched_libraries each passed 3/3, both with TZ unset and with TZ=UTC in the environment. TZ=UTC therefore does not reproduce the failure.
  • With a UTC system time zone: I did not change this machine's time zone. Instead I ran the unchanged guest_system_service_time_zone_tests.exe under gdb, with GetTimeZoneInformation answering what Windows returns for the UTC zone (Bias 0, no DaylightDate). The test aborts with 0xc0000409, as on the runner. The last call before abort() is sceSystemServiceParamGetInt(4) -> 0, the 7th call. The same gdb script without that override runs the test to a normal exit.

Cause: libc.prx's static initializer (tzname_nid_postfix → HostZoneNames() → _tzset()) runs before main while TZ is unset, so the UCRT reads the system time zone. For a zone without summer time it sets _dstbias = 0. When the test later sets TZ (_putenv_s + _tzset), the UCRT updates _timezone, _daylight and the zone names but leaves _dstbias unchanged. With CET-1CEST, localtime_s then reports tm_isdst = 1 (the CRT's default rules) but shifts by _dstbias = 0, so local time is UTC+1 and HostTimeZone() returns 60 − 60 = 0. On a CET machine, the system zone has already set _dstbias = -3600, so local time is UTC+2 and the result is 60. That is why the test passed for you.

The same libSceSystemService.prx, called with the test's steps but printing instead of aborting:

mode: GetTimeZoneInformation answers UTC (no DST) for this process, TZ env at start: []
  CRT after loading libc.prx:      _timezone=0 _daylight=0 _dstbias=0 tzname=[Coordinated Universal Time,Coordinated Universal Time]
TZ=CET-1CEST
  CRT after _tzset:                _timezone=-3600 _daylight=1 _dstbias=0 tzname=[CET,CES]
  localtime_s 02:23 isdst=1, gmtime_s 01:23
  FAIL Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_TIME_ZONE) == 60)     got 0, want 60
  pass Require(ParamInt(SYSTEM_SERVICE_PARAM_ID_SUMMERTIME) == (HostSummerTime() ? 1 : 0)) got 1, want 1

The IST-5:30, BRT+3 and UTC0 rows pass in every mode. With this machine's own time zone, and with TZ=UTC, _dstbias stays -3600 and the CET-1CEST row gives 60. Subtracting the CRT's own bias on Windows (_get_dstbias, as libc's localtime_s_nid_postfix does for tm_gmtoff) instead of a fixed 60 would give 60 in both cases. I have not tested that change.

  • Non-GPU suite (ctest -j8 --timeout 120 -E '^agc_driver_|^video_out_flip'): 800/809. These eight timed out while the host was busy and passed when rerun one at a time: conventions_function_bodies, debt_split, hardware_oracle_compare, conventions_bom_shebang, performance_compare, guest_raise_exception, guest_none_relocations, tls_function_coverage_register_address_loads. hardware_oracle_compare needed a second rerun, then passed 2/2. check_main_sync fails the same way on plain main here (console code page).
  • Prospero Explorer PPSA99109 (catalog), relinked with each side's relinker and libs, 60 s in a window, no input:
    • main: Prospero Explorer | FPS: 5.69 (244), still running at 60 s, no stop line.
    • this PR: Prospero Explorer | FPS: 11.56 (689), still running at 60 s, no stop line. The frame rate depends on host load.
  • System information > Console tab, reached with APS5_PAD_INPUT_FILE (down ×7, cross) and captured with APS5_DUMP_FRAMES:
    • main: Time zone UTC+00:00, Summer time no
    • this PR: Time zone UTC+01:00, Summer time yes

Verdict: on a CET host the change works in Explorer, and the tests and the non-GPU suite pass. The Windows Build failure reproduces with a UTC system time zone: the failing check is the CET-1CEST time-zone row (line 52), because the UCRT keeps the system zone's _dstbias of 0 after TZ is set. Either HostTimeZone() or the test needs a change before it can go green on the runner.

AI-assisted (Claude Code).

@screamm

screamm commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Follow-up: the _get_dstbias change suggested above is now tested. Head 84728d602 merged onto main d92a8dad3 (clean), same host (Windows 11 Pro 26220, WinLibs MinGW-w64 GCC 15.2.0 r7, Release, W. Europe system zone with summer time in effect).

--- a/core/libs/prx/libSceSystemService/Export.cpp
+++ b/core/libs/prx/libSceSystemService/Export.cpp
@@ -60,7 +60,14 @@ ConsoleTimeZone HostTimeZone() {
     const auto localSeconds = date.time_since_epoch() + std::chrono::hours{local.tm_hour} + std::chrono::minutes{local.tm_min} + std::chrono::seconds{local.tm_sec};
     const auto offset = std::chrono::duration_cast<std::chrono::minutes>(localSeconds - std::chrono::seconds{now});
     const bool summer = local.tm_isdst > 0;
-    return {static_cast<int>(offset.count()) - (summer ? 60 : 0), summer};
+#if defined(_WIN32)
+    long daylightBias = 0;
+    _get_dstbias(&daylightBias);
+    const int summerMinutes = summer ? static_cast<int>(-daylightBias / 60) : 0;
+#else
+    const int summerMinutes = summer ? 60 : 0;
+#endif
+    return {static_cast<int>(offset.count()) - summerMinutes, summer};
 }

No new include is needed (<ctime> declares _get_dstbias on MinGW-w64). The non-Windows branch is unchanged and was not run here.

guest_system_service_time_zone with and without the change, one process per cell. "UTC system zone" is the same gdb override of GetTimeZoneInformation as in my earlier comment (Bias 0, no DaylightDate); the machine's zone was not changed.

TZ at process start host zone, before host zone, after UTC zone, before UTC zone, after
unset (the runner's case) pass pass fails line 52 (CET-1CEST row gets 0, abort 0xc0000409) pass
UTC0 pass pass pass pass
UTC pass pass pass pass
CET-1CEST,M3.5.0,M10.5.0/3 pass pass pass pass
EST5EDT pass pass pass pass

Only "TZ unset with a system zone that has no summer time" fails before the change: the UCRT reads the system zone once when libc.prx loads and keeps that _dstbias (0) after the test's _putenv_s("TZ", ...) and _tzset(); with TZ set at start _dstbias stays at its initial -3600. After the change, ctest -R '^guest_system_service_time_zone$' --repeat until-fail:3 passes 3/3, python tools/check_conventions.py --base main passes and git diff --check is clean.

The Windows job in .github/workflows/build.yml runs plain ctest --test-dir build --output-on-failure --timeout 120 --parallel 4 with no TZ, so the runner's UTC system zone is what the "unset" row simulates. Not covered: a real run on the runner, and a system zone whose summer bias is not 60 minutes.

AI-assisted (Claude Code).

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 6044 core/libs/prx/libSceSystemService/CMakeLists.txt
PR 6447 core/libs/prx/libSceSystemService/Export.cpp
docs/dev/debt/libSceSystemService.md

@0xRickx
0xRickx force-pushed the fix/libscesystemservice-host-time-zone branch from 84728d6 to b36bbe9 Compare October 15, 2026 14:44
@0xRickx

0xRickx commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @screamm for finding the cause and testing the change, and @adriadam10 for the review.

I applied screamm's _get_dstbias change and rebased onto main ff23657 (head b36bbe9). On Windows the summer time shift is now the CRT's own bias, the one localtime_s applied; the non-Windows branch keeps 60 minutes. The debt entry now says how each platform reports a zone whose summer time is not one hour.

Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release, this machine's zone W. Europe with summer time in effect:

  • guest_system_service_time_zone, guest_system_service, guest_system_service_language, guest_system_service_path_boundary and patched_libraries pass.
  • The runner's case without changing the machine's zone: gdb answers GetTimeZoneInformation with a zeroed structure (UTC, no summer time) and TZ is unset at start. The previous head aborts in the CET-1CEST row; this head passes.
  • With TZ at process start unset, UTC0, CET-1CEST,M3.5.0,M10.5.0/3 and EST5EDT, the test passes under both the machine's zone and the simulated UTC zone.

Not covered: a run on the runner itself. The Linux code path did not change.

AI-assisted (Claude Code).

@screamm

screamm commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

Follow-up on head b36bbe9c6 (merged onto main 44df21632, no conflicts). Windows 11 Pro 26220, WinLibs MinGW-w64 GCC 15.2.0 r7, Release, NVIDIA RTX 4070 (driver 596.21). The machine's zone is W. Europe with summer time in effect, and I did not change it.

  • guest_system_service_time_zone, guest_system_service, guest_system_service_language, guest_system_service_path_boundary and patched_libraries pass 3 of 3.
  • @adriadam10's point (the abort with 0xc0000409 under a UTC system zone): I ran the test under gdb with GetTimeZoneInformation answering UTC, plus a Lord Howe zone (DaylightBias -30). Each zone was tried with TZ at start unset, UTC0, UTC, CET-1CEST,M3.5.0,M10.5.0/3 and EST5EDT, 15 runs in all, and every run exits normally. Direct runs on the host zone with six TZ values all exit 0x0.
  • Negative control: with only the _get_dstbias hunk reverted (the previous head's fixed 60), the UTC zone with TZ unset aborts with exception 0xc0000409 in ucrtbase!abort called from the test's main, the same failure as the CI log. The Lord Howe zone with TZ unset also aborts. All other cells still pass. Restoring the hunk makes all 15 pass again.
  • Prospero Explorer (PPSA99109), 120 s per build, System information > Console reached with APS5_PAD_INPUT_FILE: main shows Time zone UTC+00:00 / Summer time no; this head shows UTC+01:00 / yes. Neither build has a stop line; both were still running at 120 s (659 and 691 frames).
  • check_conventions.py --base origin/main passes; git diff --check is clean.

Not checked: the runner itself (its Windows job is still "skipping" on this head), a real tzutil zone change, Linux, the full build and the non-GPU suite (I built only libs, the relinker and these test targets).

Written with Claude Code assistance (validation by a subagent, reviewed in this session).

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked head b36bbe9: the _get_dstbias change removes the dependency on the system zone for the CET-1CEST row, matching the reproductions by @screamm and the gdb run in the PR. Built and ran the area's ctest on Linux (47/47), and guest_system_service_time_zone still fails without the Export.cpp change. Debt entry covers both platforms. Merging once the full Build is green.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
sceSystemServiceParamGetInt answered 0 for the time zone (parameter 4)
and summer time (parameter 5), so the console looked set to UTC while
libSceRtc and libc convert local time with the host's zone. Titles add
the time zone in minutes and 60 more minutes when summer time is 1 to
UTC to get the local time (Apollo, GoldHEN's frame logger). Both now
follow the host's local time zone: summer time is whether it is in
effect, and the time zone is the current offset east of UTC less the
summer time shift while it is.

On Windows that shift is the C runtime's own summer time bias
(_get_dstbias), the one localtime_s applied, not a fixed 60 minutes.
The UCRT reads the system zone when libc.prx loads and keeps that
zone's bias after TZ is set later, so on a host whose system zone has
no summer time (the CI runner's UTC) a TZ of CET-1CEST shifted by 0 and
the time zone came out as 0. The cause and the change are screamm's,
from the review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@adriadam10

Copy link
Copy Markdown
Collaborator

Rebased onto current main: the only conflict was the libSceSystemService debt entry (main's language/string lines next to this PR's time-zone line). Kept both, and dropped "time zone, summer time" from the list of values that stay fixed. No code changes. Built and ran the area's ctest on Linux (49/49, including guest_system_service_time_zone). Merging once the full Build is green.

@adriadam10
adriadam10 force-pushed the fix/libscesystemservice-host-time-zone branch from b36bbe9 to ff1de91 Compare October 16, 2026 20:01
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 16, 2026
@adriadam10
adriadam10 merged commit 11d1557 into boykopovar:main Oct 16, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants