diff --git a/src/LibHac/FsSrv/DefaultFsServerObjects.cs b/src/LibHac/FsSrv/DefaultFsServerObjects.cs
index f759fea4..c97c046a 100644
--- a/src/LibHac/FsSrv/DefaultFsServerObjects.cs
+++ b/src/LibHac/FsSrv/DefaultFsServerObjects.cs
@@ -25,7 +25,7 @@ public class DefaultFsServerObjects
var sdCard = new EmulatedSdCard();
var gameCardNew = new GameCardDummy();
- var sdmmcNew = new SdmmcApi();
+ var sdmmcNew = new SdmmcApi(fsServer);
var gcStorageCreator = new EmulatedGameCardStorageCreator(gameCard);
diff --git a/src/LibHac/Os/Event.cs b/src/LibHac/Os/Event.cs
new file mode 100644
index 00000000..078f081c
--- /dev/null
+++ b/src/LibHac/Os/Event.cs
@@ -0,0 +1,73 @@
+using System;
+
+namespace LibHac.Os;
+
+public struct EventType
+{
+}
+
+public class Event : IDisposable
+{
+ private EventType _event;
+
+ private readonly OsState _os;
+
+ public Event(OsState os, EventClearMode clearMode)
+ {
+ _os = os;
+ _os.InitializeEvent(ref _event, signaled: false, clearMode);
+ }
+
+ public void Dispose() => _os.FinalizeEvent(ref _event);
+ public void Wait() => _os.WaitEvent(ref _event);
+ public bool TryWait() => _os.TryWaitEvent(ref _event);
+ public bool TimedWait(TimeSpan timeout) => _os.TimedWaitEvent(ref _event, timeout);
+ public void Signal() => _os.SignalEvent(ref _event);
+ public void Clear() => _os.ClearEvent(ref _event);
+ public ref EventType GetBase() => ref _event;
+}
+
+public static class EventApi
+{
+ public static void InitializeEvent(this OsState os, ref EventType eventType, bool signaled,
+ EventClearMode clearMode)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void FinalizeEvent(this OsState os, ref EventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void SignalEvent(this OsState os, ref EventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void WaitEvent(this OsState os, ref EventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static bool TryWaitEvent(this OsState os, ref EventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static bool TimedWaitEvent(this OsState os, ref EventType eventType, TimeSpan timeout)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void ClearEvent(this OsState os, ref EventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void InitializeMultiWaitHolder(this OsState os, ref MultiWaitHolderType multiWaitHolder,
+ ref EventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/src/LibHac/Os/EventCommon.cs b/src/LibHac/Os/EventCommon.cs
new file mode 100644
index 00000000..4b555130
--- /dev/null
+++ b/src/LibHac/Os/EventCommon.cs
@@ -0,0 +1,7 @@
+namespace LibHac.Os;
+
+public enum EventClearMode
+{
+ ManualClear = 0,
+ AutoClear = 1
+}
\ No newline at end of file
diff --git a/src/LibHac/Os/TimerEvent.cs b/src/LibHac/Os/TimerEvent.cs
new file mode 100644
index 00000000..e0eae47a
--- /dev/null
+++ b/src/LibHac/Os/TimerEvent.cs
@@ -0,0 +1,85 @@
+using System;
+
+namespace LibHac.Os;
+
+public struct TimerEventType
+{
+}
+
+public class TimerEvent : IDisposable
+{
+ private TimerEventType _event;
+
+ private readonly OsState _os;
+
+ public TimerEvent(OsState os, EventClearMode clearMode)
+ {
+ _os = os;
+ _os.InitializeTimerEvent(ref _event, clearMode);
+ }
+
+ public void Dispose() => _os.FinalizeTimerEvent(ref _event);
+ public void StartOneShot(TimeSpan firstTime) => _os.StartOneShotTimerEvent(ref _event, firstTime);
+ public void StartPeriodic(TimeSpan firstTime, TimeSpan interval) => _os.StartPeriodicTimerEvent(ref _event, firstTime, interval);
+ public void Stop() => _os.StopTimerEvent(ref _event);
+ public void Wait() => _os.WaitTimerEvent(ref _event);
+ public bool TryWait() => _os.TryWaitTimerEvent(ref _event);
+ public void Signal() => _os.SignalTimerEvent(ref _event);
+ public void Clear() => _os.ClearTimerEvent(ref _event);
+ public ref TimerEventType GetBase() => ref _event;
+}
+
+public static class TimerEventApi
+{
+ public static void InitializeTimerEvent(this OsState os, ref TimerEventType eventType, EventClearMode clearMode)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void FinalizeTimerEvent(this OsState os, ref TimerEventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void StartOneShotTimerEvent(this OsState os, ref TimerEventType eventType, TimeSpan firstTime)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void StartPeriodicTimerEvent(this OsState os, ref TimerEventType eventType, TimeSpan firstTime,
+ TimeSpan interval)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void StopTimerEvent(this OsState os, ref TimerEventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void SignalTimerEvent(this OsState os, ref TimerEventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void ClearTimerEvent(this OsState os, ref TimerEventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void WaitTimerEvent(this OsState os, ref TimerEventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static bool TryWaitTimerEvent(this OsState os, ref TimerEventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static void InitializeMultiWaitHolder(this OsState os, ref MultiWaitHolderType multiWaitHolder,
+ ref TimerEventType eventType)
+ {
+ throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/src/LibHac/Sdmmc/Common.cs b/src/LibHac/Sdmmc/Common.cs
index 4ee2b2e1..30b35194 100644
--- a/src/LibHac/Sdmmc/Common.cs
+++ b/src/LibHac/Sdmmc/Common.cs
@@ -1,4 +1,5 @@
using System;
+using LibHac.FsSrv;
namespace LibHac.Sdmmc;
@@ -60,6 +61,14 @@ public partial class SdmmcApi
public const int DeviceCidSize = 0x10;
public const int DeviceCsdSize = 0x10;
+ private FileSystemServer _fsServer;
+ internal HorizonClient Hos => _fsServer.Hos;
+
+ public SdmmcApi(FileSystemServer fsServer)
+ {
+ _fsServer = fsServer;
+ }
+
public void SwitchToPcvClockResetControl()
{
throw new NotImplementedException();
diff --git a/src/LibHac/SdmmcSrv/PatrolReader.cs b/src/LibHac/SdmmcSrv/PatrolReader.cs
index 14a9744b..c30d4b13 100644
--- a/src/LibHac/SdmmcSrv/PatrolReader.cs
+++ b/src/LibHac/SdmmcSrv/PatrolReader.cs
@@ -1,73 +1,404 @@
using System;
+using LibHac.Common;
+using LibHac.Crypto;
+using LibHac.Diag;
+using LibHac.Fs;
+using LibHac.FsSystem;
using LibHac.Os;
using LibHac.Sdmmc;
+using static LibHac.SdmmcSrv.Common;
+using MmcPartition = LibHac.Sdmmc.MmcPartition;
namespace LibHac.SdmmcSrv;
+///
+/// Performs regular patrol reads on the internal MMC.
+///
+/// The patrol reader will read sequential half-megabyte blocks of data from the internal MMC,
+/// starting at the beginning of the storage, progressing to the end, and then looping back to the beginning.
+/// This helps with data integrity by ensuring the entire MMC is regularly read.
+/// Every 6 seconds the patrol reader will read a half-megabyte block from the MMC.
+/// Every 2 hours it will save the current state of the patrol read to Boot Partition 1 on the MMC.
+/// This state contains the next sector index to be read and the number of times the MMC has been patrolled
+/// from start to finish.
+/// Based on nnSdk 14.3.0 (FS 14.1.0)
internal class PatrolReader
{
+ // Note: This class won't work until events and timer events are properly implemented.
+
+ private const int MacSize = HmacSha256.HashSize;
+
+ ///
+ /// The patrol reader state that's stored in Boot Partition 1.
+ ///
+ public struct State
+ {
+ public uint CurrentSector;
+ public uint PatrolCount;
+ }
+
+ public enum ThreadState
+ {
+ Stop,
+ Active,
+ Sleep
+ }
+
+ private readonly int _macSize;
+ private readonly MmcPartition _statePartition;
+ private readonly int _patrolStateOffset;
+ private readonly int _patrolStateSize;
+ private readonly int _saveStateIntervalSeconds;
+ private readonly int _patrolReadSize;
+ private readonly int _waitTimeAfterAllocationSuccess;
+ private readonly int _waitTimeAfterAllocationFailure;
+ private readonly int _firstRunDelaySeconds;
+ private uint _deviceCapacitySectors;
+ private State _patrolState;
+ private bool _isPatrolStateLoaded;
+ private SdkMutex _mutex;
+ private bool _areEventsInitialized;
+ private ThreadState _threadState;
+ // private Thread _patrolReaderThread;
+ private Event _stateChangeRequestedEvent;
+ private Event _stateChangeCompletedEvent;
+ private ulong _allocateSuccessCount;
+ private ulong _allocateFailureCount;
+
+ // LibHac addition
+ private SdmmcApi _sdmmc;
+
+ private static ReadOnlySpan PatrolStateKey => "U{W5>1Kq#Gt`f6r86o`9|*||hTy9U2C\0"u8;
+
public PatrolReader(SdkMutex mutex, SdmmcApi sdmmc)
{
- throw new NotImplementedException();
+ _macSize = MacSize;
+ _statePartition = MmcPartition.BootPartition1;
+ _patrolStateOffset = 0x184000;
+ _patrolStateSize = 0x200;
+ _saveStateIntervalSeconds = 7200;
+ _patrolReadSize = 0x80000;
+ _waitTimeAfterAllocationSuccess = 6;
+ _waitTimeAfterAllocationFailure = 1;
+ _firstRunDelaySeconds = 18;
+ _isPatrolStateLoaded = false;
+ _mutex = mutex;
+ _areEventsInitialized = false;
+ _threadState = ThreadState.Stop;
+ _allocateSuccessCount = 0;
+ _allocateFailureCount = 0;
+
+ _sdmmc = sdmmc;
}
public void Dispose()
{
- throw new NotImplementedException();
+ FinalizeObject();
}
public static void PatrolReaderThreadEntry(object args)
{
- throw new NotImplementedException();
+ if (args is PatrolReader reader)
+ {
+ reader.PatrolReaderThread();
+ }
+ else
+ {
+ Abort.DoAbort($"Expected an argument of type {nameof(PatrolReader)} in {nameof(PatrolReaderThreadEntry)}");
+ }
}
- public void FinalizeObject()
+ private void FinalizeObject()
{
- throw new NotImplementedException();
+ if (_areEventsInitialized)
+ {
+ if (_threadState == ThreadState.Sleep)
+ {
+ Resume();
+ }
+
+ if (_threadState == ThreadState.Active)
+ {
+ Stop();
+ }
+
+ _stateChangeRequestedEvent.Dispose();
+ _stateChangeCompletedEvent.Dispose();
+
+ _areEventsInitialized = false;
+ }
}
public Result GetPatrolCount(out uint outCount)
{
- throw new NotImplementedException();
+ UnsafeHelpers.SkipParamInit(out outCount);
+
+ if (!_isPatrolStateLoaded)
+ return ResultFs.HasNotGottenPatrolCount.Log();
+
+ outCount = _patrolState.PatrolCount;
+ return Result.Success;
}
public void GetAndClearAllocateCount(out ulong outSuccessCount, out ulong outFailureCount)
{
- throw new NotImplementedException();
+ using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex);
+
+ outSuccessCount = _allocateSuccessCount;
+ outFailureCount = _allocateFailureCount;
+
+ _allocateSuccessCount = 0;
+ _allocateFailureCount = 0;
}
- public bool LoadState()
+ private bool LoadState()
{
- throw new NotImplementedException();
+ using var pooledBuffer = new PooledBuffer(_patrolStateSize, 1);
+ if (pooledBuffer.GetSize() < _patrolStateSize)
+ return false;
+
+ using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex);
+
+ Abort.DoAbortUnlessSuccess(_sdmmc.SelectMmcPartition(Port.Mmc0, _statePartition));
+
+ try
+ {
+ Result res = _sdmmc.GetDeviceMemoryCapacity(out _deviceCapacitySectors, Port.Mmc0);
+ if (res.IsFailure())
+ {
+ return false;
+ }
+
+ Span patrolStateBuffer = pooledBuffer.GetBuffer().Slice(0, _patrolStateSize);
+
+ res = _sdmmc.Read(patrolStateBuffer, Port.Mmc0, BytesToSectors(_patrolStateOffset),
+ BytesToSectors(_patrolStateSize));
+
+ if (res.IsFailure())
+ {
+ return false;
+ }
+
+ Span mac = stackalloc byte[MacSize];
+
+ // Load an empty state if the verification fails.
+ _patrolState = default;
+
+ HmacSha256.GenerateHmacSha256(mac, patrolStateBuffer.Slice(_macSize), PatrolStateKey);
+
+ if (CryptoUtil.IsSameBytes(mac, patrolStateBuffer, _macSize))
+ {
+ ref State readState = ref SpanHelpers.AsStruct(patrolStateBuffer.Slice(_macSize));
+
+ if (readState.CurrentSector + BytesToSectors(_patrolReadSize) <= _deviceCapacitySectors)
+ {
+ _patrolState = readState;
+ }
+ }
+
+ return true;
+ }
+ finally
+ {
+ Abort.DoAbortUnlessSuccess(_sdmmc.SelectMmcPartition(Port.Mmc0, MmcPartition.UserData));
+ }
}
- public bool SaveState()
+ private void SaveState()
{
- throw new NotImplementedException();
+ using var pooledBuffer = new PooledBuffer(_patrolStateSize, 1);
+
+ if (pooledBuffer.GetSize() < _patrolStateSize)
+ return;
+
+ using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex);
+
+ Span patrolStateBuffer = pooledBuffer.GetBuffer().Slice(0, _patrolStateSize);
+ patrolStateBuffer.Clear();
+
+ SpanHelpers.AsStruct(patrolStateBuffer) = _patrolState;
+
+ HmacSha256.GenerateHmacSha256(patrolStateBuffer.Slice(0, _macSize), patrolStateBuffer.Slice(_macSize),
+ PatrolStateKey);
+
+ Abort.DoAbortUnlessSuccess(_sdmmc.SelectMmcPartition(Port.Mmc0, _statePartition));
+
+ try
+ {
+ _sdmmc.Write(Port.Mmc0, BytesToSectors(_patrolStateOffset), BytesToSectors(_patrolStateSize),
+ patrolStateBuffer);
+ }
+ finally
+ {
+ Abort.DoAbortUnlessSuccess(_sdmmc.SelectMmcPartition(Port.Mmc0, MmcPartition.UserData));
+ }
+ }
+
+ private bool DoPatrolRead()
+ {
+ using var pooledBuffer = new PooledBuffer(_patrolReadSize, 1);
+ using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex);
+
+ if (pooledBuffer.GetSize() < _patrolReadSize)
+ {
+ if (_allocateFailureCount != ulong.MaxValue)
+ _allocateFailureCount++;
+
+ return false;
+ }
+
+ if (_allocateSuccessCount != ulong.MaxValue)
+ _allocateSuccessCount++;
+
+ _sdmmc.Read(pooledBuffer.GetBuffer().Slice(0, _patrolReadSize), Port.Mmc0, _patrolState.CurrentSector,
+ BytesToSectors(_patrolReadSize)).IgnoreResult();
+
+ _patrolState.CurrentSector += BytesToSectors(_patrolReadSize);
+
+ if (_patrolState.CurrentSector + BytesToSectors(_patrolReadSize) > _deviceCapacitySectors)
+ {
+ _patrolState.CurrentSector = 0;
+ _patrolState.PatrolCount++;
+ }
+
+ return true;
}
public void Start()
{
- throw new NotImplementedException();
+ Assert.SdkAssert(_threadState is ThreadState.Active or ThreadState.Stop);
+
+ if (_threadState == ThreadState.Stop)
+ {
+ if (!_areEventsInitialized)
+ {
+ _areEventsInitialized = true;
+ _threadState = ThreadState.Stop;
+
+ _stateChangeRequestedEvent = new Event(_sdmmc.Hos.Os, EventClearMode.AutoClear);
+ _stateChangeCompletedEvent = new Event(_sdmmc.Hos.Os, EventClearMode.AutoClear);
+ }
+
+ _threadState = ThreadState.Active;
+
+ // CreateThread(_patrolReaderThread, PatrolReaderThreadEntry, this, pPatrolReaderStack, priority);
+ // SetThreadNamePointer(_patrolReaderThread, "nn.fs.PatrolReader"u8);
+ // StartThread(_patrolReaderThread);
+ }
}
public void Stop()
{
- throw new NotImplementedException();
+ Assert.SdkAssert(_threadState is ThreadState.Active or ThreadState.Stop or ThreadState.Sleep);
+
+ if (_threadState != ThreadState.Stop)
+ {
+ _threadState = ThreadState.Stop;
+ _stateChangeRequestedEvent.Signal();
+
+ // WaitThread(_patrolReaderThread);
+ // DestroyThread(_patrolReaderThread);
+ }
}
public void Sleep()
{
- throw new NotImplementedException();
+ Assert.SdkAssert(_threadState is ThreadState.Active or ThreadState.Sleep);
+
+ if (_threadState == ThreadState.Active)
+ {
+ _threadState = ThreadState.Sleep;
+
+ _stateChangeRequestedEvent.Signal();
+ _stateChangeCompletedEvent.Wait();
+ }
}
public void Resume()
{
- throw new NotImplementedException();
+ Assert.SdkAssert(_threadState is ThreadState.Active or ThreadState.Sleep);
+
+ if (_threadState == ThreadState.Sleep)
+ {
+ _threadState = ThreadState.Active;
+
+ _stateChangeRequestedEvent.Signal();
+ _stateChangeCompletedEvent.Wait();
+ }
}
- public void PatrolReaderThread()
+ private void PatrolReaderThread()
{
- throw new NotImplementedException();
+ // Missing: SetServiceContext()
+
+ using var timer = new TimerEvent(_sdmmc.Hos.Os, EventClearMode.AutoClear);
+
+ timer.StartPeriodic(TimeSpan.FromSeconds(_firstRunDelaySeconds), TimeSpan.FromSeconds(_saveStateIntervalSeconds));
+
+ int currentWaitTime = _waitTimeAfterAllocationSuccess;
+
+ while (true)
+ {
+ // Wait until the next thread state change or until the next patrol read should be done.
+ if (_stateChangeRequestedEvent.TimedWait(TimeSpan.FromSeconds(currentWaitTime)))
+ {
+ if (_threadState == ThreadState.Stop)
+ {
+ break;
+ }
+
+ if (_threadState == ThreadState.Sleep)
+ {
+ // Acknowledge the request to sleep and wait for the next state change.
+ _stateChangeCompletedEvent.Signal();
+ _stateChangeRequestedEvent.Wait();
+
+ if (_threadState == ThreadState.Stop)
+ {
+ break;
+ }
+
+ if (_threadState == ThreadState.Active)
+ {
+ _stateChangeCompletedEvent.Signal();
+ continue;
+ }
+
+ Assert.SdkAssert(false);
+ }
+ }
+
+ if (!_isPatrolStateLoaded)
+ {
+ // The patrol state will be loaded a single time when the console is booted.
+ // Don't load the patrol state or do patrol reads until the specified amount
+ // of time has past since creating the Patrol Reader.
+ if (!timer.TryWait() || !LoadState())
+ {
+ continue;
+ }
+
+ _isPatrolStateLoaded = true;
+ }
+
+ if (DoPatrolRead())
+ {
+ currentWaitTime = _waitTimeAfterAllocationSuccess;
+ }
+ else
+ {
+ currentWaitTime = _waitTimeAfterAllocationFailure;
+ }
+
+ // Save the patrol state periodically once the specified amount of time has passed since the last save.
+ if (timer.TryWait())
+ {
+ SaveState();
+ }
+ }
+
+ // Save the patrol state if necessary when shutting down.
+ if (_isPatrolStateLoaded)
+ SaveState();
}
}
\ No newline at end of file