Skip to content

fix(libkernel): convert local time with the host time zone - #5096

Merged
adriadam10 merged 8 commits into
boykopovar:mainfrom
sguckiran:main
Oct 14, 2026
Merged

adriadam10 merged 8 commits into
boykopovar:mainfrom
sguckiran:main

Conversation

@sguckiran

@sguckiran sguckiran commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

Fixes #5067. sceKernelConvertUtcToLocaltime and sceKernelConvertLocaltimeToUtc returned the time unchanged, while libc localtime follows the host zone. PPSA99203 (PS5SX2) draws its on-screen clock and its settings log times from sceKernelConvertUtcToLocaltime followed by gmtime_r (source). Because of that, it showed UTC as local time in #5067.

  • Both conversions now use the host zone's offset at the given instant. On POSIX that is tzset followed by localtime_r/tm_gmtoff, with timezone for the standard part. On Windows it is _tzset followed by localtime_s, with _get_timezone/_get_dstbias. These are the sources libc localtime_s uses. Windows times outside the CRT range move by whole 400-year cycles, as libc does.
  • KernelTimesec.west_sec holds the standard offset in seconds east of UTC, despite its name, and dst_sec holds the daylight saving seconds in effect. This follows PS5SX2's own notes from a console, where the call wrote 7200 and 3600, and shadPS4, which also stores seconds east. Whether west_sec includes daylight saving is not settled, and the open question is recorded in docs/dev/debt/libkernel.md.
  • On Windows, instants under a zone's earlier rules get today's standard offset and daylight saving amount, as libc localtime_s does (recorded as debt).
  • sceKernelConvertLocaltimeToUtc finds the offset in two passes. In a skipped or repeated local hour it takes the offset in effect one standard offset earlier (recorded as debt).
  • sceKernelGettimezone and the gettimeofday time zone are unchanged (no offset, no daylight saving) and recorded as debt. fix(libSceSystemService): report the host time zone and summer time #5084 makes the matching change for sceSystemServiceParamGetInt.

Tested

Ubuntu 24.04, GCC 13.3, Release. guest_freebsd_clocks passes. It now pins TZ and checks both directions for UTC0, IST-5:30 (+5:30), BRT+3 (-3:00) and EST5EDT in January (-5:00) and July (-4:00 with 3600 s of daylight saving), including the KernelTimesec fields. It also passes with no zoneinfo files (TZDIR=/nonexistent) and with a host TZ=America/Toronto. The new checks abort on the previous Time.cpp. The existing identity and guard checks run under UTC0. Relinker-only build: all tests pass. Not tested on Windows or with a title.

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: none
  • AI-assisted: yes (Claude Code)

claude and others added 2 commits October 13, 2026 11:27
sceKernelConvertUtcToLocaltime and sceKernelConvertLocaltimeToUtc returned
the time unchanged while libc localtime follows the host zone, so a title
that mixes them shows UTC as local time (boykopovar#5067).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iLxaSz4qjmirfqKZdhVQk
fix(libkernel): convert local time with the host time zone
@github-actions github-actions Bot mentioned this pull request Oct 13, 2026
10 tasks done
PPSA99203 (PS5SX2) documents that the console fills west_sec with the
offset east of UTC despite the field's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iLxaSz4qjmirfqKZdhVQk
…ts section

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iLxaSz4qjmirfqKZdhVQk
@sguckiran

Copy link
Copy Markdown
Contributor Author

fixed conflict.

@xXPBSXx

xXPBSXx commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

Review note (static, nothing built): (1) The description says west_sec is the "standard offset in seconds west of UTC", but the code stores the standard offset in seconds EAST of UTC in that field (ZoneState is called with zone.standardEast; the test expects west_sec == east - daylight, so IST +5:30 gives 19800), and the debt entry says the same ("holds the standard-time offset in seconds east of UTC, despite its name"). The description and the code/debt disagree on the sign; please align the description. (2) HostZoneAt on POSIX uses ::timezone to split the offset into standard and daylight parts. POSIX does not require localtime_r to set timezone (only tzset/localtime do), so a tzset() call before reading it would make that independent of earlier calls. (3) The Windows branch reads the current _get_timezone/_get_dstbias, so instants under historical rules use today standard offset, which matches how libc localtime_s behaves here; worth a debt sentence. (4) The author did not test on Windows; the new RequireZone cases pass TZ strings like IST-5:30 and EST5EDT through _putenv_s, which we can run on Windows 10 with guest_freebsd_clocks if that helps.

AI-assisted (Claude Code).

@sguckiran

Copy link
Copy Markdown
Contributor Author

Please check updated description. Thanks.

@0xRickx

0xRickx commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

These are notes on how the PS5 libc.prx that homebrew bundles in sce_module uses these two calls. I read Prospero Explorer's (PPSA99109); offsets are in its text segment. One of them affects *dst_seconds.

  • localtime (0x70c60) and localtime_r (0x71130) call sceKernelConvertUtcToLocaltime(t, &local, &timesec, NULL) and fail on a negative return. They build the tm from local with tm_isdst = (timesec.dst_sec != 0). They never read timesec.t or west_sec.
  • mktime (0x70d20) calls sceKernelConvertLocaltimeToUtc(local, tm->tm_isdst, &utc, &timesec, &dst_seconds). The second argument, reserved here, is the caller's tm_isdst.
    • It converts utc back with sceKernelConvertUtcToLocaltime and compares the two west_sec values for equality only. It recomputes when they differ.
    • When the round trip matches, tm_isdst > 0 and the returned timesec.dst_sec is 0, it subtracts dst_seconds from both utc and the local time and sets tm_isdst = 0 (0x70ebb-0x70ee5). That branch exists to read a summer-time wall clock given outside summer time, so it only does something if dst_seconds is the zone's summer-time amount (3600) even when summer time is not in effect.
    • With this PR, dst_seconds is 0 outside summer time. So mktime of a winter date with tm_isdst = 1 keeps the hour instead of moving it back by one, unlike what that branch is written for.

So I think *dst_seconds from sceKernelConvertLocaltimeToUtc should be the zone's daylight saving amount whether or not it is in effect at that time, while timesec.dst_sec stays the amount in effect. The tm_isdst argument could also resolve the repeated hour.

AI-assisted (Claude Code).

…elConvertLocaltimeToUtc

libc mktime subtracts it when tm_isdst is set outside summer time, so it
must not be zero there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iLxaSz4qjmirfqKZdhVQk
@sguckiran

Copy link
Copy Markdown
Contributor Author

Thanks, this is really useful. You’re right: with dst_seconds at 0 outside summer time, that mktime branch never fires. Fixed in 15969f2: sceKernelConvertLocaltimeToUtc now writes the zone’s daylight saving amount to *dst_seconds whether or not it’s in effect (found by checking the host zone over the surrounding year, so it works in both hemispheres, and 0 for zones without DST), while timesec.dst_sec stays the amount in effect for localtime’s tm_isdst. The test now expects 3600 for EST5EDT in January as well as July, and the debt entry cites your PPSA99109 offsets (0x70ebb-0x70ee5).

I left reserved/tm_isdst unused for now: I can’t check how the console uses it to pick the hour when the clock goes back, so it’s recorded as debt rather than guessed. Happy to add it in this PR or a follow-up if you’ve seen how the kernel side uses it.

AI-assisted (Claude Code).

@blawesom

Copy link
Copy Markdown
Contributor

Rebase handoff — rebased onto current main (both commits), authorship unchanged.

Conflicts resolved across the two commits: main's settime-validation tests (SettimeValidatesLikeFreebsd and its helpers) and this PR's zone-conversion tests (RequireZone, now in its 5-parameter form from the second commit) are both kept, with main's <cstring> and this PR's <ctime> includes together.

Verified on Linux (CachyOS, kernel 7.2.9, GCC 16.2.1, Ninja, Release, BUILD_TESTING=ON, AMD RX 9070 XT / RADV):

To pick it up:

git fetch https://github.com/blawesom/AnyPS5 rebase/5096
git checkout <your branch>
git merge --ff-only FETCH_HEAD

Head is 59e30690f.

# Conflicts:
#	core/libs/tests/GuestFreebsdClocks.cpp
@sguckiran

Copy link
Copy Markdown
Contributor Author

Status update — all review points so far are addressed (head bb5f7d7):

  • west_sec now holds the standard offset in seconds east of UTC, matching PS5SX2's console observation and shadPS4 (ae7f5e3).
  • tzset() / _tzset() is called before reading the host zone offsets; the Windows debt note about current-rule offsets is added (1614d82).
  • sceKernelConvertLocaltimeToUtc reports the zone's daylight saving amount in *dst_seconds even outside summer time, so the bundled libc mktime (PPSA99109, 0x70ebb–0x70ee5) can shift a tm_isdst given in winter; timesec.dst_sec stays the amount in effect (15969f2). The tm_isdst passed as the second argument is still ignored and recorded as debt.
  • Merged current main; the GuestFreebsdClocks.cpp conflict keeps main's settime tests and the zone tests (bb5f7d7).

Tested on Ubuntu 24.04 / GCC 13.3: guest_freebsd_clocks passes, including without zoneinfo files and with a non-UTC host zone; conventions check passes. Thanks @blawesom for the full-suite run (1099/1100, only the known #4727 tessellation failure).

Still untested on Windows. Could a maintainer start the full Build for this PR when convenient?

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
…ndows

A TZ variable does not set the UCRT's _dstbias, so after the system zone
of a CI runner without daylight saving it stays 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iLxaSz4qjmirfqKZdhVQk
@orishamir

Copy link
Copy Markdown
Contributor

Windows run, since the description says Windows wasn't tested. Windows 11, RTX 4080, WinLibs GCC 15.2.0 r7, Release, this head (f5d856d) merged onto main e34c479. The host zone is Israel Standard Time (UTC+3 with summer time today), so _get_dstbias is -3600 and the Windows summer time path runs.

  • guest_freebsd_clocks passes 5 of 5. The full ctest has 0 failures out of 1198; the 14 skips come from the tests' own checks.
  • With only core/libs/prx/libkernel/Time/Time.cpp taken back to main, guest_freebsd_clocks aborts (exit 0xC0000409).
  • PS5SX2 (PPSA99203, libkernel/libc: the time zone is UTC in sceKernelConvertUtcToLocaltime but the host's in localtime #5067), 45 s on its start screen, no TZ variable set, so the C runtime reads the system zone. On main its clock shows 16:44 at 16:44 UTC. With this PR it shows 19:47 at 16:47 UTC, which is the local time. Otherwise the two runs match: 16 dumped frames each and no new stderr line.

Not covered: a Windows system zone without summer time, like a UTC CI runner. I didn't change this machine's zone.

AI-assisted: yes (Claude Code Opus 5.5)

@adriadam10 adriadam10 self-assigned this Oct 14, 2026
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked on Linux: the build and the libkernel ctest scope pass (guest_freebsd_clocks included, and it aborts without the Time.cpp change). Also reviewed the sign handling of tm_gmtoff/timezone for the standard and daylight parts, the two-pass UTC lookup, and the debt entry. The Windows run by @orishamir covers the _get_timezone/_get_dstbias path. Merging once CI is green.

@adriadam10
adriadam10 merged commit 4b11b97 into boykopovar:main Oct 14, 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.

libkernel/libc: the time zone is UTC in sceKernelConvertUtcToLocaltime but the host's in localtime

7 participants