View Issue Details

IDProjectCategoryView StatusLast Update
0001572OpenMPTGeneralpublic2022-05-28 05:20
Reportermanx Assigned Tomanx  
PrioritynormalSeverityminorReproducibilityhave not tried
Status assignedResolutionopen 
Product VersionOpenMPT 1.31.00.* (current testing) 
Target VersionOpenMPT 1.31 / libopenmpt 0.7 (goals) 
Summary0001572: use more std::chrono and less ctime
Description

Getting rid of C time.h simplifies date calculations and solves all year2038 problems, amongst other things.

TagsNo tags attached.
Has the bug occurred in previous versions?
Tested code revision (in case you know it)

Relationships

related to 0001555 resolvedmanx bump minimum requirements to Windows 10 1903 (Build 18362) for Windows 10 builds 

Activities

manx

manx

2022-02-23 14:39

administrator   ~0005113

[Ref] mptTime: Use std::chrono for gregorian date calculations in C++20 mode.
[Ref] mptTime: Use std::chrono::system_clock for unix time instead of time_t in C++20 mode. This automatically makes all date handling year2038-correct in C++20 mode.
[Ref] mptTime: Use a custom gregorian date type mpt::Date::UTC instead of C struct tm.
[Ref] UpdateCheck: Use mpt::Date::Unix instead of time_t.
[Mod] UpdateCheck: Store last update timestamp as Unix seconds instead of YYYY-MM-DD HH:MM. On update, reset to 0.
[Mod] Edit History: For IT/MPTM, only ever store timestamps in UTC internally instead of localtime. Only convert to localtime when displaying to the user.
[Mod] Edit History: For other formats, also assume that the date is stored in UTC.
[Ref] mptTime: Disable std::chrono date handling completely for MSVC when targeting Windows earlier than Windows 10 1903 due to <https://github.com/microsoft/STL/issues/1911> and <https://github.com/microsoft/STL/issues/2163>.
[Ref] mptTime: Disable std::chrono for date parsing in VS2022 due to <https://developercommunity.visualstudio.com/t/stdchronoget-tzdb-list-memory-leak/1644641> / <https://github.com/microsoft/STL/issues/2504>.
chrono-v8.patch (34,303 bytes)   
Index: common/mptTime.cpp
===================================================================
--- common/mptTime.cpp	(revision 16961)
+++ common/mptTime.cpp	(working copy)
@@ -13,7 +13,12 @@
 
 #include "mptStringBuffer.h"
 
-#include <time.h>
+#if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+#include <chrono>
+#if 0
+#include <format>
+#endif
+#endif
 
 #if MPT_OS_WINDOWS
 #include <windows.h>
@@ -81,23 +86,8 @@
 
 #endif // MODPLUG_TRACKER
 
-Unix::Unix()
-	: Value(0)
-{
-	return;
-}
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 
-Unix::Unix(int64 unixtime)
-	: Value(unixtime)
-{
-	return;
-}
-
-Unix::operator int64 () const
-{
-	return Value;
-}
-
 static int32 ToDaynum(int32 year, int32 month, int32 day)
 {
 	month = (month + 9) % 12;
@@ -128,77 +118,55 @@
 	day = static_cast<int32>(dd);
 }
 
-mpt::Date::Unix Unix::FromUTC(tm timeUtc)
+mpt::Date::Unix UnixFromUTC(UTC timeUtc)
 {
-	int32 daynum = ToDaynum(timeUtc.tm_year+1900, timeUtc.tm_mon+1, timeUtc.tm_mday);
-	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970,1,1))*24*60*60 + timeUtc.tm_hour*60*60 + timeUtc.tm_min*60 + timeUtc.tm_sec;
-	return mpt::Date::Unix(seconds);
+	int32 daynum = ToDaynum(timeUtc.year, timeUtc.month, timeUtc.day);
+	int64 seconds = static_cast<int64>(daynum - ToDaynum(1970, 1, 1)) * 24 * 60 * 60 + timeUtc.hours * 60 * 60 + timeUtc.minutes * 60 + timeUtc.seconds;
+	return Unix{seconds};
 }
 
-tm Unix::AsUTC() const 
+mpt::Date::UTC UnixAsUTC(Unix tp)
 {
-	int64 tmp = Value;
+	int64 tmp = tp.value;
 	int64 seconds = tmp % 60; tmp /= 60;
 	int64 minutes = tmp % 60; tmp /= 60;
 	int64 hours   = tmp % 24; tmp /= 24;
 	int32 year = 0, month = 0, day = 0;
 	FromDaynum(static_cast<int32>(tmp) + ToDaynum(1970,1,1), year, month, day);
-	tm result = {};
-	result.tm_year = year - 1900;
-	result.tm_mon = month - 1;
-	result.tm_mday = day;
-	result.tm_hour = static_cast<int32>(hours);
-	result.tm_min = static_cast<int32>(minutes);
-	result.tm_sec = static_cast<int32>(seconds);
+	mpt::Date::UTC result = {};
+	result.year = year;
+	result.month = month;
+	result.day = day;
+	result.hours = static_cast<int32>(hours);
+	result.minutes = static_cast<int32>(minutes);
+	result.seconds = static_cast<int64>(seconds);
 	return result;
 }
 
-mpt::ustring ToShortenedISO8601(tm date)
+#endif
+
+mpt::ustring ToShortenedISO8601(mpt::Date::UTC date)
 {
-	// We assume date in UTC here.
-	// There are too many differences in supported format specifiers in strftime()
-	// and strftime does not support reduced precision ISO8601 at all.
-	// Just do the formatting ourselves.
 	mpt::ustring result;
 	mpt::ustring tz = U_("Z");
-	if(date.tm_year == 0)
+	if(date.year == 0)
 	{
 		return result;
 	}
-	result += mpt::ufmt::dec0<4>(date.tm_year + 1900);
-	if(date.tm_mon < 0 || date.tm_mon > 11)
+	result += mpt::ufmt::dec0<4>(date.year);
+	result += U_("-") + mpt::ufmt::dec0<2>(date.month);
+	result += U_("-") + mpt::ufmt::dec0<2>(date.day);
+	if(date.hours == 0 && date.minutes == 0 && date.seconds)
 	{
 		return result;
 	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mon + 1);
-	if(date.tm_mday < 1 || date.tm_mday > 31)
-	{
-		return result;
-	}
-	result += U_("-") + mpt::ufmt::dec0<2>(date.tm_mday);
-	if(date.tm_hour == 0 && date.tm_min == 0 && date.tm_sec == 0)
-	{
-		return result;
-	}
-	if(date.tm_hour < 0 || date.tm_hour > 23)
-	{
-		return result;
-	}
-	if(date.tm_min < 0 || date.tm_min > 59)
-	{
-		return result;
-	}
 	result += U_("T");
-	if(date.tm_isdst > 0)
+	result += mpt::ufmt::dec0<2>(date.hours) + U_(":") + mpt::ufmt::dec0<2>(date.minutes);
+	if(date.seconds == 0)
 	{
-		tz = U_("+01:00");
-	}
-	result += mpt::ufmt::dec0<2>(date.tm_hour) + U_(":") + mpt::ufmt::dec0<2>(date.tm_min);
-	if(date.tm_sec < 0 || date.tm_sec > 61)
-	{
 		return result + tz;
 	}
-	result += U_(":") + mpt::ufmt::dec0<2>(date.tm_sec);
+	result += U_(":") + mpt::ufmt::dec0<2>(date.seconds);
 	result += tz;
 	return result;
 }
Index: common/mptTime.h
===================================================================
--- common/mptTime.h	(revision 16961)
+++ common/mptTime.h	(working copy)
@@ -12,9 +12,14 @@
 
 #include "openmpt/all/BuildSettings.hpp"
 
+#if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+#include <chrono>
+#endif
 #include <string>
 
+#if MPT_CXX_BEFORE(20) || defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
 #include <time.h>
+#endif
 
 
 OPENMPT_NAMESPACE_BEGIN
@@ -44,22 +49,124 @@
 
 #endif // MODPLUG_TRACKER
 
-class Unix
+struct UTC
 {
+	int          year    = 0;
+	unsigned int month   = 0;
+	unsigned int day     = 0;
+	int32        hours   = 0;
+	int32        minutes = 0;
+	int64        seconds = 0;
+	friend bool operator==(const UTC& lhs, const UTC& rhs)
+	{
+		return true
+			&& lhs.year == rhs.year
+			&& lhs.month == rhs.month
+			&& lhs.day == rhs.day
+			&& lhs.hours == rhs.hours
+			&& lhs.minutes == rhs.minutes
+			&& lhs.seconds == rhs.seconds
+			;
+	}
+	friend bool operator!=(const UTC& lhs, const UTC& rhs)
+	{
+		return false
+			|| lhs.year != rhs.year
+			|| lhs.month != rhs.month
+			|| lhs.day != rhs.day
+			|| lhs.hours != rhs.hours
+			|| lhs.minutes != rhs.minutes
+			|| lhs.seconds != rhs.seconds
+			;
+	}
+};
+
+#if MPT_CXX_AT_LEAST(20) && !defined(MPT_LIBCXX_QUIRK_NO_CHRONO_DATE)
+
+using Unix = std::chrono::system_clock::time_point;
+
+inline Unix UnixNow()
+{
+	return std::chrono::system_clock::now();
+}
+
+inline int64 UnixAsSeconds(Unix tp)
+{
+	return std::chrono::duration_cast<std::chrono::seconds>(tp.time_since_epoch()).count();
+}
+
+inline Unix UnixFromSeconds(int64 seconds)
+{
+	return std::chrono::system_clock::time_point{std::chrono::seconds{seconds}};
+}
+
+inline mpt::Date::Unix UnixFromUTC(UTC utc)
+{
+	std::chrono::year_month_day ymd =
+		std::chrono::year{utc.year} /
+		std::chrono::month{utc.month} /
+		std::chrono::day{utc.day};
+	std::chrono::hh_mm_ss<std::chrono::seconds> hms{
+		std::chrono::hours{utc.hours} +
+		std::chrono::minutes{utc.minutes} +
+		std::chrono::seconds{utc.seconds}};
+	return std::chrono::system_clock::time_point{static_cast<std::chrono::sys_days>(ymd)} + hms.to_duration();
+}
+
+inline mpt::Date::UTC UnixAsUTC(Unix tp)
+{
+	std::chrono::sys_days dp = std::chrono::floor<std::chrono::days>(tp);
+	std::chrono::year_month_day ymd{dp};
+	std::chrono::hh_mm_ss hms{tp - dp};
+	mpt::Date::UTC result;
+	result.year = static_cast<int>(ymd.year());
+	result.month = static_cast<unsigned int>(ymd.month());
+	result.day = static_cast<unsigned int>(ymd.day());
+	result.hours = hms.hours().count();
+	result.minutes = hms.minutes().count();
+	result.seconds = hms.seconds().count();
+	return result;
+}
+
+#else
+
 // int64 counts 1s since 1970-01-01T00:00Z
-private:
-	int64 Value;
-public:
-	Unix();
-	explicit Unix(int64 unixtime);
-	operator int64 () const;
-public:
-	static mpt::Date::Unix FromUTC(tm timeUtc);
-	tm AsUTC() const;
+struct Unix
+{
+	int64 value{};
+	friend bool operator==(const Unix &a, const Unix &b)
+	{
+		return a.value == b.value;
+	}
+	friend bool operator!=(const Unix &a, const Unix &b)
+	{
+		return a.value != b.value;
+	}
 };
 
-mpt::ustring ToShortenedISO8601(tm date); // i.e. 2015-01-15T18:32:01Z
+inline Unix UnixNow()
+{
+	return Unix{static_cast<int64>(time(nullptr))};
+}
 
+inline int64 UnixAsSeconds(Unix tp)
+{
+	return tp.value;
+}
+
+inline Unix UnixFromSeconds(int64 seconds)
+{
+	return Unix{seconds};
+}
+
+mpt::Date::Unix UnixFromUTC(UTC timeUtc);
+
+mpt::Date::UTC UnixAsUTC(Unix tp);
+
+#endif
+
+mpt::ustring ToShortenedISO8601(UTC date); // i.e. 2015-01-15T18:32:01Z
+
 } // namespace Date
 } // namespace mpt
 
Index: common/versionNumber.h
===================================================================
--- common/versionNumber.h	(revision 16961)
+++ common/versionNumber.h	(working copy)
@@ -18,6 +18,6 @@
 #define VER_MAJORMAJOR  1
 #define VER_MAJOR      31
 #define VER_MINOR      00
-#define VER_MINORMINOR 09
+#define VER_MINORMINOR 10
 
 OPENMPT_NAMESPACE_END
Index: mptrack/dlg_misc.cpp
===================================================================
--- mptrack/dlg_misc.cpp	(revision 16961)
+++ mptrack/dlg_misc.cpp	(working copy)
@@ -1311,18 +1311,8 @@
 	for(const auto &entry : editHistory)
 	{
 		totalTime += entry.openTime;
-
 		// Date
-		CString sDate;
-		if(entry.HasValidDate())
-		{
-			TCHAR szDate[32];
-			_tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &entry.loadDate);
-			sDate = szDate;
-		} else
-		{
-			sDate = _T("<unknown date>");
-		}
+		CString sDate = entry.HasValidDate() ? CTime(mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(entry.loadDate))).Format(_T("%d %b %Y, %H:%M:%S")) : CString(_T("<unknown date>"));
 		// Time + stuff
 		uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION);
 		s += MPT_CFORMAT("Loaded {}, open for {}h {}m {}s\r\n")(
Index: mptrack/MainFrm.cpp
===================================================================
--- mptrack/MainFrm.cpp	(revision 16961)
+++ mptrack/MainFrm.cpp	(working copy)
@@ -2784,8 +2784,10 @@
 	const bool isAutoUpdate = CUpdateCheck::IsAutoUpdateFromMessage(wparam, lparam);
 	const UpdateCheckResult &result = CUpdateCheck::MessageAsResult(wparam, lparam);
 	CUpdateCheck::AcknowledgeSuccess(result);
-	if(result.CheckTime != time_t{})
-		TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(result.CheckTime);
+	if(result.CheckTime != mpt::Date::Unix{})
+	{
+		TrackerSettings::Instance().UpdateLastUpdateCheck = result.CheckTime;
+	}
 	if(!isAutoUpdate)
 	{
 		if(m_UpdateOptionsDialog)
Index: mptrack/Moddoc.cpp
===================================================================
--- mptrack/Moddoc.cpp	(revision 16961)
+++ mptrack/Moddoc.cpp	(working copy)
@@ -133,7 +133,7 @@
 	, m_InstrumentUndo(*this)
 {
 	// Set the creation date of this file (or the load time if we're loading an existing file)
-	time(&m_creationTime);
+	m_creationTime = mpt::Date::UnixNow();
 
 	ReinitRecordState();
 
Index: mptrack/Moddoc.h
===================================================================
--- mptrack/Moddoc.h	(revision 16961)
+++ mptrack/Moddoc.h	(working copy)
@@ -14,10 +14,10 @@
 
 #include "Sndfile.h"
 #include "../common/misc_util.h"
+#include "../common/mptTime.h"
 #include "Undo.h"
 #include "Notification.h"
 #include "UpdateHints.h"
-#include <time.h>
 
 OPENMPT_NAMESPACE_BEGIN
 
@@ -130,7 +130,7 @@
 	CSampleUndo m_SampleUndo;
 	CInstrumentUndo m_InstrumentUndo;
 	SplitKeyboardSettings m_SplitKeyboardSettings;	// this is maybe not the best place to keep them, but it should do the job
-	time_t m_creationTime;
+	mpt::Date::Unix m_creationTime;
 
 	std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave?
 
@@ -221,7 +221,7 @@
 	CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; }
 	SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; }
 
-	time_t GetCreationTime() const { return m_creationTime; }
+	mpt::Date::Unix GetCreationTime() const { return m_creationTime; }
 
 // operations
 public:
Index: mptrack/TrackerSettings.cpp
===================================================================
--- mptrack/TrackerSettings.cpp	(revision 16961)
+++ mptrack/TrackerSettings.cpp	(working copy)
@@ -326,7 +326,7 @@
 	// Update
 	, UpdateEnabled(conf, U_("Update"), U_("Enabled"), true)
 	, UpdateInstallAutomatically(conf, U_("Update"), U_("InstallAutomatically"), false)
-	, UpdateLastUpdateCheck(conf, U_("Update"), U_("LastUpdateCheck"), mpt::Date::Unix(time_t()))
+	, UpdateLastUpdateCheck(conf, U_("Update"), U_("LastUpdateCheck"), mpt::Date::Unix{})
 	, UpdateUpdateCheckPeriod_DEPRECATED(conf, U_("Update"), U_("UpdateCheckPeriod"), 7)
 	, UpdateIntervalDays(conf, U_("Update"), U_("UpdateCheckIntervalDays"), 7)
 	, UpdateChannel(conf, U_("Update"), U_("Channel"), UpdateChannelRelease)
@@ -822,6 +822,10 @@
 		conf.Forget(UpdateUpdateURL_DEPRECATED.GetPath());
 		conf.Forget(UpdateSendGUID_DEPRECATED.GetPath());
 	}
+	if(storedVersion < MPT_V("1.31.00.10"))
+	{
+		UpdateLastUpdateCheck = mpt::Date::Unix{};
+	}
 #endif // MPT_ENABLE_UPDATE
 
 	if(storedVersion < MPT_V("1.29.00.39"))
Index: mptrack/TrackerSettings.h
===================================================================
--- mptrack/TrackerSettings.h	(revision 16961)
+++ mptrack/TrackerSettings.h	(working copy)
@@ -471,32 +471,12 @@
 
 template<> inline SettingValue ToSettingValue(const mpt::Date::Unix &val)
 {
-	time_t t = val;
-	const tm* lastUpdate = gmtime(&t);
-	CString outDate;
-	if(lastUpdate)
-	{
-		outDate.Format(_T("%04d-%02d-%02d %02d:%02d"), lastUpdate->tm_year + 1900, lastUpdate->tm_mon + 1, lastUpdate->tm_mday, lastUpdate->tm_hour, lastUpdate->tm_min);
-	}
-	return SettingValue(mpt::ToUnicode(outDate), "UTC");
+	return SettingValue(mpt::ufmt::val(mpt::Date::UnixAsSeconds(val)), "UnixTime");
 }
 template<> inline mpt::Date::Unix FromSettingValue(const SettingValue &val)
 {
-	MPT_ASSERT(val.GetTypeTag() == "UTC");
-	std::string s = mpt::ToCharset(mpt::Charset::Locale, val.as<mpt::ustring>());
-	tm lastUpdate;
-	MemsetZero(lastUpdate);
-	if(sscanf(s.c_str(), "%04d-%02d-%02d %02d:%02d", &lastUpdate.tm_year, &lastUpdate.tm_mon, &lastUpdate.tm_mday, &lastUpdate.tm_hour, &lastUpdate.tm_min) == 5)
-	{
-		lastUpdate.tm_year -= 1900;
-		lastUpdate.tm_mon--;
-	}
-	time_t outTime = mpt::Date::Unix::FromUTC(lastUpdate);
-	if(outTime < 0)
-	{
-		outTime = 0;
-	}
-	return mpt::Date::Unix(outTime);
+	MPT_ASSERT(val.GetTypeTag() == "UnixTime");
+	return mpt::Date::UnixFromSeconds(mpt::ConvertStringTo<int64>(val.as<mpt::ustring>()));
 }
 
 struct FontSetting
Index: mptrack/UpdateCheck.cpp
===================================================================
--- mptrack/UpdateCheck.cpp	(revision 16961)
+++ mptrack/UpdateCheck.cpp	(working copy)
@@ -453,11 +453,11 @@
 			return;
 		}
 		// Do we actually need to run the update check right now?
-		const time_t now = time(nullptr);
-		const time_t lastCheck = TrackerSettings::Instance().UpdateLastUpdateCheck.Get();
+		const mpt::Date::Unix now = mpt::Date::UnixNow();
+		const mpt::Date::Unix lastCheck = TrackerSettings::Instance().UpdateLastUpdateCheck.Get();
 		// Check update interval. Note that we always check for updates when the system time had gone backwards (i.e. when the last update check supposedly happened in the future).
-		const double secsSinceLastCheck = difftime(now, lastCheck);
-		if(secsSinceLastCheck > 0.0 && secsSinceLastCheck < updateCheckPeriod * 86400.0)
+		const int64 secsSinceLastCheck = mpt::Date::UnixAsSeconds(now) - mpt::Date::UnixAsSeconds(lastCheck);
+		if(secsSinceLastCheck > 0 && secsSinceLastCheck < updateCheckPeriod * 86400)
 		{
 			loadPersisted = true;
 		}
@@ -475,7 +475,7 @@
 				);
 			if(Reporting::Confirm(msg, _T("OpenMPT Update")) == cnfNo)
 			{
-				TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(now);
+				TrackerSettings::Instance().UpdateLastUpdateCheck = now;
 				return;
 			}
 		}
@@ -650,7 +650,7 @@
 				{
 					std::vector<std::byte> data = GetFileReader(f).ReadRawDataAsByteVector();
 					nlohmann::json::parse(mpt::buffer_cast<std::string>(data)).get<Update::versions>();
-					result.CheckTime = time_t{};
+					result.CheckTime = mpt::Date::Unix{};
 					result.json = data;
 					loaded = true;
 				}
@@ -781,7 +781,7 @@
 
 	// Now, evaluate the downloaded data.
 	UpdateCheckResult result;
-	result.CheckTime = time(nullptr);
+	result.CheckTime = mpt::Date::UnixNow();
 	try
 	{
 		nlohmann::json::parse(mpt::buffer_cast<std::string>(resultHTTP.Data)).get<Update::versions>();
@@ -1315,7 +1315,7 @@
 {
 	if(!result.IsFromCache())
 	{
-		TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(result.CheckTime);
+		TrackerSettings::Instance().UpdateLastUpdateCheck = result.CheckTime;
 	}
 }
 
@@ -1612,14 +1612,10 @@
 	if(changedPath == TrackerSettings::Instance().UpdateLastUpdateCheck.GetPath())
 	{
 		CString updateText;
-		const time_t t = TrackerSettings::Instance().UpdateLastUpdateCheck.Get();
-		if(t > 0)
+		const mpt::Date::Unix t = TrackerSettings::Instance().UpdateLastUpdateCheck.Get();
+		if(t != mpt::Date::Unix{})
 		{
-			const tm* const lastUpdate = localtime(&t);
-			if(lastUpdate != nullptr)
-			{
-				updateText.Format(_T("The last successful update check was run on %04d-%02d-%02d, %02d:%02d."), lastUpdate->tm_year + 1900, lastUpdate->tm_mon + 1, lastUpdate->tm_mday, lastUpdate->tm_hour, lastUpdate->tm_min);
-			}
+			updateText += CTime(mpt::Date::UnixAsSeconds(t)).Format(_T("The last successful update check was run on %F, %R."));
 		}
 		updateText += _T("\r\n");
 		SetDlgItemText(IDC_LASTUPDATE, updateText);
Index: mptrack/UpdateCheck.h
===================================================================
--- mptrack/UpdateCheck.h	(revision 16961)
+++ mptrack/UpdateCheck.h	(working copy)
@@ -14,7 +14,7 @@
 
 #include "mpt/uuid/uuid.hpp"
 
-#include <time.h>
+#include "../common/mptTime.h"
 
 #include <atomic>
 
@@ -40,9 +40,12 @@
 
 struct UpdateCheckResult
 {
-	time_t CheckTime = time_t{};
+	mpt::Date::Unix CheckTime = mpt::Date::Unix{};
 	std::vector<std::byte> json;
-	bool IsFromCache() const noexcept { return CheckTime == time_t{}; }
+	bool IsFromCache() const noexcept
+	{
+		return CheckTime == mpt::Date::Unix{};
+	}
 };
 
 class CUpdateCheck
Index: soundlib/ITTools.cpp
===================================================================
--- soundlib/ITTools.cpp	(revision 16961)
+++ soundlib/ITTools.cpp	(working copy)
@@ -637,15 +637,15 @@
 void ITHistoryStruct::ConvertToMPT(FileHistory &mptHistory) const
 {
 	// Decode FAT date and time
-	MemsetZero(mptHistory.loadDate);
+	mptHistory.loadDate = mpt::Date::UTC{};
 	if(fatdate != 0 || fattime != 0)
 	{
-		mptHistory.loadDate.tm_year = ((fatdate >> 9) & 0x7F) + 80;
-		mptHistory.loadDate.tm_mon = Clamp((fatdate >> 5) & 0x0F, 1, 12) - 1;
-		mptHistory.loadDate.tm_mday = Clamp(fatdate & 0x1F, 1, 31);
-		mptHistory.loadDate.tm_hour = Clamp((fattime >> 11) & 0x1F, 0, 23);
-		mptHistory.loadDate.tm_min = Clamp((fattime >> 5) & 0x3F, 0, 59);
-		mptHistory.loadDate.tm_sec = Clamp((fattime & 0x1F) * 2, 0, 59);
+		mptHistory.loadDate.year = ((fatdate >> 9) & 0x7F) + 1980;
+		mptHistory.loadDate.month = Clamp((fatdate >> 5) & 0x0F, 1, 12);
+		mptHistory.loadDate.day = Clamp(fatdate & 0x1F, 1, 31);
+		mptHistory.loadDate.hours = Clamp((fattime >> 11) & 0x1F, 0, 23);
+		mptHistory.loadDate.minutes = Clamp((fattime >> 5) & 0x3F, 0, 59);
+		mptHistory.loadDate.seconds = Clamp((fattime & 0x1F) * 2, 0, 59);
 	}
 	mptHistory.openTime = static_cast<uint32>(runtime * (HISTORY_TIMER_PRECISION / 18.2));
 }
@@ -657,8 +657,8 @@
 	// Create FAT file dates
 	if(mptHistory.HasValidDate())
 	{
-		fatdate = static_cast<uint16>(mptHistory.loadDate.tm_mday | ((mptHistory.loadDate.tm_mon + 1) << 5) | ((mptHistory.loadDate.tm_year - 80) << 9));
-		fattime = static_cast<uint16>((mptHistory.loadDate.tm_sec / 2) | (mptHistory.loadDate.tm_min << 5) | (mptHistory.loadDate.tm_hour << 11));
+		fatdate = static_cast<uint16>(mptHistory.loadDate.day | (mptHistory.loadDate.month << 5) | ((mptHistory.loadDate.year - 1980) << 9));
+		fattime = static_cast<uint16>((mptHistory.loadDate.seconds / 2) | (mptHistory.loadDate.minutes << 5) | (mptHistory.loadDate.hours << 11));
 	} else
 	{
 		fatdate = 0;
Index: soundlib/Load_dmf.cpp
===================================================================
--- soundlib/Load_dmf.cpp	(revision 16961)
+++ soundlib/Load_dmf.cpp	(working copy)
@@ -911,9 +911,9 @@
 	m_songArtist = mpt::ToUnicode(mpt::Charset::CP437, mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.composer));
 
 	FileHistory mptHistory;
-	mptHistory.loadDate.tm_mday = Clamp(fileHeader.creationDay, uint8(1), uint8(31));
-	mptHistory.loadDate.tm_mon = Clamp(fileHeader.creationMonth, uint8(1), uint8(12)) - 1;
-	mptHistory.loadDate.tm_year = fileHeader.creationYear;
+	mptHistory.loadDate.day = Clamp(fileHeader.creationDay, uint8(1), uint8(31));
+	mptHistory.loadDate.month = Clamp(fileHeader.creationMonth, uint8(1), uint8(12));
+	mptHistory.loadDate.year = 1900 + fileHeader.creationYear;
 	m_FileHistory.clear();
 	m_FileHistory.push_back(mptHistory);
 
Index: soundlib/Load_gt2.cpp
===================================================================
--- soundlib/Load_gt2.cpp	(revision 16961)
+++ soundlib/Load_gt2.cpp	(working copy)
@@ -1214,9 +1214,9 @@
 	m_playBehaviour.set(kApplyOffsetWithoutNote);
 
 	FileHistory mptHistory;
-	mptHistory.loadDate.tm_mday = Clamp<uint8, uint8>(fileHeader.day, 1, 31);
-	mptHistory.loadDate.tm_mon = Clamp<uint8, uint8>(fileHeader.month, 1, 12) - 1;
-	mptHistory.loadDate.tm_year = fileHeader.year - 1900;
+	mptHistory.loadDate.day = Clamp<uint8, uint8>(fileHeader.day, 1, 31);
+	mptHistory.loadDate.month = Clamp<uint8, uint8>(fileHeader.month, 1, 12);
+	mptHistory.loadDate.year = fileHeader.year;
 	m_FileHistory.push_back(mptHistory);
 
 	m_nChannels = 32;
Index: soundlib/Load_it.cpp
===================================================================
--- soundlib/Load_it.cpp	(revision 16961)
+++ soundlib/Load_it.cpp	(working copy)
@@ -1352,17 +1352,9 @@
 		} else if(pModDoc != nullptr)
 		{
 			// Current ("new") timestamp
-			const time_t creationTime = pModDoc->GetCreationTime();
-
-			MemsetZero(mptHistory.loadDate);
-			//localtime_s(&loadDate, &creationTime);
-			const tm* const p = localtime(&creationTime);
-			if (p != nullptr)
-				mptHistory.loadDate = *p;
-			else
-				sndFile.AddToLog(LogError, U_("Unable to retrieve current time."));
-
-			mptHistory.openTime = (uint32)(difftime(time(nullptr), creationTime) * HISTORY_TIMER_PRECISION);
+			const mpt::Date::Unix creationTime = pModDoc->GetCreationTime();
+			mptHistory.loadDate = mpt::Date::UnixAsUTC(creationTime);
+			mptHistory.openTime = static_cast<uint32>(mpt::round((mpt::Date::UnixAsSeconds(mpt::Date::UnixNow()) - mpt::Date::UnixAsSeconds(creationTime)) * HISTORY_TIMER_PRECISION));
 #endif // MODPLUG_TRACKER
 		}
 
Index: soundlib/Load_mod.cpp
===================================================================
--- soundlib/Load_mod.cpp	(revision 16961)
+++ soundlib/Load_mod.cpp	(working copy)
@@ -2180,12 +2180,12 @@
 		   && mpt::is_in_range(info.dateMinute, 0, 59) && mpt::is_in_range(info.dateSecond, 0, 59))
 		{
 			FileHistory mptHistory;
-			mptHistory.loadDate.tm_year = info.dateYear;
-			mptHistory.loadDate.tm_mon = info.dateMonth - 1;
-			mptHistory.loadDate.tm_mday = info.dateDay;
-			mptHistory.loadDate.tm_hour = info.dateHour;
-			mptHistory.loadDate.tm_min = info.dateMinute;
-			mptHistory.loadDate.tm_sec = info.dateSecond;
+			mptHistory.loadDate.year = info.dateYear + 1900;
+			mptHistory.loadDate.month = info.dateMonth;
+			mptHistory.loadDate.day = info.dateDay;
+			mptHistory.loadDate.hours = info.dateHour;
+			mptHistory.loadDate.minutes = info.dateMinute;
+			mptHistory.loadDate.seconds = info.dateSecond;
 			m_FileHistory.push_back(mptHistory);
 		}
 	}
Index: soundlib/Sndfile.cpp
===================================================================
--- soundlib/Sndfile.cpp	(revision 16961)
+++ soundlib/Sndfile.cpp	(working copy)
@@ -56,15 +56,15 @@
 
 mpt::ustring FileHistory::AsISO8601() const
 {
-	tm date = loadDate;
+	mpt::Date::UTC date = loadDate;
 	if(openTime > 0)
 	{
 		// Calculate the date when editing finished.
 		double openSeconds = static_cast<double>(openTime) / HISTORY_TIMER_PRECISION;
-		tm tmpLoadDate = loadDate;
-		int64 loadDateSinceEpoch = mpt::Date::Unix::FromUTC(tmpLoadDate);
+		mpt::Date::UTC tmpLoadDate = loadDate;
+		int64 loadDateSinceEpoch = mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(tmpLoadDate));
 		int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds);
-		date = mpt::Date::Unix(saveDateSinceEpoch).AsUTC();
+		date = mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(saveDateSinceEpoch));
 	}
 	return mpt::Date::ToShortenedISO8601(date);
 }
Index: soundlib/Sndfile.h
===================================================================
--- soundlib/Sndfile.h	(revision 16961)
+++ soundlib/Sndfile.h	(working copy)
@@ -243,13 +243,16 @@
 struct FileHistory
 {
 	// Date when the file was loaded in the the tracker or created.
-	tm loadDate = {};
+	mpt::Date::UTC loadDate = {};
 	// Time the file was open in the editor, in 1/18.2th seconds (frequency of a standard DOS timer, to keep compatibility with Impulse Tracker easy).
 	uint32 openTime = 0;
 	// Return the date as a (possibly truncated if not enough precision is available) ISO 8601 formatted date.
 	mpt::ustring AsISO8601() const;
 	// Returns true if the date component is valid. Some formats only store edit time, not edit date.
-	bool HasValidDate() const { return loadDate.tm_mday != 0; }
+	bool HasValidDate() const
+	{
+		return loadDate != mpt::Date::UTC{};
+	}
 };
 
 
Index: src/mpt/base/detect_quirks.hpp
===================================================================
--- src/mpt/base/detect_quirks.hpp	(revision 16961)
+++ src/mpt/base/detect_quirks.hpp	(working copy)
@@ -145,4 +145,34 @@
 
 
 
+#if MPT_CXX_AT_LEAST(20)
+#if MPT_LIBCXX_MS && MPT_OS_WINDOWS
+#if defined(NTDDI_VERSION)
+#if (NTDDI_VERSION < 0x0A000007) // < Windows 10 1903
+// std::chrono timezones require Windows 10 1903 with VS2022 as of 2022-01-22.
+// See <https://github.com/microsoft/STL/issues/1911>
+// and <https://github.com/microsoft/STL/issues/2163>.
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
+#endif
+#else
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
+#endif
+#endif
+#if MPT_LIBCXX_GNU_BEFORE(11)
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
+#elif MPT_LIBCXX_LLVM_BEFORE(7000)
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE
+#endif
+#if MPT_LIBCXX_MS
+// Causes massive memory leaks.
+// See <https://developercommunity.visualstudio.com/t/stdchronoget-tzdb-list-memory-leak/1644641>
+// / <https://github.com/microsoft/STL/issues/2504>.
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#elif MPT_LIBCXX_GNU
+#define MPT_LIBCXX_QUIRK_NO_CHRONO_DATE_PARSE
+#endif
+#endif
+
+
+
 #endif // MPT_BASE_DETECT_QUIRKS_HPP
Index: test/test.cpp
===================================================================
--- test/test.cpp	(revision 16961)
+++ test/test.cpp	(working copy)
@@ -777,48 +777,15 @@
 }
 
 
-namespace {
-
-struct Gregorian {
-	int Y,M,D,h,m,s;
-	static Gregorian FromTM(tm t) {
-		Gregorian g;
-		g.Y = t.tm_year + 1900;
-		g.M = t.tm_mon + 1;
-		g.D = t.tm_mday;
-		g.h = t.tm_hour;
-		g.m = t.tm_min;
-		g.s = t.tm_sec;
-		return g;
-	}
-	static tm ToTM(Gregorian g) {
-		tm t;
-		MemsetZero(t);
-		t.tm_year = g.Y - 1900;
-		t.tm_mon = g.M - 1;
-		t.tm_mday = g.D;
-		t.tm_hour = g.h;
-		t.tm_min = g.m;
-		t.tm_sec = g.s;
-		return t;
-	}
-};
-
-inline bool operator ==(Gregorian a, Gregorian b) {
-	return a.Y == b.Y && a.M == b.M && a.D == b.D && a.h == b.h && a.m == b.m && a.s == b.s;
+static int64 TestDate1(int s, int m, int h, unsigned int D, unsigned int M, int Y) {
+	return mpt::Date::UnixAsSeconds(mpt::Date::UnixFromUTC(mpt::Date::UTC{Y,M,D,h,m,s}));
 }
 
+static mpt::Date::UTC TestDate2(int s, int m, int h, unsigned int D, unsigned int M, int Y) {
+	return mpt::Date::UTC{Y,M,D,h,m,s};
 }
 
-static int64 TestDate1(int s, int m, int h, int D, int M, int Y) {
-	return mpt::Date::Unix::FromUTC(Gregorian::ToTM(Gregorian{Y,M,D,h,m,s}));
-}
 
-static Gregorian TestDate2(int s, int m, int h, int D, int M, int Y) {
-	return Gregorian{Y,M,D,h,m,s};
-}
-
-
 static MPT_NOINLINE void TestMisc1()
 {
 
@@ -1086,30 +1053,30 @@
 	VERIFY_EQUAL(    1413064016, TestDate1( 56, 46, 21, 11, 10, 2014 ));
 	VERIFY_EQUAL(    1413064100, TestDate1( 20, 48, 21, 11, 10, 2014 ));
 
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(             0).AsUTC()), TestDate2(  0,  0,  0,  1,  1, 1970 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(          3600).AsUTC()), TestDate2(  0,  0,  1,  1,  1, 1970 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(         86400).AsUTC()), TestDate2(  0,  0,  0,  2,  1, 1970 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(      31536000).AsUTC()), TestDate2(  0,  0,  0,  1,  1, 1971 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(     100000000).AsUTC()), TestDate2( 40, 46,  9,  3,  3, 1973 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(     951782400).AsUTC()), TestDate2(  0,  0,  0, 29,  2, 2000 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1000000000).AsUTC()), TestDate2( 40, 46,  1,  9,  9, 2001 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1044057600).AsUTC()), TestDate2(  0,  0,  0,  1,  2, 2003 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1044144000).AsUTC()), TestDate2(  0,  0,  0,  2,  2, 2003 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1046476800).AsUTC()), TestDate2(  0,  0,  0,  1,  3, 2003 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1064966400).AsUTC()), TestDate2(  0,  0,  0,  1, 10, 2003 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1077926399).AsUTC()), TestDate2( 59, 59, 23, 27,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1077926400).AsUTC()), TestDate2(  0,  0,  0, 28,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1077926410).AsUTC()), TestDate2( 10,  0,  0, 28,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078012799).AsUTC()), TestDate2( 59, 59, 23, 28,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078012800).AsUTC()), TestDate2(  0,  0,  0, 29,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078012820).AsUTC()), TestDate2( 20,  0,  0, 29,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078099199).AsUTC()), TestDate2( 59, 59, 23, 29,  2, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078099200).AsUTC()), TestDate2(  0,  0,  0,  1,  3, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078099230).AsUTC()), TestDate2( 30,  0,  0,  1,  3, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1078185599).AsUTC()), TestDate2( 59, 59, 23,  1,  3, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1096588800).AsUTC()), TestDate2(  0,  0,  0,  1, 10, 2004 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1413064016).AsUTC()), TestDate2( 56, 46, 21, 11, 10, 2014 ));
-	VERIFY_EQUAL(Gregorian::FromTM(mpt::Date::Unix(    1413064100).AsUTC()), TestDate2( 20, 48, 21, 11, 10, 2014 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(             0)), TestDate2(  0,  0,  0,  1,  1, 1970 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(          3600)), TestDate2(  0,  0,  1,  1,  1, 1970 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(         86400)), TestDate2(  0,  0,  0,  2,  1, 1970 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(      31536000)), TestDate2(  0,  0,  0,  1,  1, 1971 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(     100000000)), TestDate2( 40, 46,  9,  3,  3, 1973 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(     951782400)), TestDate2(  0,  0,  0, 29,  2, 2000 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1000000000)), TestDate2( 40, 46,  1,  9,  9, 2001 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1044057600)), TestDate2(  0,  0,  0,  1,  2, 2003 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1044144000)), TestDate2(  0,  0,  0,  2,  2, 2003 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1046476800)), TestDate2(  0,  0,  0,  1,  3, 2003 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1064966400)), TestDate2(  0,  0,  0,  1, 10, 2003 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1077926399)), TestDate2( 59, 59, 23, 27,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1077926400)), TestDate2(  0,  0,  0, 28,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1077926410)), TestDate2( 10,  0,  0, 28,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078012799)), TestDate2( 59, 59, 23, 28,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078012800)), TestDate2(  0,  0,  0, 29,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078012820)), TestDate2( 20,  0,  0, 29,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078099199)), TestDate2( 59, 59, 23, 29,  2, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078099200)), TestDate2(  0,  0,  0,  1,  3, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078099230)), TestDate2( 30,  0,  0,  1,  3, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1078185599)), TestDate2( 59, 59, 23,  1,  3, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1096588800)), TestDate2(  0,  0,  0,  1, 10, 2004 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1413064016)), TestDate2( 56, 46, 21, 11, 10, 2014 ));
+	VERIFY_EQUAL(mpt::Date::UnixAsUTC(mpt::Date::UnixFromSeconds(    1413064100)), TestDate2( 20, 48, 21, 11, 10, 2014 ));
 
 
 #ifdef MODPLUG_TRACKER
@@ -2069,12 +2036,12 @@
 	// Edit history
 	VERIFY_EQUAL_NONCONT(sndFile.GetFileHistory().size() > 15, true);
 	const FileHistory &fh = sndFile.GetFileHistory().front();
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_year, 111);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_mon, 5);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_mday, 14);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_hour, 21);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_min, 8);
-	VERIFY_EQUAL_NONCONT(fh.loadDate.tm_sec, 32);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.year, 2011);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.month, 6);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.day, 14);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.hours, 21);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.minutes, 8);
+	VERIFY_EQUAL_NONCONT(fh.loadDate.seconds, 32);
 	VERIFY_EQUAL_NONCONT((uint32)((double)fh.openTime / HISTORY_TIMER_PRECISION), 31);
 
 	// Macros
chrono-v8.patch (34,303 bytes)   
manx

manx

2022-05-28 05:20

administrator   ~0005193

Except for EditHistory, this has been partially merged as r17377, r17379, r17380, r17383.

Issue History

Date Modified Username Field Change
2022-02-23 14:38 manx New Issue
2022-02-23 14:38 manx Status new => assigned
2022-02-23 14:38 manx Assigned To => manx
2022-02-23 14:39 manx Relationship added related to 0001555
2022-02-23 14:39 manx Note Added: 0005113
2022-02-23 14:39 manx File Added: chrono-v8.patch
2022-05-28 05:20 manx Note Added: 0005193