diff options
| author | TSR Berry <20988865+TSRBerry@users.noreply.github.com> | 2023-04-08 01:22:00 +0200 |
|---|---|---|
| committer | Mary <thog@protonmail.com> | 2023-04-27 23:51:14 +0200 |
| commit | cee712105850ac3385cd0091a923438167433f9f (patch) | |
| tree | 4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx.HLE/HOS/Services/Time | |
| parent | cd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff) | |
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Services/Time')
39 files changed, 5136 insertions, 0 deletions
diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/EphemeralNetworkSystemClockContextWriter.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/EphemeralNetworkSystemClockContextWriter.cs new file mode 100644 index 00000000..14d3cb24 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/EphemeralNetworkSystemClockContextWriter.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class EphemeralNetworkSystemClockContextWriter : SystemClockContextUpdateCallback + { + protected override ResultCode Update() + { + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/EphemeralNetworkSystemClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/EphemeralNetworkSystemClockCore.cs new file mode 100644 index 00000000..003863e4 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/EphemeralNetworkSystemClockCore.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class EphemeralNetworkSystemClockCore : SystemClockCore + { + public EphemeralNetworkSystemClockCore(SteadyClockCore steadyClockCore) : base(steadyClockCore) { } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/LocalSystemClockContextWriter.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/LocalSystemClockContextWriter.cs new file mode 100644 index 00000000..fb7ebdc5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/LocalSystemClockContextWriter.cs @@ -0,0 +1,19 @@ +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class LocalSystemClockContextWriter : SystemClockContextUpdateCallback + { + private TimeSharedMemory _sharedMemory; + + public LocalSystemClockContextWriter(TimeSharedMemory sharedMemory) + { + _sharedMemory = sharedMemory; + } + + protected override ResultCode Update() + { + _sharedMemory.UpdateLocalSystemClockContext(_context); + + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/NetworkSystemClockContextWriter.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/NetworkSystemClockContextWriter.cs new file mode 100644 index 00000000..36468ec1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/NetworkSystemClockContextWriter.cs @@ -0,0 +1,19 @@ +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class NetworkSystemClockContextWriter : SystemClockContextUpdateCallback + { + private TimeSharedMemory _sharedMemory; + + public NetworkSystemClockContextWriter(TimeSharedMemory sharedMemory) + { + _sharedMemory = sharedMemory; + } + + protected override ResultCode Update() + { + _sharedMemory.UpdateNetworkSystemClockContext(_context); + + return ResultCode.Success; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardLocalSystemClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardLocalSystemClockCore.cs new file mode 100644 index 00000000..20c334e8 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardLocalSystemClockCore.cs @@ -0,0 +1,7 @@ +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class StandardLocalSystemClockCore : SystemClockCore + { + public StandardLocalSystemClockCore(StandardSteadyClockCore steadyClockCore) : base(steadyClockCore) {} + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardNetworkSystemClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardNetworkSystemClockCore.cs new file mode 100644 index 00000000..aec03485 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardNetworkSystemClockCore.cs @@ -0,0 +1,36 @@ +using Ryujinx.Cpu; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class StandardNetworkSystemClockCore : SystemClockCore + { + private TimeSpanType _standardNetworkClockSufficientAccuracy; + + public StandardNetworkSystemClockCore(StandardSteadyClockCore steadyClockCore) : base(steadyClockCore) + { + _standardNetworkClockSufficientAccuracy = new TimeSpanType(0); + } + + public bool IsStandardNetworkSystemClockAccuracySufficient(ITickSource tickSource) + { + SteadyClockCore steadyClockCore = GetSteadyClockCore(); + SteadyClockTimePoint currentTimePoint = steadyClockCore.GetCurrentTimePoint(tickSource); + + bool isStandardNetworkClockSufficientAccuracy = false; + + ResultCode result = GetClockContext(tickSource, out SystemClockContext context); + + if (result == ResultCode.Success && context.SteadyTimePoint.GetSpanBetween(currentTimePoint, out long outSpan) == ResultCode.Success) + { + isStandardNetworkClockSufficientAccuracy = outSpan * 1000000000 < _standardNetworkClockSufficientAccuracy.NanoSeconds; + } + + return isStandardNetworkClockSufficientAccuracy; + } + + public void SetStandardNetworkClockSufficientAccuracy(TimeSpanType standardNetworkClockSufficientAccuracy) + { + _standardNetworkClockSufficientAccuracy = standardNetworkClockSufficientAccuracy; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardSteadyClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardSteadyClockCore.cs new file mode 100644 index 00000000..8392c4b5 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardSteadyClockCore.cs @@ -0,0 +1,72 @@ +using Ryujinx.Cpu; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class StandardSteadyClockCore : SteadyClockCore + { + private TimeSpanType _setupValue; + private TimeSpanType _testOffset; + private TimeSpanType _internalOffset; + private TimeSpanType _cachedRawTimePoint; + + public StandardSteadyClockCore() + { + _setupValue = TimeSpanType.Zero; + _testOffset = TimeSpanType.Zero; + _internalOffset = TimeSpanType.Zero; + _cachedRawTimePoint = TimeSpanType.Zero; + } + + public override SteadyClockTimePoint GetTimePoint(ITickSource tickSource) + { + SteadyClockTimePoint result = new SteadyClockTimePoint + { + TimePoint = GetCurrentRawTimePoint(tickSource).ToSeconds(), + ClockSourceId = GetClockSourceId() + }; + + return result; + } + + public override TimeSpanType GetTestOffset() + { + return _testOffset; + } + + public override void SetTestOffset(TimeSpanType testOffset) + { + _testOffset = testOffset; + } + + public override TimeSpanType GetInternalOffset() + { + return _internalOffset; + } + + public override void SetInternalOffset(TimeSpanType internalOffset) + { + _internalOffset = internalOffset; + } + + public override TimeSpanType GetCurrentRawTimePoint(ITickSource tickSource) + { + TimeSpanType ticksTimeSpan = TimeSpanType.FromTicks(tickSource.Counter, tickSource.Frequency); + + TimeSpanType rawTimePoint = new TimeSpanType(_setupValue.NanoSeconds + ticksTimeSpan.NanoSeconds); + + if (rawTimePoint.NanoSeconds < _cachedRawTimePoint.NanoSeconds) + { + rawTimePoint.NanoSeconds = _cachedRawTimePoint.NanoSeconds; + } + + _cachedRawTimePoint = rawTimePoint; + + return rawTimePoint; + } + + public void SetSetupValue(TimeSpanType setupValue) + { + _setupValue = setupValue; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardUserSystemClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardUserSystemClockCore.cs new file mode 100644 index 00000000..fa485437 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/StandardUserSystemClockCore.cs @@ -0,0 +1,108 @@ +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Kernel.Threading; +using System; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class StandardUserSystemClockCore : SystemClockCore + { + private StandardLocalSystemClockCore _localSystemClockCore; + private StandardNetworkSystemClockCore _networkSystemClockCore; + private bool _autoCorrectionEnabled; + private SteadyClockTimePoint _autoCorrectionTime; + private KEvent _autoCorrectionEvent; + + public StandardUserSystemClockCore(StandardLocalSystemClockCore localSystemClockCore, StandardNetworkSystemClockCore networkSystemClockCore) : base(localSystemClockCore.GetSteadyClockCore()) + { + _localSystemClockCore = localSystemClockCore; + _networkSystemClockCore = networkSystemClockCore; + _autoCorrectionEnabled = false; + _autoCorrectionTime = SteadyClockTimePoint.GetRandom(); + _autoCorrectionEvent = null; + } + + protected override ResultCode Flush(SystemClockContext context) + { + // As UserSystemClock isn't a real system clock, this shouldn't happens. + throw new NotImplementedException(); + } + + public override ResultCode GetClockContext(ITickSource tickSource, out SystemClockContext context) + { + ResultCode result = ApplyAutomaticCorrection(tickSource, false); + + context = new SystemClockContext(); + + if (result == ResultCode.Success) + { + return _localSystemClockCore.GetClockContext(tickSource, out context); + } + + return result; + } + + public override ResultCode SetClockContext(SystemClockContext context) + { + return ResultCode.NotImplemented; + } + + private ResultCode ApplyAutomaticCorrection(ITickSource tickSource, bool autoCorrectionEnabled) + { + ResultCode result = ResultCode.Success; + + if (_autoCorrectionEnabled != autoCorrectionEnabled && _networkSystemClockCore.IsClockSetup(tickSource)) + { + result = _networkSystemClockCore.GetClockContext(tickSource, out SystemClockContext context); + + if (result == ResultCode.Success) + { + _localSystemClockCore.SetClockContext(context); + } + } + + return result; + } + + internal void CreateAutomaticCorrectionEvent(Horizon system) + { + _autoCorrectionEvent = new KEvent(system.KernelContext); + } + + public ResultCode SetAutomaticCorrectionEnabled(ITickSource tickSource, bool autoCorrectionEnabled) + { + ResultCode result = ApplyAutomaticCorrection(tickSource, autoCorrectionEnabled); + + if (result == ResultCode.Success) + { + _autoCorrectionEnabled = autoCorrectionEnabled; + } + + return result; + } + + public bool IsAutomaticCorrectionEnabled() + { + return _autoCorrectionEnabled; + } + + public KReadableEvent GetAutomaticCorrectionReadableEvent() + { + return _autoCorrectionEvent.ReadableEvent; + } + + public void SetAutomaticCorrectionUpdatedTime(SteadyClockTimePoint steadyClockTimePoint) + { + _autoCorrectionTime = steadyClockTimePoint; + } + + public SteadyClockTimePoint GetAutomaticCorrectionUpdatedTime() + { + return _autoCorrectionTime; + } + + public void SignalAutomaticCorrectionEvent() + { + _autoCorrectionEvent.WritableEvent.Signal(); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/SteadyClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/SteadyClockCore.cs new file mode 100644 index 00000000..18da4ed3 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/SteadyClockCore.cs @@ -0,0 +1,98 @@ +using Ryujinx.Common.Utilities; +using Ryujinx.Cpu; +using System; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + abstract class SteadyClockCore + { + private UInt128 _clockSourceId; + private bool _isRtcResetDetected; + private bool _isInitialized; + + public SteadyClockCore() + { + _clockSourceId = UInt128Utils.CreateRandom(); + _isRtcResetDetected = false; + _isInitialized = false; + } + + public UInt128 GetClockSourceId() + { + return _clockSourceId; + } + + public void SetClockSourceId(UInt128 clockSourceId) + { + _clockSourceId = clockSourceId; + } + + public void SetRtcReset() + { + _isRtcResetDetected = true; + } + + public virtual TimeSpanType GetTestOffset() + { + return new TimeSpanType(0); + } + + public virtual void SetTestOffset(TimeSpanType testOffset) {} + + public ResultCode GetRtcValue(out ulong rtcValue) + { + rtcValue = 0; + + return ResultCode.NotImplemented; + } + + public bool IsRtcResetDetected() + { + return _isRtcResetDetected; + } + + public ResultCode GetSetupResultValue() + { + return ResultCode.Success; + } + + public virtual TimeSpanType GetInternalOffset() + { + return new TimeSpanType(0); + } + + public virtual void SetInternalOffset(TimeSpanType internalOffset) {} + + public virtual SteadyClockTimePoint GetTimePoint(ITickSource tickSource) + { + throw new NotImplementedException(); + } + + public virtual TimeSpanType GetCurrentRawTimePoint(ITickSource tickSource) + { + SteadyClockTimePoint timePoint = GetTimePoint(tickSource); + + return TimeSpanType.FromSeconds(timePoint.TimePoint); + } + + public SteadyClockTimePoint GetCurrentTimePoint(ITickSource tickSource) + { + SteadyClockTimePoint result = GetTimePoint(tickSource); + + result.TimePoint += GetTestOffset().ToSeconds(); + result.TimePoint += GetInternalOffset().ToSeconds(); + + return result; + } + + public bool IsInitialized() + { + return _isInitialized; + } + + public void MarkInitialized() + { + _isInitialized = true; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/SystemClockContextUpdateCallback.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/SystemClockContextUpdateCallback.cs new file mode 100644 index 00000000..6229f5ed --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/SystemClockContextUpdateCallback.cs @@ -0,0 +1,71 @@ +using Ryujinx.HLE.HOS.Kernel.Threading; +using System.Collections.Generic; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + abstract class SystemClockContextUpdateCallback + { + private List<KWritableEvent> _operationEventList; + protected SystemClockContext _context; + private bool _hasContext; + + public SystemClockContextUpdateCallback() + { + _operationEventList = new List<KWritableEvent>(); + _context = new SystemClockContext(); + _hasContext = false; + } + + private bool NeedUpdate(SystemClockContext context) + { + if (_hasContext) + { + return _context.Offset != context.Offset || _context.SteadyTimePoint.ClockSourceId != context.SteadyTimePoint.ClockSourceId; + } + + return true; + } + + public void RegisterOperationEvent(KWritableEvent writableEvent) + { + Monitor.Enter(_operationEventList); + _operationEventList.Add(writableEvent); + Monitor.Exit(_operationEventList); + } + + private void BroadcastOperationEvent() + { + Monitor.Enter(_operationEventList); + + foreach (KWritableEvent e in _operationEventList) + { + e.Signal(); + } + + Monitor.Exit(_operationEventList); + } + + protected abstract ResultCode Update(); + + public ResultCode Update(SystemClockContext context) + { + ResultCode result = ResultCode.Success; + + if (NeedUpdate(context)) + { + _context = context; + _hasContext = true; + + result = Update(); + + if (result == ResultCode.Success) + { + BroadcastOperationEvent(); + } + } + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/SystemClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/SystemClockCore.cs new file mode 100644 index 00000000..f4bbaa60 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/SystemClockCore.cs @@ -0,0 +1,144 @@ +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Kernel.Threading; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + abstract class SystemClockCore + { + private SteadyClockCore _steadyClockCore; + private SystemClockContext _context; + private bool _isInitialized; + private SystemClockContextUpdateCallback _systemClockContextUpdateCallback; + + public SystemClockCore(SteadyClockCore steadyClockCore) + { + _steadyClockCore = steadyClockCore; + _context = new SystemClockContext(); + _isInitialized = false; + + _context.SteadyTimePoint.ClockSourceId = steadyClockCore.GetClockSourceId(); + _systemClockContextUpdateCallback = null; + } + + public virtual SteadyClockCore GetSteadyClockCore() + { + return _steadyClockCore; + } + + public ResultCode GetCurrentTime(ITickSource tickSource, out long posixTime) + { + posixTime = 0; + + SteadyClockTimePoint currentTimePoint = _steadyClockCore.GetCurrentTimePoint(tickSource); + + ResultCode result = GetClockContext(tickSource, out SystemClockContext clockContext); + + if (result == ResultCode.Success) + { + result = ResultCode.TimeMismatch; + + if (currentTimePoint.ClockSourceId == clockContext.SteadyTimePoint.ClockSourceId) + { + posixTime = clockContext.Offset + currentTimePoint.TimePoint; + + result = 0; + } + } + + return result; + } + + public ResultCode SetCurrentTime(ITickSource tickSource, long posixTime) + { + SteadyClockTimePoint currentTimePoint = _steadyClockCore.GetCurrentTimePoint(tickSource); + + SystemClockContext clockContext = new SystemClockContext() + { + Offset = posixTime - currentTimePoint.TimePoint, + SteadyTimePoint = currentTimePoint + }; + + ResultCode result = SetClockContext(clockContext); + + if (result == ResultCode.Success) + { + result = Flush(clockContext); + } + + return result; + } + + public virtual ResultCode GetClockContext(ITickSource tickSource, out SystemClockContext context) + { + context = _context; + + return ResultCode.Success; + } + + public virtual ResultCode SetClockContext(SystemClockContext context) + { + _context = context; + + return ResultCode.Success; + } + + protected virtual ResultCode Flush(SystemClockContext context) + { + if (_systemClockContextUpdateCallback == null) + { + return ResultCode.Success; + } + + return _systemClockContextUpdateCallback.Update(context); + } + + public void SetUpdateCallbackInstance(SystemClockContextUpdateCallback systemClockContextUpdateCallback) + { + _systemClockContextUpdateCallback = systemClockContextUpdateCallback; + } + + public void RegisterOperationEvent(KWritableEvent writableEvent) + { + if (_systemClockContextUpdateCallback != null) + { + _systemClockContextUpdateCallback.RegisterOperationEvent(writableEvent); + } + } + + public ResultCode SetSystemClockContext(SystemClockContext context) + { + ResultCode result = SetClockContext(context); + + if (result == ResultCode.Success) + { + result = Flush(context); + } + + return result; + } + + public bool IsInitialized() + { + return _isInitialized; + } + + public void MarkInitialized() + { + _isInitialized = true; + } + + public bool IsClockSetup(ITickSource tickSource) + { + ResultCode result = GetClockContext(tickSource, out SystemClockContext context); + + if (result == ResultCode.Success) + { + SteadyClockTimePoint steadyClockTimePoint = _steadyClockCore.GetCurrentTimePoint(tickSource); + + return steadyClockTimePoint.ClockSourceId == context.SteadyTimePoint.ClockSourceId; + } + + return false; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/TickBasedSteadyClockCore.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/TickBasedSteadyClockCore.cs new file mode 100644 index 00000000..fe74da7e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/TickBasedSteadyClockCore.cs @@ -0,0 +1,24 @@ +using Ryujinx.Cpu; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + class TickBasedSteadyClockCore : SteadyClockCore + { + public TickBasedSteadyClockCore() {} + + public override SteadyClockTimePoint GetTimePoint(ITickSource tickSource) + { + SteadyClockTimePoint result = new SteadyClockTimePoint + { + TimePoint = 0, + ClockSourceId = GetClockSourceId() + }; + + TimeSpanType ticksTimeSpan = TimeSpanType.FromTicks(tickSource.Counter, tickSource.Frequency); + + result.TimePoint = ticksTimeSpan.ToSeconds(); + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/ClockSnapshot.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/ClockSnapshot.cs new file mode 100644 index 00000000..07c1b405 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/ClockSnapshot.cs @@ -0,0 +1,50 @@ +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + [StructLayout(LayoutKind.Sequential, Size = 0xD0)] + struct ClockSnapshot + { + public SystemClockContext UserContext; + public SystemClockContext NetworkContext; + public long UserTime; + public long NetworkTime; + public CalendarTime UserCalendarTime; + public CalendarTime NetworkCalendarTime; + public CalendarAdditionalInfo UserCalendarAdditionalTime; + public CalendarAdditionalInfo NetworkCalendarAdditionalTime; + public SteadyClockTimePoint SteadyClockTimePoint; + + private LocationNameStorageHolder _locationName; + + public Span<byte> LocationName => MemoryMarshal.Cast<LocationNameStorageHolder, byte>(MemoryMarshal.CreateSpan(ref _locationName, LocationNameStorageHolder.Size)); + + [MarshalAs(UnmanagedType.I1)] + public bool IsAutomaticCorrectionEnabled; + public byte Type; + public ushort Unknown; + + + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = Size)] + private struct LocationNameStorageHolder + { + public const int Size = 0x24; + } + + public static ResultCode GetCurrentTime(out long currentTime, SteadyClockTimePoint steadyClockTimePoint, SystemClockContext context) + { + currentTime = 0; + + if (steadyClockTimePoint.ClockSourceId == context.SteadyTimePoint.ClockSourceId) + { + currentTime = steadyClockTimePoint.TimePoint + context.Offset; + + return ResultCode.Success; + } + + return ResultCode.TimeMismatch; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SteadyClockTimePoint.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SteadyClockTimePoint.cs new file mode 100644 index 00000000..729e11b6 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SteadyClockTimePoint.cs @@ -0,0 +1,43 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct SteadyClockTimePoint + { + public long TimePoint; + public UInt128 ClockSourceId; + + public ResultCode GetSpanBetween(SteadyClockTimePoint other, out long outSpan) + { + outSpan = 0; + + if (ClockSourceId == other.ClockSourceId) + { + try + { + outSpan = checked(other.TimePoint - TimePoint); + + return ResultCode.Success; + } + catch (OverflowException) + { + return ResultCode.Overflow; + } + } + + return ResultCode.Overflow; + } + + public static SteadyClockTimePoint GetRandom() + { + return new SteadyClockTimePoint + { + TimePoint = 0, + ClockSourceId = UInt128Utils.CreateRandom() + }; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SystemClockContext.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SystemClockContext.cs new file mode 100644 index 00000000..6b589c28 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/SystemClockContext.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct SystemClockContext + { + public long Offset; + public SteadyClockTimePoint SteadyTimePoint; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/TimeSpanType.cs b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/TimeSpanType.cs new file mode 100644 index 00000000..0070193f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Clock/Types/TimeSpanType.cs @@ -0,0 +1,50 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.Clock +{ + [StructLayout(LayoutKind.Sequential)] + struct TimeSpanType + { + private const long NanoSecondsPerSecond = 1000000000; + + public static readonly TimeSpanType Zero = new TimeSpanType(0); + + public long NanoSeconds; + + public TimeSpanType(long nanoSeconds) + { + NanoSeconds = nanoSeconds; + } + + public long ToSeconds() + { + return NanoSeconds / NanoSecondsPerSecond; + } + + public TimeSpanType AddSeconds(long seconds) + { + return new TimeSpanType(NanoSeconds + (seconds * NanoSecondsPerSecond)); + } + + public bool IsDaylightSavingTime() + { + return DateTime.UnixEpoch.AddSeconds(ToSeconds()).ToLocalTime().IsDaylightSavingTime(); + } + + public static TimeSpanType FromSeconds(long seconds) + { + return new TimeSpanType(seconds * NanoSecondsPerSecond); + } + + public static TimeSpanType FromTimeSpan(TimeSpan timeSpan) + { + return new TimeSpanType((long)(timeSpan.TotalMilliseconds * 1000000)); + } + + public static TimeSpanType FromTicks(ulong ticks, ulong frequency) + { + return FromSeconds((long)ticks / (long)frequency); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/IAlarmService.cs b/src/Ryujinx.HLE/HOS/Services/Time/IAlarmService.cs new file mode 100644 index 00000000..092fa8ce --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/IAlarmService.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Time +{ + [Service("time:al")] // 9.0.0+ + class IAlarmService : IpcService + { + public IAlarmService(ServiceCtx context) { } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/IPowerStateRequestHandler.cs b/src/Ryujinx.HLE/HOS/Services/Time/IPowerStateRequestHandler.cs new file mode 100644 index 00000000..8ec55c15 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/IPowerStateRequestHandler.cs @@ -0,0 +1,8 @@ +namespace Ryujinx.HLE.HOS.Services.Time +{ + [Service("time:p")] // 9.0.0+ + class IPowerStateRequestHandler : IpcService + { + public IPowerStateRequestHandler(ServiceCtx context) { } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/IStaticServiceForGlue.cs b/src/Ryujinx.HLE/HOS/Services/Time/IStaticServiceForGlue.cs new file mode 100644 index 00000000..31548b80 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/IStaticServiceForGlue.cs @@ -0,0 +1,184 @@ +using Ryujinx.Common; +using Ryujinx.HLE.HOS.Services.Pcv.Bpc; +using Ryujinx.HLE.HOS.Services.Settings; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using Ryujinx.HLE.HOS.Services.Time.StaticService; +using System; + +namespace Ryujinx.HLE.HOS.Services.Time +{ + [Service("time:a", TimePermissions.Admin)] + [Service("time:r", TimePermissions.Repair)] + [Service("time:u", TimePermissions.User)] + class IStaticServiceForGlue : IpcService + { + private IStaticServiceForPsc _inner; + private TimePermissions _permissions; + + public IStaticServiceForGlue(ServiceCtx context, TimePermissions permissions) : base(context.Device.System.TimeServer) + { + _permissions = permissions; + _inner = new IStaticServiceForPsc(context, permissions); + _inner.TrySetServer(Server); + _inner.SetParent(this); + } + + [CommandCmif(0)] + // GetStandardUserSystemClock() -> object<nn::timesrv::detail::service::ISystemClock> + public ResultCode GetStandardUserSystemClock(ServiceCtx context) + { + return _inner.GetStandardUserSystemClock(context); + } + + [CommandCmif(1)] + // GetStandardNetworkSystemClock() -> object<nn::timesrv::detail::service::ISystemClock> + public ResultCode GetStandardNetworkSystemClock(ServiceCtx context) + { + return _inner.GetStandardNetworkSystemClock(context); + } + + [CommandCmif(2)] + // GetStandardSteadyClock() -> object<nn::timesrv::detail::service::ISteadyClock> + public ResultCode GetStandardSteadyClock(ServiceCtx context) + { + return _inner.GetStandardSteadyClock(context); + } + + [CommandCmif(3)] + // GetTimeZoneService() -> object<nn::timesrv::detail::service::ITimeZoneService> + public ResultCode GetTimeZoneService(ServiceCtx context) + { + MakeObject(context, new ITimeZoneServiceForGlue(TimeManager.Instance.TimeZone, (_permissions & TimePermissions.TimeZoneWritableMask) != 0)); + + return ResultCode.Success; + } + + [CommandCmif(4)] + // GetStandardLocalSystemClock() -> object<nn::timesrv::detail::service::ISystemClock> + public ResultCode GetStandardLocalSystemClock(ServiceCtx context) + { + return _inner.GetStandardLocalSystemClock(context); + } + + [CommandCmif(5)] // 4.0.0+ + // GetEphemeralNetworkSystemClock() -> object<nn::timesrv::detail::service::ISystemClock> + public ResultCode GetEphemeralNetworkSystemClock(ServiceCtx context) + { + return _inner.GetEphemeralNetworkSystemClock(context); + } + + [CommandCmif(20)] // 6.0.0+ + // GetSharedMemoryNativeHandle() -> handle<copy> + public ResultCode GetSharedMemoryNativeHandle(ServiceCtx context) + { + return _inner.GetSharedMemoryNativeHandle(context); + } + + [CommandCmif(50)] // 4.0.0+ + // SetStandardSteadyClockInternalOffset(nn::TimeSpanType internal_offset) + public ResultCode SetStandardSteadyClockInternalOffset(ServiceCtx context) + { + if ((_permissions & TimePermissions.SteadyClockWritableMask) == 0) + { + return ResultCode.PermissionDenied; + } + + TimeSpanType internalOffset = context.RequestData.ReadStruct<TimeSpanType>(); + + // TODO: set:sys SetExternalSteadyClockInternalOffset(internalOffset.ToSeconds()) + + return ResultCode.Success; + } + + [CommandCmif(51)] // 9.0.0+ + // GetStandardSteadyClockRtcValue() -> u64 + public ResultCode GetStandardSteadyClockRtcValue(ServiceCtx context) + { + ResultCode result = (ResultCode)IRtcManager.GetExternalRtcValue(out ulong rtcValue); + + if (result == ResultCode.Success) + { + context.ResponseData.Write(rtcValue); + } + + return result; + } + + [CommandCmif(100)] + // IsStandardUserSystemClockAutomaticCorrectionEnabled() -> bool + public ResultCode IsStandardUserSystemClockAutomaticCorrectionEnabled(ServiceCtx context) + { + return _inner.IsStandardUserSystemClockAutomaticCorrectionEnabled(context); + } + + [CommandCmif(101)] + // SetStandardUserSystemClockAutomaticCorrectionEnabled(b8) + public ResultCode SetStandardUserSystemClockAutomaticCorrectionEnabled(ServiceCtx context) + { + return _inner.SetStandardUserSystemClockAutomaticCorrectionEnabled(context); + } + + [CommandCmif(102)] // 5.0.0+ + // GetStandardUserSystemClockInitialYear() -> u32 + public ResultCode GetStandardUserSystemClockInitialYear(ServiceCtx context) + { + if (!NxSettings.Settings.TryGetValue("time!standard_user_clock_initial_year", out object standardUserSystemClockInitialYear)) + { + throw new InvalidOperationException("standard_user_clock_initial_year isn't defined in system settings!"); + } + + context.ResponseData.Write((int)standardUserSystemClockInitialYear); + + return ResultCode.Success; + } + + [CommandCmif(200)] // 3.0.0+ + // IsStandardNetworkSystemClockAccuracySufficient() -> bool + public ResultCode IsStandardNetworkSystemClockAccuracySufficient(ServiceCtx context) + { + return _inner.IsStandardNetworkSystemClockAccuracySufficient(context); + } + + [CommandCmif(201)] // 6.0.0+ + // GetStandardUserSystemClockAutomaticCorrectionUpdatedTime() -> nn::time::SteadyClockTimePoint + public ResultCode GetStandardUserSystemClockAutomaticCorrectionUpdatedTime(ServiceCtx context) + { + return _inner.GetStandardUserSystemClockAutomaticCorrectionUpdatedTime(context); + } + + [CommandCmif(300)] // 4.0.0+ + // CalculateMonotonicSystemClockBaseTimePoint(nn::time::SystemClockContext) -> s64 + public ResultCode CalculateMonotonicSystemClockBaseTimePoint(ServiceCtx context) + { + return _inner.CalculateMonotonicSystemClockBaseTimePoint(context); + } + + [CommandCmif(400)] // 4.0.0+ + // GetClockSnapshot(u8) -> buffer<nn::time::sf::ClockSnapshot, 0x1a> + public ResultCode GetClockSnapshot(ServiceCtx context) + { + return _inner.GetClockSnapshot(context); + } + + [CommandCmif(401)] // 4.0.0+ + // GetClockSnapshotFromSystemClockContext(u8, nn::time::SystemClockContext, nn::time::SystemClockContext) -> buffer<nn::time::sf::ClockSnapshot, 0x1a> + public ResultCode GetClockSnapshotFromSystemClockContext(ServiceCtx context) + { + return _inner.GetClockSnapshotFromSystemClockContext(context); + } + + [CommandCmif(500)] // 4.0.0+ + // CalculateStandardUserSystemClockDifferenceByUser(buffer<nn::time::sf::ClockSnapshot, 0x19>, buffer<nn::time::sf::ClockSnapshot, 0x19>) -> nn::TimeSpanType + public ResultCode CalculateStandardUserSystemClockDifferenceByUser(ServiceCtx context) + { + return _inner.CalculateStandardUserSystemClockDifferenceByUser(context); + } + + [CommandCmif(501)] // 4.0.0+ + // CalculateSpanBetween(buffer<nn::time::sf::ClockSnapshot, 0x19>, buffer<nn::time::sf::ClockSnapshot, 0x19>) -> nn::TimeSpanType + public ResultCode CalculateSpanBetween(ServiceCtx context) + { + return _inner.CalculateSpanBetween(context); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/IStaticServiceForPsc.cs b/src/Ryujinx.HLE/HOS/Services/Time/IStaticServiceForPsc.cs new file mode 100644 index 00000000..145d4e3b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/IStaticServiceForPsc.cs @@ -0,0 +1,433 @@ +using Ryujinx.Common; +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using Ryujinx.HLE.HOS.Services.Time.StaticService; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using Ryujinx.Horizon.Common; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.HLE.HOS.Services.Time +{ + [Service("time:s", TimePermissions.System)] + [Service("time:su", TimePermissions.SystemUpdate)] + class IStaticServiceForPsc : IpcService + { + private TimeManager _timeManager; + private TimePermissions _permissions; + + private int _timeSharedMemoryNativeHandle = 0; + + public IStaticServiceForPsc(ServiceCtx context, TimePermissions permissions) : this(TimeManager.Instance, permissions) {} + + public IStaticServiceForPsc(TimeManager manager, TimePermissions permissions) + { + _permissions = permissions; + _timeManager = manager; + } + + [CommandCmif(0)] + // GetStandardUserSystemClock() -> object<nn::timesrv::detail::service::ISystemClock> + public ResultCode GetStandardUserSystemClock(ServiceCtx context) + { + MakeObject(context, new ISystemClock(_timeManager.StandardUserSystemClock, + (_permissions & TimePermissions.UserSystemClockWritableMask) != 0, + (_permissions & TimePermissions.BypassUninitialized) != 0)); + + return ResultCode.Success; + } + + [CommandCmif(1)] + // GetStandardNetworkSystemClock() -> object<nn::timesrv::detail::service::ISystemClock> + public ResultCode GetStandardNetworkSystemClock(ServiceCtx context) + { + MakeObject(context, new ISystemClock(_timeManager.StandardNetworkSystemClock, + (_permissions & TimePermissions.NetworkSystemClockWritableMask) != 0, + (_permissions & TimePermissions.BypassUninitialized) != 0)); + + return ResultCode.Success; + } + + [CommandCmif(2)] + // GetStandardSteadyClock() -> object<nn::timesrv::detail::service::ISteadyClock> + public ResultCode GetStandardSteadyClock(ServiceCtx context) + { + MakeObject(context, new ISteadyClock(_timeManager.StandardSteadyClock, + (_permissions & TimePermissions.SteadyClockWritableMask) != 0, + (_permissions & TimePermissions.BypassUninitialized) != 0)); + + return ResultCode.Success; + } + + [CommandCmif(3)] + // GetTimeZoneService() -> object<nn::timesrv::detail::service::ITimeZoneService> + public ResultCode GetTimeZoneService(ServiceCtx context) + { + MakeObject(context, new ITimeZoneServiceForPsc(_timeManager.TimeZone.Manager, + (_permissions & TimePermissions.TimeZoneWritableMask) != 0)); + + return ResultCode.Success; + } + + [CommandCmif(4)] + // GetStandardLocalSystemClock() -> object<nn::timesrv::detail::service::ISystemClock> + public ResultCode GetStandardLocalSystemClock(ServiceCtx context) + { + MakeObject(context, new ISystemClock(_timeManager.StandardLocalSystemClock, + (_permissions & TimePermissions.LocalSystemClockWritableMask) != 0, + (_permissions & TimePermissions.BypassUninitialized) != 0)); + + return ResultCode.Success; + } + + [CommandCmif(5)] // 4.0.0+ + // GetEphemeralNetworkSystemClock() -> object<nn::timesrv::detail::service::ISystemClock> + public ResultCode GetEphemeralNetworkSystemClock(ServiceCtx context) + { + MakeObject(context, new ISystemClock(_timeManager.StandardNetworkSystemClock, + (_permissions & TimePermissions.NetworkSystemClockWritableMask) != 0, + (_permissions & TimePermissions.BypassUninitialized) != 0)); + + return ResultCode.Success; + } + + [CommandCmif(20)] // 6.0.0+ + // GetSharedMemoryNativeHandle() -> handle<copy> + public ResultCode GetSharedMemoryNativeHandle(ServiceCtx context) + { + if (_timeSharedMemoryNativeHandle == 0) + { + if (context.Process.HandleTable.GenerateHandle(_timeManager.SharedMemory.GetSharedMemory(), out _timeSharedMemoryNativeHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_timeSharedMemoryNativeHandle); + + return ResultCode.Success; + } + + [CommandCmif(50)] // 4.0.0+ + // SetStandardSteadyClockInternalOffset(nn::TimeSpanType internal_offset) + public ResultCode SetStandardSteadyClockInternalOffset(ServiceCtx context) + { + // This is only implemented in glue's StaticService. + return ResultCode.NotImplemented; + } + + [CommandCmif(51)] // 9.0.0+ + // GetStandardSteadyClockRtcValue() -> u64 + public ResultCode GetStandardSteadyClockRtcValue(ServiceCtx context) + { + // This is only implemented in glue's StaticService. + return ResultCode.NotImplemented; + } + + [CommandCmif(100)] + // IsStandardUserSystemClockAutomaticCorrectionEnabled() -> bool + public ResultCode IsStandardUserSystemClockAutomaticCorrectionEnabled(ServiceCtx context) + { + StandardUserSystemClockCore userClock = _timeManager.StandardUserSystemClock; + + if (!userClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + context.ResponseData.Write(userClock.IsAutomaticCorrectionEnabled()); + + return ResultCode.Success; + } + + [CommandCmif(101)] + // SetStandardUserSystemClockAutomaticCorrectionEnabled(b8) + public ResultCode SetStandardUserSystemClockAutomaticCorrectionEnabled(ServiceCtx context) + { + SteadyClockCore steadyClock = _timeManager.StandardSteadyClock; + StandardUserSystemClockCore userClock = _timeManager.StandardUserSystemClock; + + if (!userClock.IsInitialized() || !steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + if ((_permissions & TimePermissions.UserSystemClockWritableMask) == 0) + { + return ResultCode.PermissionDenied; + } + + bool autoCorrectionEnabled = context.RequestData.ReadBoolean(); + + ITickSource tickSource = context.Device.System.TickSource; + + ResultCode result = userClock.SetAutomaticCorrectionEnabled(tickSource, autoCorrectionEnabled); + + if (result == ResultCode.Success) + { + _timeManager.SharedMemory.SetAutomaticCorrectionEnabled(autoCorrectionEnabled); + + SteadyClockTimePoint currentTimePoint = userClock.GetSteadyClockCore().GetCurrentTimePoint(tickSource); + + userClock.SetAutomaticCorrectionUpdatedTime(currentTimePoint); + userClock.SignalAutomaticCorrectionEvent(); + } + + return result; + } + + [CommandCmif(102)] // 5.0.0+ + // GetStandardUserSystemClockInitialYear() -> u32 + public ResultCode GetStandardUserSystemClockInitialYear(ServiceCtx context) + { + // This is only implemented in glue's StaticService. + return ResultCode.NotImplemented; + } + + [CommandCmif(200)] // 3.0.0+ + // IsStandardNetworkSystemClockAccuracySufficient() -> bool + public ResultCode IsStandardNetworkSystemClockAccuracySufficient(ServiceCtx context) + { + ITickSource tickSource = context.Device.System.TickSource; + + context.ResponseData.Write(_timeManager.StandardNetworkSystemClock.IsStandardNetworkSystemClockAccuracySufficient(tickSource)); + + return ResultCode.Success; + } + + [CommandCmif(201)] // 6.0.0+ + // GetStandardUserSystemClockAutomaticCorrectionUpdatedTime() -> nn::time::SteadyClockTimePoint + public ResultCode GetStandardUserSystemClockAutomaticCorrectionUpdatedTime(ServiceCtx context) + { + StandardUserSystemClockCore userClock = _timeManager.StandardUserSystemClock; + + if (!userClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + context.ResponseData.WriteStruct(userClock.GetAutomaticCorrectionUpdatedTime()); + + return ResultCode.Success; + } + + [CommandCmif(300)] // 4.0.0+ + // CalculateMonotonicSystemClockBaseTimePoint(nn::time::SystemClockContext) -> s64 + public ResultCode CalculateMonotonicSystemClockBaseTimePoint(ServiceCtx context) + { + SteadyClockCore steadyClock = _timeManager.StandardSteadyClock; + + if (!steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + ITickSource tickSource = context.Device.System.TickSource; + + SystemClockContext otherContext = context.RequestData.ReadStruct<SystemClockContext>(); + SteadyClockTimePoint currentTimePoint = steadyClock.GetCurrentTimePoint(tickSource); + + ResultCode result = ResultCode.TimeMismatch; + + if (currentTimePoint.ClockSourceId == otherContext.SteadyTimePoint.ClockSourceId) + { + TimeSpanType ticksTimeSpan = TimeSpanType.FromTicks(tickSource.Counter, tickSource.Frequency); + long baseTimePoint = otherContext.Offset + currentTimePoint.TimePoint - ticksTimeSpan.ToSeconds(); + + context.ResponseData.Write(baseTimePoint); + + result = ResultCode.Success; + } + + return result; + } + + [CommandCmif(400)] // 4.0.0+ + // GetClockSnapshot(u8) -> buffer<nn::time::sf::ClockSnapshot, 0x1a> + public ResultCode GetClockSnapshot(ServiceCtx context) + { + byte type = context.RequestData.ReadByte(); + + context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize((uint)Marshal.SizeOf<ClockSnapshot>()); + + ITickSource tickSource = context.Device.System.TickSource; + + ResultCode result = _timeManager.StandardUserSystemClock.GetClockContext(tickSource, out SystemClockContext userContext); + + if (result == ResultCode.Success) + { + result = _timeManager.StandardNetworkSystemClock.GetClockContext(tickSource, out SystemClockContext networkContext); + + if (result == ResultCode.Success) + { + result = GetClockSnapshotFromSystemClockContextInternal(tickSource, userContext, networkContext, type, out ClockSnapshot clockSnapshot); + + if (result == ResultCode.Success) + { + WriteClockSnapshotFromBuffer(context, context.Request.RecvListBuff[0], clockSnapshot); + } + } + } + + return result; + } + + [CommandCmif(401)] // 4.0.0+ + // GetClockSnapshotFromSystemClockContext(u8, nn::time::SystemClockContext, nn::time::SystemClockContext) -> buffer<nn::time::sf::ClockSnapshot, 0x1a> + public ResultCode GetClockSnapshotFromSystemClockContext(ServiceCtx context) + { + byte type = context.RequestData.ReadByte(); + + context.Response.PtrBuff[0] = context.Response.PtrBuff[0].WithSize((uint)Unsafe.SizeOf<ClockSnapshot>()); + + context.RequestData.BaseStream.Position += 7; + + SystemClockContext userContext = context.RequestData.ReadStruct<SystemClockContext>(); + SystemClockContext networkContext = context.RequestData.ReadStruct<SystemClockContext>(); + + ITickSource tickSource = context.Device.System.TickSource; + + ResultCode result = GetClockSnapshotFromSystemClockContextInternal(tickSource, userContext, networkContext, type, out ClockSnapshot clockSnapshot); + + if (result == ResultCode.Success) + { + WriteClockSnapshotFromBuffer(context, context.Request.RecvListBuff[0], clockSnapshot); + } + + return result; + } + + [CommandCmif(500)] // 4.0.0+ + // CalculateStandardUserSystemClockDifferenceByUser(buffer<nn::time::sf::ClockSnapshot, 0x19>, buffer<nn::time::sf::ClockSnapshot, 0x19>) -> nn::TimeSpanType + public ResultCode CalculateStandardUserSystemClockDifferenceByUser(ServiceCtx context) + { + ClockSnapshot clockSnapshotA = ReadClockSnapshotFromBuffer(context, context.Request.PtrBuff[0]); + ClockSnapshot clockSnapshotB = ReadClockSnapshotFromBuffer(context, context.Request.PtrBuff[1]); + TimeSpanType difference = TimeSpanType.FromSeconds(clockSnapshotB.UserContext.Offset - clockSnapshotA.UserContext.Offset); + + if (clockSnapshotB.UserContext.SteadyTimePoint.ClockSourceId != clockSnapshotA.UserContext.SteadyTimePoint.ClockSourceId || (clockSnapshotB.IsAutomaticCorrectionEnabled && clockSnapshotA.IsAutomaticCorrectionEnabled)) + { + difference = new TimeSpanType(0); + } + + context.ResponseData.Write(difference.NanoSeconds); + + return ResultCode.Success; + } + + [CommandCmif(501)] // 4.0.0+ + // CalculateSpanBetween(buffer<nn::time::sf::ClockSnapshot, 0x19>, buffer<nn::time::sf::ClockSnapshot, 0x19>) -> nn::TimeSpanType + public ResultCode CalculateSpanBetween(ServiceCtx context) + { + ClockSnapshot clockSnapshotA = ReadClockSnapshotFromBuffer(context, context.Request.PtrBuff[0]); + ClockSnapshot clockSnapshotB = ReadClockSnapshotFromBuffer(context, context.Request.PtrBuff[1]); + + TimeSpanType result; + + ResultCode resultCode = clockSnapshotA.SteadyClockTimePoint.GetSpanBetween(clockSnapshotB.SteadyClockTimePoint, out long timeSpan); + + if (resultCode != ResultCode.Success) + { + resultCode = ResultCode.TimeNotFound; + + if (clockSnapshotA.NetworkTime != 0 && clockSnapshotB.NetworkTime != 0) + { + result = TimeSpanType.FromSeconds(clockSnapshotB.NetworkTime - clockSnapshotA.NetworkTime); + resultCode = ResultCode.Success; + } + else + { + return resultCode; + } + } + else + { + result = TimeSpanType.FromSeconds(timeSpan); + } + + context.ResponseData.Write(result.NanoSeconds); + + return resultCode; + } + + private ResultCode GetClockSnapshotFromSystemClockContextInternal(ITickSource tickSource, SystemClockContext userContext, SystemClockContext networkContext, byte type, out ClockSnapshot clockSnapshot) + { + clockSnapshot = new ClockSnapshot(); + + SteadyClockCore steadyClockCore = _timeManager.StandardSteadyClock; + SteadyClockTimePoint currentTimePoint = steadyClockCore.GetCurrentTimePoint(tickSource); + + clockSnapshot.IsAutomaticCorrectionEnabled = _timeManager.StandardUserSystemClock.IsAutomaticCorrectionEnabled(); + clockSnapshot.UserContext = userContext; + clockSnapshot.NetworkContext = networkContext; + clockSnapshot.SteadyClockTimePoint = currentTimePoint; + + ResultCode result = _timeManager.TimeZone.Manager.GetDeviceLocationName(out string deviceLocationName); + + if (result != ResultCode.Success) + { + return result; + } + + ReadOnlySpan<byte> tzName = Encoding.ASCII.GetBytes(deviceLocationName); + + tzName.CopyTo(clockSnapshot.LocationName); + + result = ClockSnapshot.GetCurrentTime(out clockSnapshot.UserTime, currentTimePoint, clockSnapshot.UserContext); + + if (result == ResultCode.Success) + { + result = _timeManager.TimeZone.Manager.ToCalendarTimeWithMyRules(clockSnapshot.UserTime, out CalendarInfo userCalendarInfo); + + if (result == ResultCode.Success) + { + clockSnapshot.UserCalendarTime = userCalendarInfo.Time; + clockSnapshot.UserCalendarAdditionalTime = userCalendarInfo.AdditionalInfo; + + if (ClockSnapshot.GetCurrentTime(out clockSnapshot.NetworkTime, currentTimePoint, clockSnapshot.NetworkContext) != ResultCode.Success) + { + clockSnapshot.NetworkTime = 0; + } + + result = _timeManager.TimeZone.Manager.ToCalendarTimeWithMyRules(clockSnapshot.NetworkTime, out CalendarInfo networkCalendarInfo); + + if (result == ResultCode.Success) + { + clockSnapshot.NetworkCalendarTime = networkCalendarInfo.Time; + clockSnapshot.NetworkCalendarAdditionalTime = networkCalendarInfo.AdditionalInfo; + clockSnapshot.Type = type; + + // Probably a version field? + clockSnapshot.Unknown = 0; + } + } + } + + return result; + } + + private ClockSnapshot ReadClockSnapshotFromBuffer(ServiceCtx context, IpcPtrBuffDesc ipcDesc) + { + Debug.Assert(ipcDesc.Size == (ulong)Unsafe.SizeOf<ClockSnapshot>()); + + byte[] temp = new byte[ipcDesc.Size]; + + context.Memory.Read(ipcDesc.Position, temp); + + using (BinaryReader bufferReader = new BinaryReader(new MemoryStream(temp))) + { + return bufferReader.ReadStruct<ClockSnapshot>(); + } + } + + private void WriteClockSnapshotFromBuffer(ServiceCtx context, IpcRecvListBuffDesc ipcDesc, ClockSnapshot clockSnapshot) + { + MemoryHelper.Write(context.Memory, ipcDesc.Position, clockSnapshot); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/ITimeServiceManager.cs b/src/Ryujinx.HLE/HOS/Services/Time/ITimeServiceManager.cs new file mode 100644 index 00000000..6c9c15f1 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/ITimeServiceManager.cs @@ -0,0 +1,231 @@ +using Ryujinx.Common; +using Ryujinx.Cpu; +using Ryujinx.HLE.Exceptions; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using Ryujinx.HLE.Utilities; +using Ryujinx.Horizon.Common; +using System; +using System.IO; + +namespace Ryujinx.HLE.HOS.Services.Time +{ + [Service("time:m")] // 9.0.0+ + class ITimeServiceManager : IpcService + { + private TimeManager _timeManager; + private int _automaticCorrectionEvent; + + public ITimeServiceManager(ServiceCtx context) + { + _timeManager = TimeManager.Instance; + _automaticCorrectionEvent = 0; + } + + [CommandCmif(0)] + // GetUserStaticService() -> object<nn::timesrv::detail::service::IStaticService> + public ResultCode GetUserStaticService(ServiceCtx context) + { + MakeObject(context, new IStaticServiceForPsc(_timeManager, TimePermissions.User)); + + return ResultCode.Success; + } + + [CommandCmif(5)] + // GetAdminStaticService() -> object<nn::timesrv::detail::service::IStaticService> + public ResultCode GetAdminStaticService(ServiceCtx context) + { + MakeObject(context, new IStaticServiceForPsc(_timeManager, TimePermissions.Admin)); + + return ResultCode.Success; + } + + [CommandCmif(6)] + // GetRepairStaticService() -> object<nn::timesrv::detail::service::IStaticService> + public ResultCode GetRepairStaticService(ServiceCtx context) + { + MakeObject(context, new IStaticServiceForPsc(_timeManager, TimePermissions.Repair)); + + return ResultCode.Success; + } + + [CommandCmif(9)] + // GetManufactureStaticService() -> object<nn::timesrv::detail::service::IStaticService> + public ResultCode GetManufactureStaticService(ServiceCtx context) + { + MakeObject(context, new IStaticServiceForPsc(_timeManager, TimePermissions.Manufacture)); + + return ResultCode.Success; + } + + [CommandCmif(10)] + // SetupStandardSteadyClock(nn::util::Uuid clock_source_id, nn::TimeSpanType setup_value, nn::TimeSpanType internal_offset, nn::TimeSpanType test_offset, bool is_rtc_reset_detected) + public ResultCode SetupStandardSteadyClock(ServiceCtx context) + { + UInt128 clockSourceId = context.RequestData.ReadStruct<UInt128>(); + TimeSpanType setupValue = context.RequestData.ReadStruct<TimeSpanType>(); + TimeSpanType internalOffset = context.RequestData.ReadStruct<TimeSpanType>(); + TimeSpanType testOffset = context.RequestData.ReadStruct<TimeSpanType>(); + bool isRtcResetDetected = context.RequestData.ReadBoolean(); + + ITickSource tickSource = context.Device.System.TickSource; + + _timeManager.SetupStandardSteadyClock(tickSource, clockSourceId, setupValue, internalOffset, testOffset, isRtcResetDetected); + + return ResultCode.Success; + } + + [CommandCmif(11)] + // SetupStandardLocalSystemClock(nn::time::SystemClockContext context, nn::time::PosixTime posix_time) + public ResultCode SetupStandardLocalSystemClock(ServiceCtx context) + { + SystemClockContext clockContext = context.RequestData.ReadStruct<SystemClockContext>(); + long posixTime = context.RequestData.ReadInt64(); + + ITickSource tickSource = context.Device.System.TickSource; + + _timeManager.SetupStandardLocalSystemClock(tickSource, clockContext, posixTime); + + return ResultCode.Success; + } + + [CommandCmif(12)] + // SetupStandardNetworkSystemClock(nn::time::SystemClockContext context, nn::TimeSpanType sufficient_accuracy) + public ResultCode SetupStandardNetworkSystemClock(ServiceCtx context) + { + SystemClockContext clockContext = context.RequestData.ReadStruct<SystemClockContext>(); + TimeSpanType sufficientAccuracy = context.RequestData.ReadStruct<TimeSpanType>(); + + _timeManager.SetupStandardNetworkSystemClock(clockContext, sufficientAccuracy); + + return ResultCode.Success; + } + + [CommandCmif(13)] + // SetupStandardUserSystemClock(bool automatic_correction_enabled, nn::time::SteadyClockTimePoint steady_clock_timepoint) + public ResultCode SetupStandardUserSystemClock(ServiceCtx context) + { + bool isAutomaticCorrectionEnabled = context.RequestData.ReadBoolean(); + + context.RequestData.BaseStream.Position += 7; + + SteadyClockTimePoint steadyClockTimePoint = context.RequestData.ReadStruct<SteadyClockTimePoint>(); + + ITickSource tickSource = context.Device.System.TickSource; + + _timeManager.SetupStandardUserSystemClock(tickSource, isAutomaticCorrectionEnabled, steadyClockTimePoint); + + return ResultCode.Success; + } + + [CommandCmif(14)] + // SetupTimeZoneManager(nn::time::LocationName location_name, nn::time::SteadyClockTimePoint timezone_update_timepoint, u32 total_location_name_count, nn::time::TimeZoneRuleVersion timezone_rule_version, buffer<nn::time::TimeZoneBinary, 0x21> timezone_binary) + public ResultCode SetupTimeZoneManager(ServiceCtx context) + { + string locationName = StringUtils.ReadInlinedAsciiString(context.RequestData, 0x24); + SteadyClockTimePoint timeZoneUpdateTimePoint = context.RequestData.ReadStruct<SteadyClockTimePoint>(); + uint totalLocationNameCount = context.RequestData.ReadUInt32(); + UInt128 timeZoneRuleVersion = context.RequestData.ReadStruct<UInt128>(); + + (ulong bufferPosition, ulong bufferSize) = context.Request.GetBufferType0x21(); + + byte[] temp = new byte[bufferSize]; + + context.Memory.Read(bufferPosition, temp); + + using (MemoryStream timeZoneBinaryStream = new MemoryStream(temp)) + { + _timeManager.SetupTimeZoneManager(locationName, timeZoneUpdateTimePoint, totalLocationNameCount, timeZoneRuleVersion, timeZoneBinaryStream); + } + + return ResultCode.Success; + } + + [CommandCmif(15)] + // SetupEphemeralNetworkSystemClock() + public ResultCode SetupEphemeralNetworkSystemClock(ServiceCtx context) + { + _timeManager.SetupEphemeralNetworkSystemClock(); + + return ResultCode.Success; + } + + [CommandCmif(50)] + // Unknown50() -> handle<copy> + public ResultCode Unknown50(ServiceCtx context) + { + // TODO: figure out the usage of this event + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(51)] + // Unknown51() -> handle<copy> + public ResultCode Unknown51(ServiceCtx context) + { + // TODO: figure out the usage of this event + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(52)] + // Unknown52() -> handle<copy> + public ResultCode Unknown52(ServiceCtx context) + { + // TODO: figure out the usage of this event + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(60)] + // GetStandardUserSystemClockAutomaticCorrectionEvent() -> handle<copy> + public ResultCode GetStandardUserSystemClockAutomaticCorrectionEvent(ServiceCtx context) + { + if (_automaticCorrectionEvent == 0) + { + if (context.Process.HandleTable.GenerateHandle(_timeManager.StandardUserSystemClock.GetAutomaticCorrectionReadableEvent(), out _automaticCorrectionEvent) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_automaticCorrectionEvent); + + return ResultCode.Success; + } + + [CommandCmif(100)] + // SetStandardSteadyClockRtcOffset(nn::TimeSpanType rtc_offset) + public ResultCode SetStandardSteadyClockRtcOffset(ServiceCtx context) + { + TimeSpanType rtcOffset = context.RequestData.ReadStruct<TimeSpanType>(); + + ITickSource tickSource = context.Device.System.TickSource; + + _timeManager.SetStandardSteadyClockRtcOffset(tickSource, rtcOffset); + + return ResultCode.Success; + } + + [CommandCmif(200)] + // GetAlarmRegistrationEvent() -> handle<copy> + public ResultCode GetAlarmRegistrationEvent(ServiceCtx context) + { + // TODO + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(201)] + // UpdateSteadyAlarms() + public ResultCode UpdateSteadyAlarms(ServiceCtx context) + { + // TODO + throw new ServiceNotImplementedException(this, context); + } + + [CommandCmif(202)] + // TryGetNextSteadyClockAlarmSnapshot() -> (bool, nn::time::SteadyClockAlarmSnapshot) + public ResultCode TryGetNextSteadyClockAlarmSnapshot(ServiceCtx context) + { + // TODO + throw new ServiceNotImplementedException(this, context); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/ResultCode.cs b/src/Ryujinx.HLE/HOS/Services/Time/ResultCode.cs new file mode 100644 index 00000000..3b042ec0 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/ResultCode.cs @@ -0,0 +1,24 @@ +namespace Ryujinx.HLE.HOS.Services.Time +{ + public enum ResultCode + { + ModuleId = 116, + ErrorCodeShift = 9, + + Success = 0, + + TimeServiceNotInitialized = (0 << ErrorCodeShift) | ModuleId, + PermissionDenied = (1 << ErrorCodeShift) | ModuleId, + TimeMismatch = (102 << ErrorCodeShift) | ModuleId, + UninitializedClock = (103 << ErrorCodeShift) | ModuleId, + TimeNotFound = (200 << ErrorCodeShift) | ModuleId, + Overflow = (201 << ErrorCodeShift) | ModuleId, + LocationNameTooLong = (801 << ErrorCodeShift) | ModuleId, + OutOfRange = (902 << ErrorCodeShift) | ModuleId, + TimeZoneConversionFailed = (903 << ErrorCodeShift) | ModuleId, + TimeZoneNotFound = (989 << ErrorCodeShift) | ModuleId, + NotImplemented = (990 << ErrorCodeShift) | ModuleId, + NetworkTimeNotAvailable = (1000 << ErrorCodeShift) | ModuleId, + NetworkTimeTaskCanceled = (1003 << ErrorCodeShift) | ModuleId, + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ISteadyClock.cs b/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ISteadyClock.cs new file mode 100644 index 00000000..97d7884e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ISteadyClock.cs @@ -0,0 +1,155 @@ +using Ryujinx.Common; +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Services.Time.Clock; + +namespace Ryujinx.HLE.HOS.Services.Time.StaticService +{ + class ISteadyClock : IpcService + { + private SteadyClockCore _steadyClock; + private bool _writePermission; + private bool _bypassUninitializedClock; + + public ISteadyClock(SteadyClockCore steadyClock, bool writePermission, bool bypassUninitializedClock) + { + _steadyClock = steadyClock; + _writePermission = writePermission; + _bypassUninitializedClock = bypassUninitializedClock; + } + + [CommandCmif(0)] + // GetCurrentTimePoint() -> nn::time::SteadyClockTimePoint + public ResultCode GetCurrentTimePoint(ServiceCtx context) + { + if (!_bypassUninitializedClock && !_steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + ITickSource tickSource = context.Device.System.TickSource; + + SteadyClockTimePoint currentTimePoint = _steadyClock.GetCurrentTimePoint(tickSource); + + context.ResponseData.WriteStruct(currentTimePoint); + + return ResultCode.Success; + } + + [CommandCmif(2)] + // GetTestOffset() -> nn::TimeSpanType + public ResultCode GetTestOffset(ServiceCtx context) + { + if (!_bypassUninitializedClock && !_steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + context.ResponseData.WriteStruct(_steadyClock.GetTestOffset()); + + return ResultCode.Success; + } + + [CommandCmif(3)] + // SetTestOffset(nn::TimeSpanType) + public ResultCode SetTestOffset(ServiceCtx context) + { + if (!_writePermission) + { + return ResultCode.PermissionDenied; + } + + if (!_bypassUninitializedClock && !_steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + TimeSpanType testOffset = context.RequestData.ReadStruct<TimeSpanType>(); + + _steadyClock.SetTestOffset(testOffset); + + return ResultCode.Success; + } + + [CommandCmif(100)] // 2.0.0+ + // GetRtcValue() -> u64 + public ResultCode GetRtcValue(ServiceCtx context) + { + if (!_bypassUninitializedClock && !_steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + ResultCode result = _steadyClock.GetRtcValue(out ulong rtcValue); + + if (result == ResultCode.Success) + { + context.ResponseData.Write(rtcValue); + } + + return result; + } + + [CommandCmif(101)] // 2.0.0+ + // IsRtcResetDetected() -> bool + public ResultCode IsRtcResetDetected(ServiceCtx context) + { + if (!_bypassUninitializedClock && !_steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + context.ResponseData.Write(_steadyClock.IsRtcResetDetected()); + + return ResultCode.Success; + } + + [CommandCmif(102)] // 2.0.0+ + // GetSetupResultValue() -> u32 + public ResultCode GetSetupResultValue(ServiceCtx context) + { + if (!_bypassUninitializedClock && !_steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + context.ResponseData.Write((uint)_steadyClock.GetSetupResultValue()); + + return ResultCode.Success; + } + + [CommandCmif(200)] // 3.0.0+ + // GetInternalOffset() -> nn::TimeSpanType + public ResultCode GetInternalOffset(ServiceCtx context) + { + if (!_bypassUninitializedClock && !_steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + context.ResponseData.WriteStruct(_steadyClock.GetInternalOffset()); + + return ResultCode.Success; + } + + [CommandCmif(201)] // 3.0.0-3.0.2 + // SetInternalOffset(nn::TimeSpanType) + public ResultCode SetInternalOffset(ServiceCtx context) + { + if (!_writePermission) + { + return ResultCode.PermissionDenied; + } + + if (!_bypassUninitializedClock && !_steadyClock.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + TimeSpanType internalOffset = context.RequestData.ReadStruct<TimeSpanType>(); + + _steadyClock.SetInternalOffset(internalOffset); + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ISystemClock.cs b/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ISystemClock.cs new file mode 100644 index 00000000..3cd0a4a6 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ISystemClock.cs @@ -0,0 +1,131 @@ +using Ryujinx.Common; +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Ipc; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using Ryujinx.Horizon.Common; +using System; + +namespace Ryujinx.HLE.HOS.Services.Time.StaticService +{ + class ISystemClock : IpcService + { + private SystemClockCore _clockCore; + private bool _writePermission; + private bool _bypassUninitializedClock; + private int _operationEventReadableHandle; + + public ISystemClock(SystemClockCore clockCore, bool writePermission, bool bypassUninitializedClock) + { + _clockCore = clockCore; + _writePermission = writePermission; + _bypassUninitializedClock = bypassUninitializedClock; + _operationEventReadableHandle = 0; + } + + [CommandCmif(0)] + // GetCurrentTime() -> nn::time::PosixTime + public ResultCode GetCurrentTime(ServiceCtx context) + { + if (!_bypassUninitializedClock && !_clockCore.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + ITickSource tickSource = context.Device.System.TickSource; + + ResultCode result = _clockCore.GetCurrentTime(tickSource, out long posixTime); + + if (result == ResultCode.Success) + { + context.ResponseData.Write(posixTime); + } + + return result; + } + + [CommandCmif(1)] + // SetCurrentTime(nn::time::PosixTime) + public ResultCode SetCurrentTime(ServiceCtx context) + { + if (!_writePermission) + { + return ResultCode.PermissionDenied; + } + + if (!_bypassUninitializedClock && !_clockCore.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + long posixTime = context.RequestData.ReadInt64(); + + ITickSource tickSource = context.Device.System.TickSource; + + return _clockCore.SetCurrentTime(tickSource, posixTime); + } + + [CommandCmif(2)] + // GetClockContext() -> nn::time::SystemClockContext + public ResultCode GetSystemClockContext(ServiceCtx context) + { + if (!_bypassUninitializedClock && !_clockCore.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + ITickSource tickSource = context.Device.System.TickSource; + + ResultCode result = _clockCore.GetClockContext(tickSource, out SystemClockContext clockContext); + + if (result == ResultCode.Success) + { + context.ResponseData.WriteStruct(clockContext); + } + + return result; + } + + [CommandCmif(3)] + // SetClockContext(nn::time::SystemClockContext) + public ResultCode SetSystemClockContext(ServiceCtx context) + { + if (!_writePermission) + { + return ResultCode.PermissionDenied; + } + + if (!_bypassUninitializedClock && !_clockCore.IsInitialized()) + { + return ResultCode.UninitializedClock; + } + + SystemClockContext clockContext = context.RequestData.ReadStruct<SystemClockContext>(); + + ResultCode result = _clockCore.SetSystemClockContext(clockContext); + + return result; + } + + [CommandCmif(4)] // 9.0.0+ + // GetOperationEventReadableHandle() -> handle<copy> + public ResultCode GetOperationEventReadableHandle(ServiceCtx context) + { + if (_operationEventReadableHandle == 0) + { + KEvent kEvent = new KEvent(context.Device.System.KernelContext); + + _clockCore.RegisterOperationEvent(kEvent.WritableEvent); + + if (context.Process.HandleTable.GenerateHandle(kEvent.ReadableEvent, out _operationEventReadableHandle) != Result.Success) + { + throw new InvalidOperationException("Out of handles!"); + } + } + + context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_operationEventReadableHandle); + + return ResultCode.Success; + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ITimeZoneServiceForGlue.cs b/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ITimeZoneServiceForGlue.cs new file mode 100644 index 00000000..96a7e604 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ITimeZoneServiceForGlue.cs @@ -0,0 +1,142 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using Ryujinx.HLE.Utilities; +using Ryujinx.Memory; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ryujinx.HLE.HOS.Services.Time.StaticService +{ + class ITimeZoneServiceForGlue : IpcService + { + private TimeZoneContentManager _timeZoneContentManager; + private ITimeZoneServiceForPsc _inner; + private bool _writePermission; + + public ITimeZoneServiceForGlue(TimeZoneContentManager timeZoneContentManager, bool writePermission) + { + _timeZoneContentManager = timeZoneContentManager; + _writePermission = writePermission; + _inner = new ITimeZoneServiceForPsc(timeZoneContentManager.Manager, writePermission); + } + + [CommandCmif(0)] + // GetDeviceLocationName() -> nn::time::LocationName + public ResultCode GetDeviceLocationName(ServiceCtx context) + { + return _inner.GetDeviceLocationName(context); + } + + [CommandCmif(1)] + // SetDeviceLocationName(nn::time::LocationName) + public ResultCode SetDeviceLocationName(ServiceCtx context) + { + if (!_writePermission) + { + return ResultCode.PermissionDenied; + } + + string locationName = StringUtils.ReadInlinedAsciiString(context.RequestData, 0x24); + + return _timeZoneContentManager.SetDeviceLocationName(locationName); + } + + [CommandCmif(2)] + // GetTotalLocationNameCount() -> u32 + public ResultCode GetTotalLocationNameCount(ServiceCtx context) + { + return _inner.GetTotalLocationNameCount(context); + } + + [CommandCmif(3)] + // LoadLocationNameList(u32 index) -> (u32 outCount, buffer<nn::time::LocationName, 6>) + public ResultCode LoadLocationNameList(ServiceCtx context) + { + uint index = context.RequestData.ReadUInt32(); + ulong bufferPosition = context.Request.ReceiveBuff[0].Position; + ulong bufferSize = context.Request.ReceiveBuff[0].Size; + + ResultCode errorCode = _timeZoneContentManager.LoadLocationNameList(index, out string[] locationNameArray, (uint)bufferSize / 0x24); + + if (errorCode == 0) + { + uint offset = 0; + + foreach (string locationName in locationNameArray) + { + int padding = 0x24 - locationName.Length; + + if (padding < 0) + { + return ResultCode.LocationNameTooLong; + } + + context.Memory.Write(bufferPosition + offset, Encoding.ASCII.GetBytes(locationName)); + MemoryHelper.FillWithZeros(context.Memory, bufferPosition + offset + (ulong)locationName.Length, padding); + + offset += 0x24; + } + + context.ResponseData.Write((uint)locationNameArray.Length); + } + + return errorCode; + } + + [CommandCmif(4)] + // LoadTimeZoneRule(nn::time::LocationName locationName) -> buffer<nn::time::TimeZoneRule, 0x16> + public ResultCode LoadTimeZoneRule(ServiceCtx context) + { + ulong bufferPosition = context.Request.ReceiveBuff[0].Position; + ulong bufferSize = context.Request.ReceiveBuff[0].Size; + + if (bufferSize != 0x4000) + { + // TODO: find error code here + Logger.Error?.Print(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{bufferSize:x} (expected 0x4000)"); + + throw new InvalidOperationException(); + } + + string locationName = StringUtils.ReadInlinedAsciiString(context.RequestData, 0x24); + + using (WritableRegion region = context.Memory.GetWritableRegion(bufferPosition, Unsafe.SizeOf<TimeZoneRule>())) + { + ref TimeZoneRule rules = ref MemoryMarshal.Cast<byte, TimeZoneRule>(region.Memory.Span)[0]; + + return _timeZoneContentManager.LoadTimeZoneRule(ref rules, locationName); + } + } + + [CommandCmif(100)] + // ToCalendarTime(nn::time::PosixTime time, buffer<nn::time::TimeZoneRule, 0x15> rules) -> (nn::time::CalendarTime, nn::time::sf::CalendarAdditionalInfo) + public ResultCode ToCalendarTime(ServiceCtx context) + { + return _inner.ToCalendarTime(context); + } + + [CommandCmif(101)] + // ToCalendarTimeWithMyRule(nn::time::PosixTime) -> (nn::time::CalendarTime, nn::time::sf::CalendarAdditionalInfo) + public ResultCode ToCalendarTimeWithMyRule(ServiceCtx context) + { + return _inner.ToCalendarTimeWithMyRule(context); + } + + [CommandCmif(201)] + // ToPosixTime(nn::time::CalendarTime calendarTime, buffer<nn::time::TimeZoneRule, 0x15> rules) -> (u32 outCount, buffer<nn::time::PosixTime, 0xa>) + public ResultCode ToPosixTime(ServiceCtx context) + { + return _inner.ToPosixTime(context); + } + + [CommandCmif(202)] + // ToPosixTimeWithMyRule(nn::time::CalendarTime calendarTime) -> (u32 outCount, buffer<nn::time::PosixTime, 0xa>) + public ResultCode ToPosixTimeWithMyRule(ServiceCtx context) + { + return _inner.ToPosixTimeWithMyRule(context); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ITimeZoneServiceForPsc.cs b/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ITimeZoneServiceForPsc.cs new file mode 100644 index 00000000..3c9ac71f --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/StaticService/ITimeZoneServiceForPsc.cs @@ -0,0 +1,303 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using Ryujinx.HLE.Utilities; +using Ryujinx.Memory; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.StaticService +{ + class ITimeZoneServiceForPsc : IpcService + { + private TimeZoneManager _timeZoneManager; + private bool _writePermission; + + public ITimeZoneServiceForPsc(TimeZoneManager timeZoneManager, bool writePermission) + { + _timeZoneManager = timeZoneManager; + _writePermission = writePermission; + } + + [CommandCmif(0)] + // GetDeviceLocationName() -> nn::time::LocationName + public ResultCode GetDeviceLocationName(ServiceCtx context) + { + ResultCode result = _timeZoneManager.GetDeviceLocationName(out string deviceLocationName); + + if (result == ResultCode.Success) + { + WriteLocationName(context, deviceLocationName); + } + + return result; + } + + [CommandCmif(1)] + // SetDeviceLocationName(nn::time::LocationName) + public ResultCode SetDeviceLocationName(ServiceCtx context) + { + if (!_writePermission) + { + return ResultCode.PermissionDenied; + } + + return ResultCode.NotImplemented; + } + + [CommandCmif(2)] + // GetTotalLocationNameCount() -> u32 + public ResultCode GetTotalLocationNameCount(ServiceCtx context) + { + ResultCode result = _timeZoneManager.GetTotalLocationNameCount(out uint totalLocationNameCount); + + if (result == ResultCode.Success) + { + context.ResponseData.Write(totalLocationNameCount); + } + + return ResultCode.Success; + } + + [CommandCmif(3)] + // LoadLocationNameList(u32 index) -> (u32 outCount, buffer<nn::time::LocationName, 6>) + public ResultCode LoadLocationNameList(ServiceCtx context) + { + return ResultCode.NotImplemented; + } + + [CommandCmif(4)] + // LoadTimeZoneRule(nn::time::LocationName locationName) -> buffer<nn::time::TimeZoneRule, 0x16> + public ResultCode LoadTimeZoneRule(ServiceCtx context) + { + return ResultCode.NotImplemented; + } + + [CommandCmif(5)] // 2.0.0+ + // GetTimeZoneRuleVersion() -> nn::time::TimeZoneRuleVersion + public ResultCode GetTimeZoneRuleVersion(ServiceCtx context) + { + ResultCode result = _timeZoneManager.GetTimeZoneRuleVersion(out UInt128 timeZoneRuleVersion); + + if (result == ResultCode.Success) + { + context.ResponseData.WriteStruct(timeZoneRuleVersion); + } + + return result; + } + + [CommandCmif(6)] // 5.0.0+ + // GetDeviceLocationNameAndUpdatedTime() -> (nn::time::LocationName, nn::time::SteadyClockTimePoint) + public ResultCode GetDeviceLocationNameAndUpdatedTime(ServiceCtx context) + { + ResultCode result = _timeZoneManager.GetDeviceLocationName(out string deviceLocationName); + + if (result == ResultCode.Success) + { + result = _timeZoneManager.GetUpdatedTime(out SteadyClockTimePoint timeZoneUpdateTimePoint); + + if (result == ResultCode.Success) + { + WriteLocationName(context, deviceLocationName); + + // Skip padding + context.ResponseData.BaseStream.Position += 0x4; + + context.ResponseData.WriteStruct(timeZoneUpdateTimePoint); + } + } + + return result; + } + + [CommandCmif(7)] // 9.0.0+ + // SetDeviceLocationNameWithTimeZoneRule(nn::time::LocationName locationName, buffer<nn::time::TimeZoneBinary, 0x21> timeZoneBinary) + public ResultCode SetDeviceLocationNameWithTimeZoneRule(ServiceCtx context) + { + if (!_writePermission) + { + return ResultCode.PermissionDenied; + } + + (ulong bufferPosition, ulong bufferSize) = context.Request.GetBufferType0x21(); + + string locationName = StringUtils.ReadInlinedAsciiString(context.RequestData, 0x24); + + ResultCode result; + + byte[] temp = new byte[bufferSize]; + + context.Memory.Read(bufferPosition, temp); + + using (MemoryStream timeZoneBinaryStream = new MemoryStream(temp)) + { + result = _timeZoneManager.SetDeviceLocationNameWithTimeZoneRule(locationName, timeZoneBinaryStream); + } + + return result; + } + + [CommandCmif(8)] // 9.0.0+ + // ParseTimeZoneBinary(buffer<nn::time::TimeZoneBinary, 0x21> timeZoneBinary) -> buffer<nn::time::TimeZoneRule, 0x16> + public ResultCode ParseTimeZoneBinary(ServiceCtx context) + { + (ulong bufferPosition, ulong bufferSize) = context.Request.GetBufferType0x21(); + + ulong timeZoneRuleBufferPosition = context.Request.ReceiveBuff[0].Position; + ulong timeZoneRuleBufferSize = context.Request.ReceiveBuff[0].Size; + + if (timeZoneRuleBufferSize != 0x4000) + { + // TODO: find error code here + Logger.Error?.Print(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{timeZoneRuleBufferSize:x} (expected 0x4000)"); + + throw new InvalidOperationException(); + } + + ResultCode result; + + byte[] temp = new byte[bufferSize]; + + context.Memory.Read(bufferPosition, temp); + + using (MemoryStream timeZoneBinaryStream = new MemoryStream(temp)) + { + using (WritableRegion region = context.Memory.GetWritableRegion(timeZoneRuleBufferPosition, Unsafe.SizeOf<TimeZoneRule>())) + { + ref TimeZoneRule rule = ref MemoryMarshal.Cast<byte, TimeZoneRule>(region.Memory.Span)[0]; + + result = _timeZoneManager.ParseTimeZoneRuleBinary(ref rule, timeZoneBinaryStream); + } + } + + return result; + } + + [CommandCmif(20)] // 9.0.0+ + // GetDeviceLocationNameOperationEventReadableHandle() -> handle<copy> + public ResultCode GetDeviceLocationNameOperationEventReadableHandle(ServiceCtx context) + { + return ResultCode.NotImplemented; + } + + [CommandCmif(100)] + // ToCalendarTime(nn::time::PosixTime time, buffer<nn::time::TimeZoneRule, 0x15> rules) -> (nn::time::CalendarTime, nn::time::sf::CalendarAdditionalInfo) + public ResultCode ToCalendarTime(ServiceCtx context) + { + long posixTime = context.RequestData.ReadInt64(); + ulong bufferPosition = context.Request.SendBuff[0].Position; + ulong bufferSize = context.Request.SendBuff[0].Size; + + if (bufferSize != 0x4000) + { + // TODO: find error code here + Logger.Error?.Print(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{bufferSize:x} (expected 0x4000)"); + + throw new InvalidOperationException(); + } + + ReadOnlySpan<TimeZoneRule> rules = MemoryMarshal.Cast<byte, TimeZoneRule>(context.Memory.GetSpan(bufferPosition, (int)bufferSize)); + + ResultCode resultCode = _timeZoneManager.ToCalendarTime(in rules[0], posixTime, out CalendarInfo calendar); + + if (resultCode == 0) + { + context.ResponseData.WriteStruct(calendar); + } + + return resultCode; + } + + [CommandCmif(101)] + // ToCalendarTimeWithMyRule(nn::time::PosixTime) -> (nn::time::CalendarTime, nn::time::sf::CalendarAdditionalInfo) + public ResultCode ToCalendarTimeWithMyRule(ServiceCtx context) + { + long posixTime = context.RequestData.ReadInt64(); + + ResultCode resultCode = _timeZoneManager.ToCalendarTimeWithMyRules(posixTime, out CalendarInfo calendar); + + if (resultCode == ResultCode.Success) + { + context.ResponseData.WriteStruct(calendar); + } + + return resultCode; + } + + [CommandCmif(201)] + // ToPosixTime(nn::time::CalendarTime calendarTime, buffer<nn::time::TimeZoneRule, 0x15> rules) -> (u32 outCount, buffer<nn::time::PosixTime, 0xa>) + public ResultCode ToPosixTime(ServiceCtx context) + { + ulong inBufferPosition = context.Request.SendBuff[0].Position; + ulong inBufferSize = context.Request.SendBuff[0].Size; + + CalendarTime calendarTime = context.RequestData.ReadStruct<CalendarTime>(); + + if (inBufferSize != 0x4000) + { + // TODO: find error code here + Logger.Error?.Print(LogClass.ServiceTime, $"TimeZoneRule buffer size is 0x{inBufferSize:x} (expected 0x4000)"); + + throw new InvalidOperationException(); + } + + ReadOnlySpan<TimeZoneRule> rules = MemoryMarshal.Cast<byte, TimeZoneRule>(context.Memory.GetSpan(inBufferPosition, (int)inBufferSize)); + + ResultCode resultCode = _timeZoneManager.ToPosixTime(in rules[0], calendarTime, out long posixTime); + + if (resultCode == ResultCode.Success) + { + ulong outBufferPosition = context.Request.RecvListBuff[0].Position; + ulong outBufferSize = context.Request.RecvListBuff[0].Size; + + context.Memory.Write(outBufferPosition, posixTime); + context.ResponseData.Write(1); + } + + return resultCode; + } + + [CommandCmif(202)] + // ToPosixTimeWithMyRule(nn::time::CalendarTime calendarTime) -> (u32 outCount, buffer<nn::time::PosixTime, 0xa>) + public ResultCode ToPosixTimeWithMyRule(ServiceCtx context) + { + CalendarTime calendarTime = context.RequestData.ReadStruct<CalendarTime>(); + + ResultCode resultCode = _timeZoneManager.ToPosixTimeWithMyRules(calendarTime, out long posixTime); + + if (resultCode == ResultCode.Success) + { + ulong outBufferPosition = context.Request.RecvListBuff[0].Position; + ulong outBufferSize = context.Request.RecvListBuff[0].Size; + + context.Memory.Write(outBufferPosition, posixTime); + + // There could be only one result on one calendar as leap seconds aren't supported. + context.ResponseData.Write(1); + } + + return resultCode; + } + + private void WriteLocationName(ServiceCtx context, string locationName) + { + char[] locationNameArray = locationName.ToCharArray(); + + int padding = 0x24 - locationNameArray.Length; + + Debug.Assert(padding >= 0, "LocationName exceeded limit (0x24 bytes)"); + + context.ResponseData.Write(locationNameArray); + + for (int index = 0; index < padding; index++) + { + context.ResponseData.Write((byte)0); + } + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeManager.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeManager.cs new file mode 100644 index 00000000..e3b65f2a --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeManager.cs @@ -0,0 +1,182 @@ +using Ryujinx.Cpu; +using Ryujinx.HLE.Exceptions; +using Ryujinx.HLE.HOS.Kernel.Memory; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using System; +using System.IO; + +namespace Ryujinx.HLE.HOS.Services.Time +{ + class TimeManager + { + private static TimeManager _instance; + + public static TimeManager Instance + { + get + { + if (_instance == null) + { + _instance = new TimeManager(); + } + + return _instance; + } + } + + public StandardSteadyClockCore StandardSteadyClock { get; } + public TickBasedSteadyClockCore TickBasedSteadyClock { get; } + public StandardLocalSystemClockCore StandardLocalSystemClock { get; } + public StandardNetworkSystemClockCore StandardNetworkSystemClock { get; } + public StandardUserSystemClockCore StandardUserSystemClock { get; } + public TimeZoneContentManager TimeZone { get; } + public EphemeralNetworkSystemClockCore EphemeralNetworkSystemClock { get; } + public TimeSharedMemory SharedMemory { get; } + public LocalSystemClockContextWriter LocalClockContextWriter { get; } + public NetworkSystemClockContextWriter NetworkClockContextWriter { get; } + public EphemeralNetworkSystemClockContextWriter EphemeralClockContextWriter { get; } + + // TODO: 9.0.0+ power states and alarms + + public TimeManager() + { + StandardSteadyClock = new StandardSteadyClockCore(); + TickBasedSteadyClock = new TickBasedSteadyClockCore(); + StandardLocalSystemClock = new StandardLocalSystemClockCore(StandardSteadyClock); + StandardNetworkSystemClock = new StandardNetworkSystemClockCore(StandardSteadyClock); + StandardUserSystemClock = new StandardUserSystemClockCore(StandardLocalSystemClock, StandardNetworkSystemClock); + TimeZone = new TimeZoneContentManager(); + EphemeralNetworkSystemClock = new EphemeralNetworkSystemClockCore(TickBasedSteadyClock); + SharedMemory = new TimeSharedMemory(); + LocalClockContextWriter = new LocalSystemClockContextWriter(SharedMemory); + NetworkClockContextWriter = new NetworkSystemClockContextWriter(SharedMemory); + EphemeralClockContextWriter = new EphemeralNetworkSystemClockContextWriter(); + } + + public void Initialize(Switch device, Horizon system, KSharedMemory sharedMemory, SharedMemoryStorage timeSharedMemoryStorage, int timeSharedMemorySize) + { + SharedMemory.Initialize(device, sharedMemory, timeSharedMemoryStorage, timeSharedMemorySize); + + // Here we use system on purpose as device. System isn't initialized at this point. + StandardUserSystemClock.CreateAutomaticCorrectionEvent(system); + } + + public void InitializeTimeZone(Switch device) + { + TimeZone.Initialize(this, device); + } + + public void SetupStandardSteadyClock(ITickSource tickSource, UInt128 clockSourceId, TimeSpanType setupValue, TimeSpanType internalOffset, TimeSpanType testOffset, bool isRtcResetDetected) + { + SetupInternalStandardSteadyClock(clockSourceId, setupValue, internalOffset, testOffset, isRtcResetDetected); + + TimeSpanType currentTimePoint = StandardSteadyClock.GetCurrentRawTimePoint(tickSource); + + SharedMemory.SetupStandardSteadyClock(tickSource, clockSourceId, currentTimePoint); + + // TODO: propagate IPC late binding of "time:s" and "time:p" + } + + private void SetupInternalStandardSteadyClock(UInt128 clockSourceId, TimeSpanType setupValue, TimeSpanType internalOffset, TimeSpanType testOffset, bool isRtcResetDetected) + { + StandardSteadyClock.SetClockSourceId(clockSourceId); + StandardSteadyClock.SetSetupValue(setupValue); + StandardSteadyClock.SetInternalOffset(internalOffset); + StandardSteadyClock.SetTestOffset(testOffset); + + if (isRtcResetDetected) + { + StandardSteadyClock.SetRtcReset(); + } + + StandardSteadyClock.MarkInitialized(); + + // TODO: propagate IPC late binding of "time:s" and "time:p" + } + + public void SetupStandardLocalSystemClock(ITickSource tickSource, SystemClockContext clockContext, long posixTime) + { + StandardLocalSystemClock.SetUpdateCallbackInstance(LocalClockContextWriter); + + SteadyClockTimePoint currentTimePoint = StandardLocalSystemClock.GetSteadyClockCore().GetCurrentTimePoint(tickSource); + if (currentTimePoint.ClockSourceId == clockContext.SteadyTimePoint.ClockSourceId) + { + StandardLocalSystemClock.SetSystemClockContext(clockContext); + } + else + { + if (StandardLocalSystemClock.SetCurrentTime(tickSource, posixTime) != ResultCode.Success) + { + throw new InternalServiceException("Cannot set current local time"); + } + } + + StandardLocalSystemClock.MarkInitialized(); + + // TODO: propagate IPC late binding of "time:s" and "time:p" + } + + public void SetupStandardNetworkSystemClock(SystemClockContext clockContext, TimeSpanType sufficientAccuracy) + { + StandardNetworkSystemClock.SetUpdateCallbackInstance(NetworkClockContextWriter); + + if (StandardNetworkSystemClock.SetSystemClockContext(clockContext) != ResultCode.Success) + { + throw new InternalServiceException("Cannot set network SystemClockContext"); + } + + StandardNetworkSystemClock.SetStandardNetworkClockSufficientAccuracy(sufficientAccuracy); + StandardNetworkSystemClock.MarkInitialized(); + + // TODO: propagate IPC late binding of "time:s" and "time:p" + } + + public void SetupTimeZoneManager(string locationName, SteadyClockTimePoint timeZoneUpdatedTimePoint, uint totalLocationNameCount, UInt128 timeZoneRuleVersion, Stream timeZoneBinaryStream) + { + if (TimeZone.Manager.SetDeviceLocationNameWithTimeZoneRule(locationName, timeZoneBinaryStream) != ResultCode.Success) + { + throw new InternalServiceException("Cannot set DeviceLocationName with a given TimeZoneBinary"); + } + + TimeZone.Manager.SetUpdatedTime(timeZoneUpdatedTimePoint, true); + TimeZone.Manager.SetTotalLocationNameCount(totalLocationNameCount); + TimeZone.Manager.SetTimeZoneRuleVersion(timeZoneRuleVersion); + TimeZone.Manager.MarkInitialized(); + + // TODO: propagate IPC late binding of "time:s" and "time:p" + } + + public void SetupEphemeralNetworkSystemClock() + { + EphemeralNetworkSystemClock.SetUpdateCallbackInstance(EphemeralClockContextWriter); + EphemeralNetworkSystemClock.MarkInitialized(); + + // TODO: propagate IPC late binding of "time:s" and "time:p" + } + + public void SetupStandardUserSystemClock(ITickSource tickSource, bool isAutomaticCorrectionEnabled, SteadyClockTimePoint steadyClockTimePoint) + { + if (StandardUserSystemClock.SetAutomaticCorrectionEnabled(tickSource, isAutomaticCorrectionEnabled) != ResultCode.Success) + { + throw new InternalServiceException("Cannot set automatic user time correction state"); + } + + StandardUserSystemClock.SetAutomaticCorrectionUpdatedTime(steadyClockTimePoint); + StandardUserSystemClock.MarkInitialized(); + + SharedMemory.SetAutomaticCorrectionEnabled(isAutomaticCorrectionEnabled); + + // TODO: propagate IPC late binding of "time:s" and "time:p" + } + + public void SetStandardSteadyClockRtcOffset(ITickSource tickSource, TimeSpanType rtcOffset) + { + StandardSteadyClock.SetSetupValue(rtcOffset); + + TimeSpanType currentTimePoint = StandardSteadyClock.GetCurrentRawTimePoint(tickSource); + + SharedMemory.SetSteadyClockRawTimePoint(tickSource, currentTimePoint); + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeSharedMemory.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeSharedMemory.cs new file mode 100644 index 00000000..7063290b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeSharedMemory.cs @@ -0,0 +1,114 @@ +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Kernel.Memory; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using Ryujinx.HLE.HOS.Services.Time.Types; +using Ryujinx.HLE.Utilities; +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace Ryujinx.HLE.HOS.Services.Time +{ + class TimeSharedMemory + { + private Switch _device; + private KSharedMemory _sharedMemory; + private SharedMemoryStorage _timeSharedMemoryStorage; + private int _timeSharedMemorySize; + + private const uint SteadyClockContextOffset = 0x00; + private const uint LocalSystemClockContextOffset = 0x38; + private const uint NetworkSystemClockContextOffset = 0x80; + private const uint AutomaticCorrectionEnabledOffset = 0xC8; + + public void Initialize(Switch device, KSharedMemory sharedMemory, SharedMemoryStorage timeSharedMemoryStorage, int timeSharedMemorySize) + { + _device = device; + _sharedMemory = sharedMemory; + _timeSharedMemoryStorage = timeSharedMemoryStorage; + _timeSharedMemorySize = timeSharedMemorySize; + + // Clean the shared memory + timeSharedMemoryStorage.ZeroFill(); + } + + public KSharedMemory GetSharedMemory() + { + return _sharedMemory; + } + + public void SetupStandardSteadyClock(ITickSource tickSource, UInt128 clockSourceId, TimeSpanType currentTimePoint) + { + TimeSpanType ticksTimeSpan = TimeSpanType.FromTicks(tickSource.Counter, tickSource.Frequency); + + SteadyClockContext context = new SteadyClockContext + { + InternalOffset = (ulong)(currentTimePoint.NanoSeconds - ticksTimeSpan.NanoSeconds), + ClockSourceId = clockSourceId + }; + + WriteObjectToSharedMemory(SteadyClockContextOffset, 4, context); + } + + public void SetAutomaticCorrectionEnabled(bool isAutomaticCorrectionEnabled) + { + // We convert the bool to byte here as a bool in C# takes 4 bytes... + WriteObjectToSharedMemory(AutomaticCorrectionEnabledOffset, 0, Convert.ToByte(isAutomaticCorrectionEnabled)); + } + + public void SetSteadyClockRawTimePoint(ITickSource tickSource, TimeSpanType currentTimePoint) + { + SteadyClockContext context = ReadObjectFromSharedMemory<SteadyClockContext>(SteadyClockContextOffset, 4); + TimeSpanType ticksTimeSpan = TimeSpanType.FromTicks(tickSource.Counter, tickSource.Frequency); + + context.InternalOffset = (ulong)(currentTimePoint.NanoSeconds - ticksTimeSpan.NanoSeconds); + + WriteObjectToSharedMemory(SteadyClockContextOffset, 4, context); + } + + public void UpdateLocalSystemClockContext(SystemClockContext context) + { + WriteObjectToSharedMemory(LocalSystemClockContextOffset, 4, context); + } + + public void UpdateNetworkSystemClockContext(SystemClockContext context) + { + WriteObjectToSharedMemory(NetworkSystemClockContextOffset, 4, context); + } + + private T ReadObjectFromSharedMemory<T>(ulong offset, ulong padding) where T : unmanaged + { + T result; + uint index; + uint possiblyNewIndex; + + do + { + index = _timeSharedMemoryStorage.GetRef<uint>(offset); + + ulong objectOffset = offset + 4 + padding + (ulong)((index & 1) * Unsafe.SizeOf<T>()); + + result = _timeSharedMemoryStorage.GetRef<T>(objectOffset); + + Thread.MemoryBarrier(); + + possiblyNewIndex = _device.Memory.Read<uint>(offset); + } while (index != possiblyNewIndex); + + return result; + } + + private void WriteObjectToSharedMemory<T>(ulong offset, ulong padding, T value) where T : unmanaged + { + uint newIndex = _timeSharedMemoryStorage.GetRef<uint>(offset) + 1; + + ulong objectOffset = offset + 4 + padding + (ulong)((newIndex & 1) * Unsafe.SizeOf<T>()); + + _timeSharedMemoryStorage.GetRef<T>(objectOffset) = value; + + Thread.MemoryBarrier(); + + _timeSharedMemoryStorage.GetRef<uint>(offset) = newIndex; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs new file mode 100644 index 00000000..f7477e97 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZone.cs @@ -0,0 +1,1703 @@ +using Ryujinx.Common; +using Ryujinx.Common.Memory; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.Utilities; +using System; +using System.Buffers.Binary; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using static Ryujinx.HLE.HOS.Services.Time.TimeZone.TimeZoneRule; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + public class TimeZone + { + private const int TimeTypeSize = 8; + private const int EpochYear = 1970; + private const int YearBase = 1900; + private const int EpochWeekDay = 4; + private const int SecondsPerMinute = 60; + private const int MinutesPerHour = 60; + private const int HoursPerDays = 24; + private const int DaysPerWekk = 7; + private const int DaysPerNYear = 365; + private const int DaysPerLYear = 366; + private const int MonthsPerYear = 12; + private const int SecondsPerHour = SecondsPerMinute * MinutesPerHour; + private const int SecondsPerDay = SecondsPerHour * HoursPerDays; + + private const int YearsPerRepeat = 400; + private const long AverageSecondsPerYear = 31556952; + private const long SecondsPerRepeat = YearsPerRepeat * AverageSecondsPerYear; + + private static readonly int[] YearLengths = { DaysPerNYear, DaysPerLYear }; + private static readonly int[][] MonthsLengths = new int[][] + { + new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, + new int[] { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } + }; + + private static ReadOnlySpan<byte> TimeZoneDefaultRule => ",M4.1.0,M10.5.0"u8; + + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x10)] + private struct CalendarTimeInternal + { + // NOTE: On the IPC side this is supposed to be a 16 bits value but internally this need to be a 64 bits value for ToPosixTime. + public long Year; + public sbyte Month; + public sbyte Day; + public sbyte Hour; + public sbyte Minute; + public sbyte Second; + + public int CompareTo(CalendarTimeInternal other) + { + if (Year != other.Year) + { + if (Year < other.Year) + { + return -1; + } + + return 1; + } + + if (Month != other.Month) + { + return Month - other.Month; + } + + if (Day != other.Day) + { + return Day - other.Day; + } + + if (Hour != other.Hour) + { + return Hour - other.Hour; + } + + if (Minute != other.Minute) + { + return Minute - other.Minute; + } + + if (Second != other.Second) + { + return Second - other.Second; + } + + return 0; + } + } + + private enum RuleType + { + JulianDay, + DayOfYear, + MonthNthDayOfWeek + } + + private struct Rule + { + public RuleType Type; + public int Day; + public int Week; + public int Month; + public int TransitionTime; + } + + private static int Detzcode32(ReadOnlySpan<byte> bytes) + { + return BinaryPrimitives.ReadInt32BigEndian(bytes); + } + + private static int Detzcode32(int value) + { + if (BitConverter.IsLittleEndian) + { + return BinaryPrimitives.ReverseEndianness(value); + } + + return value; + } + + private static long Detzcode64(ReadOnlySpan<byte> bytes) + { + return BinaryPrimitives.ReadInt64BigEndian(bytes); + } + + private static bool DifferByRepeat(long t1, long t0) + { + return (t1 - t0) == SecondsPerRepeat; + } + + private static bool TimeTypeEquals(in TimeZoneRule outRules, byte aIndex, byte bIndex) + { + if (aIndex < 0 || aIndex >= outRules.TypeCount || bIndex < 0 || bIndex >= outRules.TypeCount) + { + return false; + } + + TimeTypeInfo a = outRules.Ttis[aIndex]; + TimeTypeInfo b = outRules.Ttis[bIndex]; + + return a.GmtOffset == b.GmtOffset && + a.IsDaySavingTime == b.IsDaySavingTime && + a.IsStandardTimeDaylight == b.IsStandardTimeDaylight && + a.IsGMT == b.IsGMT && + StringUtils.CompareCStr(outRules.Chars[a.AbbreviationListIndex..], outRules.Chars[b.AbbreviationListIndex..]) == 0; + } + + private static int GetQZName(ReadOnlySpan<byte> name, int namePosition, char delimiter) + { + int i = namePosition; + + while (name[i] != '\0' && name[i] != delimiter) + { + i++; + } + + return i; + } + + private static int GetTZName(ReadOnlySpan<byte> name, int namePosition) + { + int i = namePosition; + + char c; + + while ((c = (char)name[i]) != '\0' && !char.IsDigit(c) && c != ',' && c != '-' && c != '+') + { + i++; + } + + return i; + } + + private static bool GetNum(ReadOnlySpan<byte> name, ref int namePosition, out int num, int min, int max) + { + num = 0; + + if (namePosition >= name.Length) + { + return false; + } + + char c = (char)name[namePosition]; + + if (!char.IsDigit(c)) + { + return false; + } + + do + { + num = num * 10 + (c - '0'); + if (num > max) + { + return false; + } + + if (++namePosition >= name.Length) + { + return false; + } + + c = (char)name[namePosition]; + } + while (char.IsDigit(c)); + + if (num < min) + { + return false; + } + + return true; + } + + private static bool GetSeconds(ReadOnlySpan<byte> name, ref int namePosition, out int seconds) + { + seconds = 0; + + + bool isValid = GetNum(name, ref namePosition, out int num, 0, HoursPerDays * DaysPerWekk - 1); + if (!isValid) + { + return false; + } + + seconds = num * SecondsPerHour; + + if (namePosition >= name.Length) + { + return false; + } + + if (name[namePosition] == ':') + { + namePosition++; + isValid = GetNum(name, ref namePosition, out num, 0, MinutesPerHour - 1); + if (!isValid) + { + return false; + } + + seconds += num * SecondsPerMinute; + + if (namePosition >= name.Length) + { + return false; + } + + if (name[namePosition] == ':') + { + namePosition++; + isValid = GetNum(name, ref namePosition, out num, 0, SecondsPerMinute); + if (!isValid) + { + return false; + } + + seconds += num; + } + } + return true; + } + + private static bool GetOffset(ReadOnlySpan<byte> name, ref int namePosition, ref int offset) + { + bool isNegative = false; + + if (namePosition >= name.Length) + { + return false; + } + + if (name[namePosition] == '-') + { + isNegative = true; + namePosition++; + } + else if (name[namePosition] == '+') + { + namePosition++; + } + + if (namePosition >= name.Length) + { + return false; + } + + bool isValid = GetSeconds(name, ref namePosition, out offset); + if (!isValid) + { + return false; + } + + if (isNegative) + { + offset = -offset; + } + + return true; + } + + private static bool GetRule(ReadOnlySpan<byte> name, ref int namePosition, out Rule rule) + { + rule = new Rule(); + + bool isValid = false; + + if (name[namePosition] == 'J') + { + namePosition++; + + rule.Type = RuleType.JulianDay; + isValid = GetNum(name, ref namePosition, out rule.Day, 1, DaysPerNYear); + } + else if (name[namePosition] == 'M') + { + namePosition++; + + rule.Type = RuleType.MonthNthDayOfWeek; + isValid = GetNum(name, ref namePosition, out rule.Month, 1, MonthsPerYear); + + if (!isValid) + { + return false; + } + + if (name[namePosition++] != '.') + { + return false; + } + + isValid = GetNum(name, ref namePosition, out rule.Week, 1, 5); + if (!isValid) + { + return false; + } + + if (name[namePosition++] != '.') + { + return false; + } + + isValid = GetNum(name, ref namePosition, out rule.Day, 0, DaysPerWekk - 1); + } + else if (char.IsDigit((char)name[namePosition])) + { + rule.Type = RuleType.DayOfYear; + isValid = GetNum(name, ref namePosition, out rule.Day, 0, DaysPerLYear - 1); + } + else + { + return false; + } + + if (!isValid) + { + return false; + } + + if (name[namePosition] == '/') + { + namePosition++; + return GetOffset(name, ref namePosition, ref rule.TransitionTime); + } + else + { + rule.TransitionTime = 2 * SecondsPerHour; + } + + return true; + } + + private static int IsLeap(int year) + { + if (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0)) + { + return 1; + } + + return 0; + } + + private static bool ParsePosixName(ReadOnlySpan<byte> name, ref TimeZoneRule outRules, bool lastDitch) + { + outRules = new TimeZoneRule(); + + int stdLen; + + ReadOnlySpan<byte> stdName = name; + int namePosition = 0; + int stdOffset = 0; + + if (lastDitch) + { + stdLen = 3; + namePosition += stdLen; + } + else + { + if (name[namePosition] == '<') + { + namePosition++; + + stdName = name.Slice(namePosition); + + int stdNamePosition = namePosition; + + namePosition = GetQZName(name, namePosition, '>'); + + if (name[namePosition] != '>') + { + return false; + } + + stdLen = namePosition - stdNamePosition; + namePosition++; + } + else + { + namePosition = GetTZName(name, namePosition); + stdLen = namePosition; + } + + if (stdLen == 0) + { + return false; + } + + bool isValid = GetOffset(name.ToArray(), ref namePosition, ref stdOffset); + + if (!isValid) + { + return false; + } + } + + int charCount = stdLen + 1; + int destLen = 0; + int dstOffset = 0; + + ReadOnlySpan<byte> destName = name.Slice(namePosition); + + if (TzCharsArraySize < charCount) + { + return false; + } + + if (name[namePosition] != '\0') + { + if (name[namePosition] == '<') + { + destName = name.Slice(++namePosition); + int destNamePosition = namePosition; + + namePosition = GetQZName(name.ToArray(), namePosition, '>'); + + if (name[namePosition] != '>') + { + return false; + } + + destLen = namePosition - destNamePosition; + namePosition++; + } + else + { + destName = name.Slice(namePosition); + namePosition = GetTZName(name, namePosition); + destLen = namePosition; + } + + if (destLen == 0) + { + return false; + } + + charCount += destLen + 1; + if (TzCharsArraySize < charCount) + { + return false; + } + + if (name[namePosition] != '\0' && name[namePosition] != ',' && name[namePosition] != ';') + { + bool isValid = GetOffset(name.ToArray(), ref namePosition, ref dstOffset); + + if (!isValid) + { + return false; + } + } + else + { + dstOffset = stdOffset - SecondsPerHour; + } + + if (name[namePosition] == '\0') + { + name = TimeZoneDefaultRule; + namePosition = 0; + } + + if (name[namePosition] == ',' || name[namePosition] == ';') + { + namePosition++; + + bool IsRuleValid = GetRule(name, ref namePosition, out Rule start); + if (!IsRuleValid) + { + return false; + } + + if (name[namePosition++] != ',') + { + return false; + } + + IsRuleValid = GetRule(name, ref namePosition, out Rule end); + if (!IsRuleValid) + { + return false; + } + + if (name[namePosition] != '\0') + { + return false; + } + + outRules.TypeCount = 2; + + outRules.Ttis[0] = new TimeTypeInfo + { + GmtOffset = -dstOffset, + IsDaySavingTime = true, + AbbreviationListIndex = stdLen + 1 + }; + + outRules.Ttis[1] = new TimeTypeInfo + { + GmtOffset = -stdOffset, + IsDaySavingTime = false, + AbbreviationListIndex = 0 + }; + + outRules.DefaultType = 0; + + int timeCount = 0; + long janFirst = 0; + int janOffset = 0; + int yearBegining = EpochYear; + + do + { + int yearSeconds = YearLengths[IsLeap(yearBegining - 1)] * SecondsPerDay; + yearBegining--; + if (IncrementOverflow64(ref janFirst, -yearSeconds)) + { + janOffset = -yearSeconds; + break; + } + } + while (EpochYear - YearsPerRepeat / 2 < yearBegining); + + int yearLimit = yearBegining + YearsPerRepeat + 1; + int year; + for (year = yearBegining; year < yearLimit; year++) + { + int startTime = TransitionTime(year, start, stdOffset); + int endTime = TransitionTime(year, end, dstOffset); + + int yearSeconds = YearLengths[IsLeap(year)] * SecondsPerDay; + + bool isReversed = endTime < startTime; + if (isReversed) + { + int swap = startTime; + + startTime = endTime; + endTime = swap; + } + + if (isReversed || (startTime < endTime && (endTime - startTime < (yearSeconds + (stdOffset - dstOffset))))) + { + if (TzMaxTimes - 2 < timeCount) + { + break; + } + + outRules.Ats[timeCount] = janFirst; + if (!IncrementOverflow64(ref outRules.Ats[timeCount], janOffset + startTime)) + { + outRules.Types[timeCount++] = isReversed ? (byte)1 : (byte)0; + } + else if (janOffset != 0) + { + outRules.DefaultType = isReversed ? 1 : 0; + } + + outRules.Ats[timeCount] = janFirst; + if (!IncrementOverflow64(ref outRules.Ats[timeCount], janOffset + endTime)) + { + outRules.Types[timeCount++] = isReversed ? (byte)0 : (byte)1; + yearLimit = year + YearsPerRepeat + 1; + } + else if (janOffset != 0) + { + outRules.DefaultType = isReversed ? 0 : 1; + } + } + + if (IncrementOverflow64(ref janFirst, janOffset + yearSeconds)) + { + break; + } + + janOffset = 0; + } + + outRules.TimeCount = timeCount; + + // There is no time variation, this is then a perpetual DST rule + if (timeCount == 0) + { + outRules.TypeCount = 1; + } + else if (YearsPerRepeat < year - yearBegining) + { + outRules.GoBack = true; + outRules.GoAhead = true; + } + } + else + { + if (name[namePosition] == '\0') + { + return false; + } + + long theirStdOffset = 0; + for (int i = 0; i < outRules.TimeCount; i++) + { + int j = outRules.Types[i]; + if (outRules.Ttis[j].IsStandardTimeDaylight) + { + theirStdOffset = -outRules.Ttis[j].GmtOffset; + } + } + + long theirDstOffset = 0; + for (int i = 0; i < outRules.TimeCount; i++) + { + int j = outRules.Types[i]; + if (outRules.Ttis[j].IsDaySavingTime) + { + theirDstOffset = -outRules.Ttis[j].GmtOffset; + } + } + + bool isDaySavingTime = false; + long theirOffset = theirStdOffset; + for (int i = 0; i < outRules.TimeCount; i++) + { + int j = outRules.Types[i]; + outRules.Types[i] = outRules.Ttis[j].IsDaySavingTime ? (byte)1 : (byte)0; + if (!outRules.Ttis[j].IsGMT) + { + if (isDaySavingTime && !outRules.Ttis[j].IsStandardTimeDaylight) + { + outRules.Ats[i] += dstOffset - theirStdOffset; + } + else + { + outRules.Ats[i] += stdOffset - theirStdOffset; + } + } + + theirOffset = -outRules.Ttis[j].GmtOffset; + if (outRules.Ttis[j].IsDaySavingTime) + { + theirDstOffset = theirOffset; + } + else + { + theirStdOffset = theirOffset; + } + } + + outRules.Ttis[0] = new TimeTypeInfo + { + GmtOffset = -stdOffset, + IsDaySavingTime = false, + AbbreviationListIndex = 0 + }; + + outRules.Ttis[1] = new TimeTypeInfo + { + GmtOffset = -dstOffset, + IsDaySavingTime = true, + AbbreviationListIndex = stdLen + 1 + }; + + outRules.TypeCount = 2; + outRules.DefaultType = 0; + } + } + else + { + // default is perpetual standard time + outRules.TypeCount = 1; + outRules.TimeCount = 0; + outRules.DefaultType = 0; + outRules.Ttis[0] = new TimeTypeInfo + { + GmtOffset = -stdOffset, + IsDaySavingTime = false, + AbbreviationListIndex = 0 + }; + } + + outRules.CharCount = charCount; + + int charsPosition = 0; + + for (int i = 0; i < stdLen; i++) + { + outRules.Chars[i] = stdName[i]; + } + + charsPosition += stdLen; + outRules.Chars[charsPosition++] = 0; + + if (destLen != 0) + { + for (int i = 0; i < destLen; i++) + { + outRules.Chars[charsPosition + i] = destName[i]; + } + outRules.Chars[charsPosition + destLen] = 0; + } + + return true; + } + + private static int TransitionTime(int year, Rule rule, int offset) + { + int leapYear = IsLeap(year); + + int value; + switch (rule.Type) + { + case RuleType.JulianDay: + value = (rule.Day - 1) * SecondsPerDay; + if (leapYear == 1 && rule.Day >= 60) + { + value += SecondsPerDay; + } + break; + + case RuleType.DayOfYear: + value = rule.Day * SecondsPerDay; + break; + + case RuleType.MonthNthDayOfWeek: + // Here we use Zeller's Congruence to get the day of week of the first month. + + int m1 = (rule.Month + 9) % 12 + 1; + int yy0 = (rule.Month <= 2) ? (year - 1) : year; + int yy1 = yy0 / 100; + int yy2 = yy0 % 100; + + int dayOfWeek = ((26 * m1 - 2) / 10 + 1 + yy2 + yy2 / 4 + yy1 / 4 - 2 * yy1) % 7; + + if (dayOfWeek < 0) + { + dayOfWeek += DaysPerWekk; + } + + // Get the zero origin + int d = rule.Day - dayOfWeek; + + if (d < 0) + { + d += DaysPerWekk; + } + + for (int i = 1; i < rule.Week; i++) + { + if (d + DaysPerWekk >= MonthsLengths[leapYear][rule.Month - 1]) + { + break; + } + + d += DaysPerWekk; + } + + value = d * SecondsPerDay; + for (int i = 0; i < rule.Month - 1; i++) + { + value += MonthsLengths[leapYear][i] * SecondsPerDay; + } + + break; + default: + throw new NotImplementedException("Unknown time transition!"); + } + + return value + rule.TransitionTime + offset; + } + + private static bool NormalizeOverflow32(ref int ip, ref int unit, int baseValue) + { + int delta; + + if (unit >= 0) + { + delta = unit / baseValue; + } + else + { + delta = -1 - (-1 - unit) / baseValue; + } + + unit -= delta * baseValue; + + return IncrementOverflow32(ref ip, delta); + } + + private static bool NormalizeOverflow64(ref long ip, ref long unit, long baseValue) + { + long delta; + + if (unit >= 0) + { + delta = unit / baseValue; + } + else + { + delta = -1 - (-1 - unit) / baseValue; + } + + unit -= delta * baseValue; + + return IncrementOverflow64(ref ip, delta); + } + + private static bool IncrementOverflow32(ref int time, int j) + { + try + { + time = checked(time + j); + + return false; + } + catch (OverflowException) + { + return true; + } + } + + private static bool IncrementOverflow64(ref long time, long j) + { + try + { + time = checked(time + j); + + return false; + } + catch (OverflowException) + { + return true; + } + } + + internal static bool ParsePosixName(string name, ref TimeZoneRule outRules) + { + return ParsePosixName(Encoding.ASCII.GetBytes(name), ref outRules, false); + } + + internal static bool ParseTimeZoneBinary(ref TimeZoneRule outRules, Stream inputData) + { + outRules = new TimeZoneRule(); + + BinaryReader reader = new BinaryReader(inputData); + + long streamLength = reader.BaseStream.Length; + + if (streamLength < Unsafe.SizeOf<TzifHeader>()) + { + return false; + } + + TzifHeader header = reader.ReadStruct<TzifHeader>(); + + streamLength -= Unsafe.SizeOf<TzifHeader>(); + + int ttisGMTCount = Detzcode32(header.TtisGMTCount); + int ttisSTDCount = Detzcode32(header.TtisSTDCount); + int leapCount = Detzcode32(header.LeapCount); + int timeCount = Detzcode32(header.TimeCount); + int typeCount = Detzcode32(header.TypeCount); + int charCount = Detzcode32(header.CharCount); + + if (!(0 <= leapCount + && leapCount < TzMaxLeaps + && 0 < typeCount + && typeCount < TzMaxTypes + && 0 <= timeCount + && timeCount < TzMaxTimes + && 0 <= charCount + && charCount < TzMaxChars + && (ttisSTDCount == typeCount || ttisSTDCount == 0) + && (ttisGMTCount == typeCount || ttisGMTCount == 0))) + { + return false; + } + + + if (streamLength < (timeCount * TimeTypeSize + + timeCount + + typeCount * 6 + + charCount + + leapCount * (TimeTypeSize + 4) + + ttisSTDCount + + ttisGMTCount)) + { + return false; + } + + outRules.TimeCount = timeCount; + outRules.TypeCount = typeCount; + outRules.CharCount = charCount; + + byte[] workBuffer = StreamUtils.StreamToBytes(inputData); + + timeCount = 0; + + { + Span<byte> p = workBuffer; + for (int i = 0; i < outRules.TimeCount; i++) + { + long at = Detzcode64(p); + outRules.Types[i] = 1; + + if (timeCount != 0 && at <= outRules.Ats[timeCount - 1]) + { + if (at < outRules.Ats[timeCount - 1]) + { + return false; + } + + outRules.Types[i - 1] = 0; + timeCount--; + } + + outRules.Ats[timeCount++] = at; + + p = p[TimeTypeSize..]; + } + + timeCount = 0; + for (int i = 0; i < outRules.TimeCount; i++) + { + byte type = p[0]; + p = p[1..]; + + if (outRules.TypeCount <= type) + { + return false; + } + + if (outRules.Types[i] != 0) + { + outRules.Types[timeCount++] = type; + } + } + + outRules.TimeCount = timeCount; + + for (int i = 0; i < outRules.TypeCount; i++) + { + TimeTypeInfo ttis = outRules.Ttis[i]; + ttis.GmtOffset = Detzcode32(p); + p = p[sizeof(int)..]; + + if (p[0] >= 2) + { + return false; + } + + ttis.IsDaySavingTime = p[0] != 0; + p = p[1..]; + + int abbreviationListIndex = p[0]; + p = p[1..]; + + if (abbreviationListIndex >= outRules.CharCount) + { + return false; + } + + ttis.AbbreviationListIndex = abbreviationListIndex; + + outRules.Ttis[i] = ttis; + } + + p[..outRules.CharCount].CopyTo(outRules.Chars); + + p = p[outRules.CharCount..]; + outRules.Chars[outRules.CharCount] = 0; + + for (int i = 0; i < outRules.TypeCount; i++) + { + if (ttisSTDCount == 0) + { + outRules.Ttis[i].IsStandardTimeDaylight = false; + } + else + { + if (p[0] >= 2) + { + return false; + } + + outRules.Ttis[i].IsStandardTimeDaylight = p[0] != 0; + p = p[1..]; + } + } + + for (int i = 0; i < outRules.TypeCount; i++) + { + if (ttisSTDCount == 0) + { + outRules.Ttis[i].IsGMT = false; + } + else + { + if (p[0] >= 2) + { + return false; + } + + outRules.Ttis[i].IsGMT = p[0] != 0; + p = p[1..]; + } + + } + + long position = (workBuffer.Length - p.Length); + long nRead = streamLength - position; + + if (nRead < 0) + { + return false; + } + + // Nintendo abort in case of a TzIf file with a POSIX TZ Name too long to fit inside a TimeZoneRule. + // As it's impossible in normal usage to achive this, we also force a crash. + if (nRead > (TzNameMax + 1)) + { + throw new InvalidOperationException(); + } + + byte[] tempName = new byte[TzNameMax + 1]; + Array.Copy(workBuffer, position, tempName, 0, nRead); + + if (nRead > 2 && tempName[0] == '\n' && tempName[nRead - 1] == '\n' && outRules.TypeCount + 2 <= TzMaxTypes) + { + tempName[nRead - 1] = 0; + + byte[] name = new byte[TzNameMax]; + Array.Copy(tempName, 1, name, 0, nRead - 1); + + Box<TimeZoneRule> tempRulesBox = new Box<TimeZoneRule>(); + ref TimeZoneRule tempRules = ref tempRulesBox.Data; + + if (ParsePosixName(name, ref tempRulesBox.Data, false)) + { + int abbreviationCount = 0; + charCount = outRules.CharCount; + + Span<byte> chars = outRules.Chars; + + for (int i = 0; i < tempRules.TypeCount; i++) + { + ReadOnlySpan<byte> tempChars = tempRules.Chars; + ReadOnlySpan<byte> tempAbbreviation = tempChars[tempRules.Ttis[i].AbbreviationListIndex..]; + + int j; + + for (j = 0; j < charCount; j++) + { + if (StringUtils.CompareCStr(chars[j..], tempAbbreviation) == 0) + { + tempRules.Ttis[i].AbbreviationListIndex = j; + abbreviationCount++; + break; + } + } + + if (j >= charCount) + { + int abbreviationLength = StringUtils.LengthCstr(tempAbbreviation); + if (j + abbreviationLength < TzMaxChars) + { + for (int x = 0; x < abbreviationLength; x++) + { + chars[j + x] = tempAbbreviation[x]; + } + + charCount = j + abbreviationLength + 1; + + tempRules.Ttis[i].AbbreviationListIndex = j; + abbreviationCount++; + } + } + } + + if (abbreviationCount == tempRules.TypeCount) + { + outRules.CharCount = charCount; + + // Remove trailing + while (1 < outRules.TimeCount && (outRules.Types[outRules.TimeCount - 1] == outRules.Types[outRules.TimeCount - 2])) + { + outRules.TimeCount--; + } + + int i; + + for (i = 0; i < tempRules.TimeCount; i++) + { + if (outRules.TimeCount == 0 || outRules.Ats[outRules.TimeCount - 1] < tempRules.Ats[i]) + { + break; + } + } + + while (i < tempRules.TimeCount && outRules.TimeCount < TzMaxTimes) + { + outRules.Ats[outRules.TimeCount] = tempRules.Ats[i]; + outRules.Types[outRules.TimeCount] = (byte)(outRules.TypeCount + (byte)tempRules.Types[i]); + + outRules.TimeCount++; + i++; + } + + for (i = 0; i < tempRules.TypeCount; i++) + { + outRules.Ttis[outRules.TypeCount++] = tempRules.Ttis[i]; + } + } + } + } + + if (outRules.TypeCount == 0) + { + return false; + } + + if (outRules.TimeCount > 1) + { + for (int i = 1; i < outRules.TimeCount; i++) + { + if (TimeTypeEquals(in outRules, outRules.Types[i], outRules.Types[0]) && DifferByRepeat(outRules.Ats[i], outRules.Ats[0])) + { + outRules.GoBack = true; + break; + } + } + + for (int i = outRules.TimeCount - 2; i >= 0; i--) + { + if (TimeTypeEquals(in outRules, outRules.Types[outRules.TimeCount - 1], outRules.Types[i]) && DifferByRepeat(outRules.Ats[outRules.TimeCount - 1], outRules.Ats[i])) + { + outRules.GoAhead = true; + break; + } + } + } + + int defaultType; + + for (defaultType = 0; defaultType < outRules.TimeCount; defaultType++) + { + if (outRules.Types[defaultType] == 0) + { + break; + } + } + + defaultType = defaultType < outRules.TimeCount ? -1 : 0; + + if (defaultType < 0 && outRules.TimeCount > 0 && outRules.Ttis[outRules.Types[0]].IsDaySavingTime) + { + defaultType = outRules.Types[0]; + while (--defaultType >= 0) + { + if (!outRules.Ttis[defaultType].IsDaySavingTime) + { + break; + } + } + } + + if (defaultType < 0) + { + defaultType = 0; + while (outRules.Ttis[defaultType].IsDaySavingTime) + { + if (++defaultType >= outRules.TypeCount) + { + defaultType = 0; + break; + } + } + } + + outRules.DefaultType = defaultType; + } + + return true; + } + + private static long GetLeapDaysNotNeg(long year) + { + return year / 4 - year / 100 + year / 400; + } + + private static long GetLeapDays(long year) + { + if (year < 0) + { + return -1 - GetLeapDaysNotNeg(-1 - year); + } + else + { + return GetLeapDaysNotNeg(year); + } + } + + private static ResultCode CreateCalendarTime(long time, int gmtOffset, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo) + { + long year = EpochYear; + long timeDays = time / SecondsPerDay; + long remainingSeconds = time % SecondsPerDay; + + calendarTime = new CalendarTimeInternal(); + calendarAdditionalInfo = new CalendarAdditionalInfo(); + + while (timeDays < 0 || timeDays >= YearLengths[IsLeap((int)year)]) + { + long timeDelta = timeDays / DaysPerLYear; + long delta = timeDelta; + + if (delta == 0) + { + delta = timeDays < 0 ? -1 : 1; + } + + long newYear = year; + + if (IncrementOverflow64(ref newYear, delta)) + { + return ResultCode.OutOfRange; + } + + long leapDays = GetLeapDays(newYear - 1) - GetLeapDays(year - 1); + timeDays -= (newYear - year) * DaysPerNYear; + timeDays -= leapDays; + year = newYear; + } + + long dayOfYear = timeDays; + remainingSeconds += gmtOffset; + while (remainingSeconds < 0) + { + remainingSeconds += SecondsPerDay; + dayOfYear -= 1; + } + + while (remainingSeconds >= SecondsPerDay) + { + remainingSeconds -= SecondsPerDay; + dayOfYear += 1; + } + + while (dayOfYear < 0) + { + if (IncrementOverflow64(ref year, -1)) + { + return ResultCode.OutOfRange; + } + + dayOfYear += YearLengths[IsLeap((int)year)]; + } + + while (dayOfYear >= YearLengths[IsLeap((int)year)]) + { + dayOfYear -= YearLengths[IsLeap((int)year)]; + + if (IncrementOverflow64(ref year, 1)) + { + return ResultCode.OutOfRange; + } + } + + calendarTime.Year = year; + calendarAdditionalInfo.DayOfYear = (uint)dayOfYear; + + long dayOfWeek = (EpochWeekDay + ((year - EpochYear) % DaysPerWekk) * (DaysPerNYear % DaysPerWekk) + GetLeapDays(year - 1) - GetLeapDays(EpochYear - 1) + dayOfYear) % DaysPerWekk; + if (dayOfWeek < 0) + { + dayOfWeek += DaysPerWekk; + } + + calendarAdditionalInfo.DayOfWeek = (uint)dayOfWeek; + + calendarTime.Hour = (sbyte)((remainingSeconds / SecondsPerHour) % SecondsPerHour); + remainingSeconds %= SecondsPerHour; + + calendarTime.Minute = (sbyte)(remainingSeconds / SecondsPerMinute); + calendarTime.Second = (sbyte)(remainingSeconds % SecondsPerMinute); + + int[] ip = MonthsLengths[IsLeap((int)year)]; + + for (calendarTime.Month = 0; dayOfYear >= ip[calendarTime.Month]; ++calendarTime.Month) + { + dayOfYear -= ip[calendarTime.Month]; + } + + calendarTime.Day = (sbyte)(dayOfYear + 1); + + calendarAdditionalInfo.IsDaySavingTime = false; + calendarAdditionalInfo.GmtOffset = gmtOffset; + + return 0; + } + + private static ResultCode ToCalendarTimeInternal(in TimeZoneRule rules, long time, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo) + { + calendarTime = new CalendarTimeInternal(); + calendarAdditionalInfo = new CalendarAdditionalInfo(); + + ResultCode result; + + if ((rules.GoAhead && time < rules.Ats[0]) || (rules.GoBack && time > rules.Ats[rules.TimeCount - 1])) + { + long newTime = time; + + long seconds; + long years; + + if (time < rules.Ats[0]) + { + seconds = rules.Ats[0] - time; + } + else + { + seconds = time - rules.Ats[rules.TimeCount - 1]; + } + + seconds -= 1; + + years = (seconds / SecondsPerRepeat + 1) * YearsPerRepeat; + seconds = years * AverageSecondsPerYear; + + if (time < rules.Ats[0]) + { + newTime += seconds; + } + else + { + newTime -= seconds; + } + + if (newTime < rules.Ats[0] && newTime > rules.Ats[rules.TimeCount - 1]) + { + return ResultCode.TimeNotFound; + } + + result = ToCalendarTimeInternal(in rules, newTime, out calendarTime, out calendarAdditionalInfo); + if (result != 0) + { + return result; + } + + if (time < rules.Ats[0]) + { + calendarTime.Year -= years; + } + else + { + calendarTime.Year += years; + } + + return ResultCode.Success; + } + + int ttiIndex; + + if (rules.TimeCount == 0 || time < rules.Ats[0]) + { + ttiIndex = rules.DefaultType; + } + else + { + int low = 1; + int high = rules.TimeCount; + + while (low < high) + { + int mid = (low + high) >> 1; + + if (time < rules.Ats[mid]) + { + high = mid; + } + else + { + low = mid + 1; + } + } + + ttiIndex = rules.Types[low - 1]; + } + + result = CreateCalendarTime(time, rules.Ttis[ttiIndex].GmtOffset, out calendarTime, out calendarAdditionalInfo); + + if (result == 0) + { + calendarAdditionalInfo.IsDaySavingTime = rules.Ttis[ttiIndex].IsDaySavingTime; + + ReadOnlySpan<byte> timeZoneAbbreviation = rules.Chars[rules.Ttis[ttiIndex].AbbreviationListIndex..]; + + int timeZoneSize = Math.Min(StringUtils.LengthCstr(timeZoneAbbreviation), 8); + + timeZoneAbbreviation[..timeZoneSize].CopyTo(calendarAdditionalInfo.TimezoneName.AsSpan()); + } + + return result; + } + + private static ResultCode ToPosixTimeInternal(in TimeZoneRule rules, CalendarTimeInternal calendarTime, out long posixTime) + { + posixTime = 0; + + int hour = calendarTime.Hour; + int minute = calendarTime.Minute; + + if (NormalizeOverflow32(ref hour, ref minute, MinutesPerHour)) + { + return ResultCode.Overflow; + } + + calendarTime.Minute = (sbyte)minute; + + int day = calendarTime.Day; + if (NormalizeOverflow32(ref day, ref hour, HoursPerDays)) + { + return ResultCode.Overflow; + } + + calendarTime.Day = (sbyte)day; + calendarTime.Hour = (sbyte)hour; + + long year = calendarTime.Year; + long month = calendarTime.Month; + + if (NormalizeOverflow64(ref year, ref month, MonthsPerYear)) + { + return ResultCode.Overflow; + } + + calendarTime.Month = (sbyte)month; + + if (IncrementOverflow64(ref year, YearBase)) + { + return ResultCode.Overflow; + } + + while (day <= 0) + { + if (IncrementOverflow64(ref year, -1)) + { + return ResultCode.Overflow; + } + + long li = year; + + if (1 < calendarTime.Month) + { + li++; + } + + day += YearLengths[IsLeap((int)li)]; + } + + while (day > DaysPerLYear) + { + long li = year; + + if (1 < calendarTime.Month) + { + li++; + } + + day -= YearLengths[IsLeap((int)li)]; + + if (IncrementOverflow64(ref year, 1)) + { + return ResultCode.Overflow; + } + } + + while (true) + { + int i = MonthsLengths[IsLeap((int)year)][calendarTime.Month]; + + if (day <= i) + { + break; + } + + day -= i; + calendarTime.Month += 1; + + if (calendarTime.Month >= MonthsPerYear) + { + calendarTime.Month = 0; + if (IncrementOverflow64(ref year, 1)) + { + return ResultCode.Overflow; + } + } + } + + calendarTime.Day = (sbyte)day; + + if (IncrementOverflow64(ref year, -YearBase)) + { + return ResultCode.Overflow; + } + + calendarTime.Year = year; + + int savedSeconds; + + if (calendarTime.Second >= 0 && calendarTime.Second < SecondsPerMinute) + { + savedSeconds = 0; + } + else if (year + YearBase < EpochYear) + { + int second = calendarTime.Second; + if (IncrementOverflow32(ref second, 1 - SecondsPerMinute)) + { + return ResultCode.Overflow; + } + + savedSeconds = second; + calendarTime.Second = 1 - SecondsPerMinute; + } + else + { + savedSeconds = calendarTime.Second; + calendarTime.Second = 0; + } + + long low = long.MinValue; + long high = long.MaxValue; + + while (true) + { + long pivot = low / 2 + high / 2; + + if (pivot < low) + { + pivot = low; + } + else if (pivot > high) + { + pivot = high; + } + + int direction; + + ResultCode result = ToCalendarTimeInternal(in rules, pivot, out CalendarTimeInternal candidateCalendarTime, out _); + if (result != 0) + { + if (pivot > 0) + { + direction = 1; + } + else + { + direction = -1; + } + } + else + { + direction = candidateCalendarTime.CompareTo(calendarTime); + } + + if (direction == 0) + { + long timeResult = pivot + savedSeconds; + + if ((timeResult < pivot) != (savedSeconds < 0)) + { + return ResultCode.Overflow; + } + + posixTime = timeResult; + break; + } + else + { + if (pivot == low) + { + if (pivot == long.MaxValue) + { + return ResultCode.TimeNotFound; + } + + pivot += 1; + low += 1; + } + else if (pivot == high) + { + if (pivot == long.MinValue) + { + return ResultCode.TimeNotFound; + } + + pivot -= 1; + high -= 1; + } + + if (low > high) + { + return ResultCode.TimeNotFound; + } + + if (direction > 0) + { + high = pivot; + } + else + { + low = pivot; + } + } + } + + return ResultCode.Success; + } + + internal static ResultCode ToCalendarTime(in TimeZoneRule rules, long time, out CalendarInfo calendar) + { + ResultCode result = ToCalendarTimeInternal(in rules, time, out CalendarTimeInternal calendarTime, out CalendarAdditionalInfo calendarAdditionalInfo); + + calendar = new CalendarInfo() + { + Time = new CalendarTime() + { + Year = (short)calendarTime.Year, + // NOTE: Nintendo's month range is 1-12, internal range is 0-11. + Month = (sbyte)(calendarTime.Month + 1), + Day = calendarTime.Day, + Hour = calendarTime.Hour, + Minute = calendarTime.Minute, + Second = calendarTime.Second + }, + AdditionalInfo = calendarAdditionalInfo + }; + + return result; + } + + internal static ResultCode ToPosixTime(in TimeZoneRule rules, CalendarTime calendarTime, out long posixTime) + { + CalendarTimeInternal calendarTimeInternal = new CalendarTimeInternal() + { + Year = calendarTime.Year, + // NOTE: Nintendo's month range is 1-12, internal range is 0-11. + Month = (sbyte)(calendarTime.Month - 1), + Day = calendarTime.Day, + Hour = calendarTime.Hour, + Minute = calendarTime.Minute, + Second = calendarTime.Second + }; + + return ToPosixTimeInternal(in rules, calendarTimeInternal, out posixTime); + } + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs new file mode 100644 index 00000000..9367024e --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneContentManager.cs @@ -0,0 +1,304 @@ +using LibHac; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Logging; +using Ryujinx.Cpu; +using Ryujinx.HLE.Exceptions; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using Ryujinx.HLE.Utilities; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using TimeZoneRuleBox = Ryujinx.Common.Memory.Box<Ryujinx.HLE.HOS.Services.Time.TimeZone.TimeZoneRule>; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + public class TimeZoneContentManager + { + private const long TimeZoneBinaryTitleId = 0x010000000000080E; + + private readonly string TimeZoneSystemTitleMissingErrorMessage = "TimeZoneBinary system title not found! TimeZone conversions will not work, provide the system archive to fix this error. (See https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide#initial-setup-continued---installation-of-firmware for more information)"; + + private VirtualFileSystem _virtualFileSystem; + private IntegrityCheckLevel _fsIntegrityCheckLevel; + private ContentManager _contentManager; + + public string[] LocationNameCache { get; private set; } + + internal TimeZoneManager Manager { get; private set; } + + public TimeZoneContentManager() + { + Manager = new TimeZoneManager(); + } + + public void InitializeInstance(VirtualFileSystem virtualFileSystem, ContentManager contentManager, IntegrityCheckLevel fsIntegrityCheckLevel) + { + _virtualFileSystem = virtualFileSystem; + _contentManager = contentManager; + _fsIntegrityCheckLevel = fsIntegrityCheckLevel; + + InitializeLocationNameCache(); + } + + public string SanityCheckDeviceLocationName(string locationName) + { + if (IsLocationNameValid(locationName)) + { + return locationName; + } + + Logger.Warning?.Print(LogClass.ServiceTime, $"Invalid device TimeZone {locationName}, switching back to UTC"); + + return "UTC"; + } + + internal void Initialize(TimeManager timeManager, Switch device) + { + InitializeInstance(device.FileSystem, device.System.ContentManager, device.System.FsIntegrityCheckLevel); + + ITickSource tickSource = device.System.TickSource; + + SteadyClockTimePoint timeZoneUpdatedTimePoint = timeManager.StandardSteadyClock.GetCurrentTimePoint(tickSource); + + string deviceLocationName = SanityCheckDeviceLocationName(device.Configuration.TimeZone); + + ResultCode result = GetTimeZoneBinary(deviceLocationName, out Stream timeZoneBinaryStream, out LocalStorage ncaFile); + + if (result == ResultCode.Success) + { + // TODO: Read TimeZoneVersion from sysarchive. + timeManager.SetupTimeZoneManager(deviceLocationName, timeZoneUpdatedTimePoint, (uint)LocationNameCache.Length, new UInt128(), timeZoneBinaryStream); + + ncaFile.Dispose(); + } + else + { + // In the case the user don't have the timezone system archive, we just mark the manager as initialized. + Manager.MarkInitialized(); + } + } + + private void InitializeLocationNameCache() + { + if (HasTimeZoneBinaryTitle()) + { + using (IStorage ncaFileStream = new LocalStorage(_virtualFileSystem.SwitchPathToSystemPath(GetTimeZoneBinaryTitleContentPath()), FileAccess.Read, FileMode.Open)) + { + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFileStream); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, _fsIntegrityCheckLevel); + + using var binaryListFile = new UniqueRef<IFile>(); + + romfs.OpenFile(ref binaryListFile.Ref, "/binaryList.txt".ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + StreamReader reader = new StreamReader(binaryListFile.Get.AsStream()); + + List<string> locationNameList = new List<string>(); + + string locationName; + while ((locationName = reader.ReadLine()) != null) + { + locationNameList.Add(locationName); + } + + LocationNameCache = locationNameList.ToArray(); + } + } + else + { + LocationNameCache = new string[] { "UTC" }; + + Logger.Error?.Print(LogClass.ServiceTime, TimeZoneSystemTitleMissingErrorMessage); + } + } + + public IEnumerable<(int Offset, string Location, string Abbr)> ParseTzOffsets() + { + var tzBinaryContentPath = GetTimeZoneBinaryTitleContentPath(); + + if (string.IsNullOrEmpty(tzBinaryContentPath)) + { + return new[] { (0, "UTC", "UTC") }; + } + + List<(int Offset, string Location, string Abbr)> outList = new List<(int Offset, string Location, string Abbr)>(); + var now = DateTimeOffset.Now.ToUnixTimeSeconds(); + using (IStorage ncaStorage = new LocalStorage(_virtualFileSystem.SwitchPathToSystemPath(tzBinaryContentPath), FileAccess.Read, FileMode.Open)) + using (IFileSystem romfs = new Nca(_virtualFileSystem.KeySet, ncaStorage).OpenFileSystem(NcaSectionType.Data, _fsIntegrityCheckLevel)) + { + foreach (string locName in LocationNameCache) + { + if (locName.StartsWith("Etc")) + { + continue; + } + + using var tzif = new UniqueRef<IFile>(); + + if (romfs.OpenFile(ref tzif.Ref, $"/zoneinfo/{locName}".ToU8Span(), OpenMode.Read).IsFailure()) + { + Logger.Error?.Print(LogClass.ServiceTime, $"Error opening /zoneinfo/{locName}"); + continue; + } + + TimeZoneRuleBox tzRuleBox = new TimeZoneRuleBox(); + ref TimeZoneRule tzRule = ref tzRuleBox.Data; + + TimeZone.ParseTimeZoneBinary(ref tzRule, tzif.Get.AsStream()); + + + TimeTypeInfo ttInfo; + if (tzRule.TimeCount > 0) // Find the current transition period + { + int fin = 0; + for (int i = 0; i < tzRule.TimeCount; ++i) + { + if (tzRule.Ats[i] <= now) + { + fin = i; + } + } + ttInfo = tzRule.Ttis[tzRule.Types[fin]]; + } + else if (tzRule.TypeCount >= 1) // Otherwise, use the first offset in TTInfo + { + ttInfo = tzRule.Ttis[0]; + } + else + { + Logger.Error?.Print(LogClass.ServiceTime, $"Couldn't find UTC offset for zone {locName}"); + continue; + } + + var abbrStart = tzRule.Chars[ttInfo.AbbreviationListIndex..]; + int abbrEnd = abbrStart.IndexOf((byte)0); + + outList.Add((ttInfo.GmtOffset, locName, Encoding.UTF8.GetString(abbrStart[..abbrEnd]))); + } + } + + outList.Sort(); + + return outList; + } + + private bool IsLocationNameValid(string locationName) + { + foreach (string cachedLocationName in LocationNameCache) + { + if (cachedLocationName.Equals(locationName)) + { + return true; + } + } + + return false; + } + + public ResultCode SetDeviceLocationName(string locationName) + { + ResultCode result = GetTimeZoneBinary(locationName, out Stream timeZoneBinaryStream, out LocalStorage ncaFile); + + if (result == ResultCode.Success) + { + result = Manager.SetDeviceLocationNameWithTimeZoneRule(locationName, timeZoneBinaryStream); + + ncaFile.Dispose(); + } + + return result; + } + + public ResultCode LoadLocationNameList(uint index, out string[] outLocationNameArray, uint maxLength) + { + List<string> locationNameList = new List<string>(); + + for (int i = 0; i < LocationNameCache.Length && i < maxLength; i++) + { + if (i < index) + { + continue; + } + + string locationName = LocationNameCache[i]; + + // If the location name is too long, error out. + if (locationName.Length > 0x24) + { + outLocationNameArray = Array.Empty<string>(); + + return ResultCode.LocationNameTooLong; + } + + locationNameList.Add(locationName); + } + + outLocationNameArray = locationNameList.ToArray(); + + return ResultCode.Success; + } + + public string GetTimeZoneBinaryTitleContentPath() + { + return _contentManager.GetInstalledContentPath(TimeZoneBinaryTitleId, StorageId.BuiltInSystem, NcaContentType.Data); + } + + public bool HasTimeZoneBinaryTitle() + { + return !string.IsNullOrEmpty(GetTimeZoneBinaryTitleContentPath()); + } + + internal ResultCode GetTimeZoneBinary(string locationName, out Stream timeZoneBinaryStream, out LocalStorage ncaFile) + { + timeZoneBinaryStream = null; + ncaFile = null; + + if (!HasTimeZoneBinaryTitle() || !IsLocationNameValid(locationName)) + { + return ResultCode.TimeZoneNotFound; + } + + ncaFile = new LocalStorage(_virtualFileSystem.SwitchPathToSystemPath(GetTimeZoneBinaryTitleContentPath()), FileAccess.Read, FileMode.Open); + + Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile); + IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, _fsIntegrityCheckLevel); + + using var timeZoneBinaryFile = new UniqueRef<IFile>(); + + Result result = romfs.OpenFile(ref timeZoneBinaryFile.Ref, $"/zoneinfo/{locationName}".ToU8Span(), OpenMode.Read); + + timeZoneBinaryStream = timeZoneBinaryFile.Release().AsStream(); + + return (ResultCode)result.Value; + } + + internal ResultCode LoadTimeZoneRule(ref TimeZoneRule rules, string locationName) + { + rules = default; + + if (!HasTimeZoneBinaryTitle()) + { + throw new InvalidSystemResourceException(TimeZoneSystemTitleMissingErrorMessage); + } + + ResultCode result = GetTimeZoneBinary(locationName, out Stream timeZoneBinaryStream, out LocalStorage ncaFile); + + if (result == ResultCode.Success) + { + result = Manager.ParseTimeZoneRuleBinary(ref rules, timeZoneBinaryStream); + + ncaFile.Dispose(); + } + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs new file mode 100644 index 00000000..ef4b7b39 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/TimeZoneManager.cs @@ -0,0 +1,261 @@ +using Ryujinx.Common.Memory; +using Ryujinx.HLE.HOS.Services.Time.Clock; +using System; +using System.IO; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + class TimeZoneManager + { + private bool _isInitialized; + private Box<TimeZoneRule> _myRules; + private string _deviceLocationName; + private UInt128 _timeZoneRuleVersion; + private uint _totalLocationNameCount; + private SteadyClockTimePoint _timeZoneUpdateTimePoint; + private object _lock; + + public TimeZoneManager() + { + _isInitialized = false; + _deviceLocationName = "UTC"; + _timeZoneRuleVersion = new UInt128(); + _lock = new object(); + _myRules = new Box<TimeZoneRule>(); + + _timeZoneUpdateTimePoint = SteadyClockTimePoint.GetRandom(); + } + + public bool IsInitialized() + { + bool res; + + lock (_lock) + { + res = _isInitialized; + } + + return res; + } + + public void MarkInitialized() + { + lock (_lock) + { + _isInitialized = true; + } + } + + public ResultCode GetDeviceLocationName(out string deviceLocationName) + { + ResultCode result = ResultCode.UninitializedClock; + + deviceLocationName = null; + + lock (_lock) + { + if (_isInitialized) + { + deviceLocationName = _deviceLocationName; + result = ResultCode.Success; + } + } + + return result; + } + + public ResultCode SetDeviceLocationNameWithTimeZoneRule(string locationName, Stream timeZoneBinaryStream) + { + ResultCode result = ResultCode.TimeZoneConversionFailed; + + lock (_lock) + { + Box<TimeZoneRule> rules = new Box<TimeZoneRule>(); + + bool timeZoneConversionSuccess = TimeZone.ParseTimeZoneBinary(ref rules.Data, timeZoneBinaryStream); + + if (timeZoneConversionSuccess) + { + _deviceLocationName = locationName; + _myRules = rules; + result = ResultCode.Success; + } + } + + return result; + } + + public void SetTotalLocationNameCount(uint totalLocationNameCount) + { + lock (_lock) + { + _totalLocationNameCount = totalLocationNameCount; + } + } + + public ResultCode GetTotalLocationNameCount(out uint totalLocationNameCount) + { + ResultCode result = ResultCode.UninitializedClock; + + totalLocationNameCount = 0; + + lock (_lock) + { + if (_isInitialized) + { + totalLocationNameCount = _totalLocationNameCount; + result = ResultCode.Success; + } + } + + return result; + } + + public ResultCode SetUpdatedTime(SteadyClockTimePoint timeZoneUpdatedTimePoint, bool bypassUninitialized = false) + { + ResultCode result = ResultCode.UninitializedClock; + + lock (_lock) + { + if (_isInitialized || bypassUninitialized) + { + _timeZoneUpdateTimePoint = timeZoneUpdatedTimePoint; + result = ResultCode.Success; + } + } + + return result; + } + + public ResultCode GetUpdatedTime(out SteadyClockTimePoint timeZoneUpdatedTimePoint) + { + ResultCode result; + + lock (_lock) + { + if (_isInitialized) + { + timeZoneUpdatedTimePoint = _timeZoneUpdateTimePoint; + result = ResultCode.Success; + } + else + { + timeZoneUpdatedTimePoint = SteadyClockTimePoint.GetRandom(); + result = ResultCode.UninitializedClock; + } + } + + return result; + } + + public ResultCode ParseTimeZoneRuleBinary(ref TimeZoneRule outRules, Stream timeZoneBinaryStream) + { + ResultCode result = ResultCode.Success; + + lock (_lock) + { + bool timeZoneConversionSuccess = TimeZone.ParseTimeZoneBinary(ref outRules, timeZoneBinaryStream); + + if (!timeZoneConversionSuccess) + { + result = ResultCode.TimeZoneConversionFailed; + } + } + + return result; + } + + public void SetTimeZoneRuleVersion(UInt128 timeZoneRuleVersion) + { + lock (_lock) + { + _timeZoneRuleVersion = timeZoneRuleVersion; + } + } + + public ResultCode GetTimeZoneRuleVersion(out UInt128 timeZoneRuleVersion) + { + ResultCode result; + + lock (_lock) + { + if (_isInitialized) + { + timeZoneRuleVersion = _timeZoneRuleVersion; + result = ResultCode.Success; + } + else + { + timeZoneRuleVersion = new UInt128(); + result = ResultCode.UninitializedClock; + } + } + + return result; + } + + public ResultCode ToCalendarTimeWithMyRules(long time, out CalendarInfo calendar) + { + ResultCode result; + + lock (_lock) + { + if (_isInitialized) + { + result = ToCalendarTime(in _myRules.Data, time, out calendar); + } + else + { + calendar = new CalendarInfo(); + result = ResultCode.UninitializedClock; + } + } + + return result; + } + + public ResultCode ToCalendarTime(in TimeZoneRule rules, long time, out CalendarInfo calendar) + { + ResultCode result; + + lock (_lock) + { + result = TimeZone.ToCalendarTime(in rules, time, out calendar); + } + + return result; + } + + public ResultCode ToPosixTimeWithMyRules(CalendarTime calendarTime, out long posixTime) + { + ResultCode result; + + lock (_lock) + { + if (_isInitialized) + { + result = ToPosixTime(in _myRules.Data, calendarTime, out posixTime); + } + else + { + posixTime = 0; + result = ResultCode.UninitializedClock; + } + } + + return result; + } + + public ResultCode ToPosixTime(in TimeZoneRule rules, CalendarTime calendarTime, out long posixTime) + { + ResultCode result; + + lock (_lock) + { + result = TimeZone.ToPosixTime(in rules, calendarTime, out posixTime); + } + + return result; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarAdditionalInfo.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarAdditionalInfo.cs new file mode 100644 index 00000000..a84a2785 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarAdditionalInfo.cs @@ -0,0 +1,21 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x18, CharSet = CharSet.Ansi)] + struct CalendarAdditionalInfo + { + public uint DayOfWeek; + public uint DayOfYear; + + public Array8<byte> TimezoneName; + + [MarshalAs(UnmanagedType.I1)] + public bool IsDaySavingTime; + + public Array3<byte> Padding; + + public int GmtOffset; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarInfo.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarInfo.cs new file mode 100644 index 00000000..68e6245b --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarInfo.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x20, CharSet = CharSet.Ansi)] + struct CalendarInfo + { + public CalendarTime Time; + public CalendarAdditionalInfo AdditionalInfo; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarTime.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarTime.cs new file mode 100644 index 00000000..d594223d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/CalendarTime.cs @@ -0,0 +1,15 @@ +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x8)] + struct CalendarTime + { + public short Year; + public sbyte Month; + public sbyte Day; + public sbyte Hour; + public sbyte Minute; + public sbyte Second; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TimeTypeInfo.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TimeTypeInfo.cs new file mode 100644 index 00000000..b8b3d917 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TimeTypeInfo.cs @@ -0,0 +1,28 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 4)] + public struct TimeTypeInfo + { + public const int Size = 0x10; + + public int GmtOffset; + + [MarshalAs(UnmanagedType.I1)] + public bool IsDaySavingTime; + + public Array3<byte> Padding1; + + public int AbbreviationListIndex; + + [MarshalAs(UnmanagedType.I1)] + public bool IsStandardTimeDaylight; + + [MarshalAs(UnmanagedType.I1)] + public bool IsGMT; + + public ushort Padding2; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TimeZoneRule.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TimeZoneRule.cs new file mode 100644 index 00000000..67237f3d --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TimeZoneRule.cs @@ -0,0 +1,56 @@ +using Ryujinx.Common.Utilities; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + [StructLayout(LayoutKind.Sequential, Pack = 4, Size = 0x4000, CharSet = CharSet.Ansi)] + public struct TimeZoneRule + { + public const int TzMaxTypes = 128; + public const int TzMaxChars = 50; + public const int TzMaxLeaps = 50; + public const int TzMaxTimes = 1000; + public const int TzNameMax = 255; + public const int TzCharsArraySize = 2 * (TzNameMax + 1); + + public int TimeCount; + public int TypeCount; + public int CharCount; + + [MarshalAs(UnmanagedType.I1)] + public bool GoBack; + + [MarshalAs(UnmanagedType.I1)] + public bool GoAhead; + + [StructLayout(LayoutKind.Sequential, Size = sizeof(long) * TzMaxTimes)] + private struct AtsStorageStruct { } + + private AtsStorageStruct _ats; + + public Span<long> Ats => SpanHelpers.AsSpan<AtsStorageStruct, long>(ref _ats); + + [StructLayout(LayoutKind.Sequential, Size = sizeof(byte) * TzMaxTimes)] + private struct TypesStorageStruct { } + + private TypesStorageStruct _types; + + public Span<byte> Types => SpanHelpers.AsByteSpan(ref _types); + + [StructLayout(LayoutKind.Sequential, Size = TimeTypeInfo.Size * TzMaxTypes)] + private struct TimeTypeInfoStorageStruct { } + + private TimeTypeInfoStorageStruct _ttis; + + public Span<TimeTypeInfo> Ttis => SpanHelpers.AsSpan<TimeTypeInfoStorageStruct, TimeTypeInfo>(ref _ttis); + + [StructLayout(LayoutKind.Sequential, Size = sizeof(byte) * TzCharsArraySize)] + private struct CharsStorageStruct { } + + private CharsStorageStruct _chars; + public Span<byte> Chars => SpanHelpers.AsByteSpan(ref _chars); + + public int DefaultType; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TzifHeader.cs b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TzifHeader.cs new file mode 100644 index 00000000..022c34a9 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/TimeZone/Types/TzifHeader.cs @@ -0,0 +1,19 @@ +using Ryujinx.Common.Memory; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.TimeZone +{ + [StructLayout(LayoutKind.Sequential, Pack = 0x4, Size = 0x2C)] + struct TzifHeader + { + public Array4<byte> Magic; + public byte Version; + private Array15<byte> _reserved; + public int TtisGMTCount; + public int TtisSTDCount; + public int LeapCount; + public int TimeCount; + public int TypeCount; + public int CharCount; + } +}
\ No newline at end of file diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Types/SteadyClockContext.cs b/src/Ryujinx.HLE/HOS/Services/Time/Types/SteadyClockContext.cs new file mode 100644 index 00000000..38d37055 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Types/SteadyClockContext.cs @@ -0,0 +1,12 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.HLE.HOS.Services.Time.Types +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct SteadyClockContext + { + public ulong InternalOffset; + public UInt128 ClockSourceId; + } +} diff --git a/src/Ryujinx.HLE/HOS/Services/Time/Types/TimePermissions.cs b/src/Ryujinx.HLE/HOS/Services/Time/Types/TimePermissions.cs new file mode 100644 index 00000000..3fcd3a14 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Services/Time/Types/TimePermissions.cs @@ -0,0 +1,22 @@ +using System; + +namespace Ryujinx.HLE.HOS.Services.Time +{ + [Flags] + enum TimePermissions + { + LocalSystemClockWritableMask = 0x1, + UserSystemClockWritableMask = 0x2, + NetworkSystemClockWritableMask = 0x4, + TimeZoneWritableMask = 0x8, + SteadyClockWritableMask = 0x10, + BypassUninitialized = 0x20, + + User = 0, + Admin = LocalSystemClockWritableMask | UserSystemClockWritableMask | TimeZoneWritableMask, + System = NetworkSystemClockWritableMask, + SystemUpdate = BypassUninitialized, + Repair = SteadyClockWritableMask, + Manufacture = LocalSystemClockWritableMask | UserSystemClockWritableMask | NetworkSystemClockWritableMask | TimeZoneWritableMask | SteadyClockWritableMask + } +} |
