using System; using System.Collections.Generic; using System.IO; using System.Linq; using BizHawk.Common.NumberExtensions; using BizHawk.Emulation.Common; using BizHawk.Emulation.Common.IEmulatorExtensions; namespace BizHawk.Client.Common { /// /// Captures savestates and manages the logic of adding, retrieving, /// invalidating/clearing of states. Also does memory management and limiting of states /// public class TasStateManager : IStateManager { private const int MinFrequency = 1; private const int MaxFrequency = 16; // TODO: pass this in, and find a solution to a stale reference (this is instantiated BEFORE a new core instance is made, making this one stale if it is simply set in the constructor private IStatable Core => Global.Emulator.AsStatable(); private IEmulator Emulator => Global.Emulator; private readonly StateManagerDecay _decay; private readonly TasMovie _movie; private readonly SortedList _states; private readonly ulong _expectedStateSize; private ulong _used; private int _stateFrequency; private int MaxStates => (int)(Settings.Cap / _expectedStateSize) + (int)((ulong)Settings.DiskCapacityMb * 1024 * 1024 / _expectedStateSize); private int FileStateGap => 1 << Settings.FileStateGap; /// loaded core expects savestate size of 0 B public TasStateManager(TasMovie movie, TasStateManagerSettings settings) { _movie = movie; Settings = new TasStateManagerSettings(settings); if (_movie.StartsFromSavestate) { SetState(0, _movie.BinarySavestate); } _decay = new StateManagerDecay(_movie, this); _expectedStateSize = (ulong)Core.SaveStateBinary().Length; if (_expectedStateSize == 0) { throw new InvalidOperationException("Savestate size can not be zero!"); } _states = new SortedList(MaxStates); UpdateStateFrequency(); } public Action InvalidateCallback { get; set; } public TasStateManagerSettings Settings { get; set; } public byte[] this[int frame] { get { if (frame == 0) { return InitialState; } if (_states.ContainsKey(frame)) { return _states[frame]; } return new byte[0]; } } public int Count => _states.Count; public int Last => _states.Count > 0 ? _states.Last().Key : 0; private byte[] InitialState => _movie.StartsFromSavestate ? _movie.BinarySavestate : _states[0]; public bool Any() { if (_movie.StartsFromSavestate) { return _states.Count > 0; } return _states.Count > 1; } public void UpdateStateFrequency() { _stateFrequency = ((int)_expectedStateSize / Settings.MemStateGapDivider / 1024) .Clamp(MinFrequency, MaxFrequency); _decay.UpdateSettings(MaxStates, _stateFrequency, 4); LimitStateCount(); } public void Capture(bool force = false) { bool shouldCapture; int frame = Emulator.Frame; if (_movie.StartsFromSavestate && frame == 0) // Never capture frame 0 on savestate anchored movies since we have it anyway { shouldCapture = false; } else if (force) { shouldCapture = true; } else if (frame == 0) // For now, long term, TasMovie should have a .StartState property, and a .tasproj file for the start state in non-savestate anchored movies { shouldCapture = true; } else if (IsMarkerState(frame)) { shouldCapture = true; // Markers should always get priority } else { shouldCapture = frame % _stateFrequency == 0; } if (shouldCapture) { SetState(frame, (byte[])Core.SaveStateBinary().Clone(), skipRemoval: false); } } public void Clear() { if (_states.Any()) { // For power-on movies, we can't lose frame 0; byte[] power = null; if (!_movie.StartsFromSavestate) { power = _states[0]; } _states.Clear(); if (power != null) { SetState(0, power); _used = (ulong)power.Length; } } } public bool HasState(int frame) { if (_movie.StartsFromSavestate && frame == 0) { return true; } return _states.ContainsKey(frame); } /// true iff any frames were invalidated public bool Invalidate(int frame) { if (!Any()) return false; if (frame == 0) frame = 1; // Never invalidate frame 0 var statesToRemove = _states.Where(s => s.Key >= frame).ToList(); foreach (var state in statesToRemove) Remove(state.Key); InvalidateCallback?.Invoke(frame); return statesToRemove.Count != 0; } public bool Remove(int frame) { int index = _states.IndexOfKey(frame); if (frame < 1 || index < 1) { return false; } var state = _states[frame]; _used -= (ulong)state.Length; _states.RemoveAt(index); return true; } // Map: // 4 bytes - total savestate count // [Foreach state] // 4 bytes - frame // 4 bytes - length of savestate // 0 - n savestate public void Save(BinaryWriter bw) { List noSave = ExcludeStates(); bw.Write(_states.Count - noSave.Count); for (int i = 0; i < _states.Count; i++) { if (noSave.Contains(i)) { continue; } bw.Write(_states.Keys[i]); bw.Write(_states.Values[i].Length); bw.Write(_states.Values[i]); } } public void Load(BinaryReader br) { _states.Clear(); try { int nstates = br.ReadInt32(); for (int i = 0; i < nstates; i++) { int frame = br.ReadInt32(); int len = br.ReadInt32(); byte[] data = br.ReadBytes(len); // whether we should allow state removal check here is an interesting question // nothing was edited yet, so it might make sense to show the project untouched first SetState(frame, data); } } catch (EndOfStreamException) { } } public KeyValuePair GetStateClosestToFrame(int frame) { var s = _states.LastOrDefault(state => state.Key < frame); if (s.Key > 0) { return s; } return new KeyValuePair(0, InitialState); } public int GetStateIndexByFrame(int frame) { return _states.IndexOfKey(GetStateClosestToFrame(frame).Key); } public int GetStateFrameByIndex(int index) { return _states.Keys[index]; } private bool IsMarkerState(int frame) { return _movie.Markers.IsMarker(frame + 1); } private void SetState(int frame, byte[] state, bool skipRemoval = true) { if (!skipRemoval) // skipRemoval: false only when capturing new states { LimitStateCount(); // Remove before adding so this state won't be removed. } if (_states.ContainsKey(frame)) { _states[frame] = state; } else { _used += (ulong)state.Length; _states.Add(frame, state); } } // Deletes states to follow the state storage size limits. // Used after changing the settings too. private void LimitStateCount() { if (Count + 1 > MaxStates) { _decay.Trigger(Count + 1 - MaxStates); } } private List ExcludeStates() { List ret = new List(); ulong saveUsed = _used; // respect state gap no matter how small the resulting size will be // still leave marker states for (int i = 1; i < _states.Count; i++) { int frame = GetStateFrameByIndex(i); if (IsMarkerState(frame) || frame % FileStateGap < _stateFrequency) { continue; } ret.Add(i); saveUsed -= (ulong)_states.Values[i].Length; } // if the size is still too big, exclude states form the beginning // still leave marker states int index = 0; while (saveUsed > (ulong)Settings.DiskSaveCapacityMb * 1024 * 1024) { do { if (++index >= _states.Count) { break; } } while (IsMarkerState(GetStateFrameByIndex(index))); if (index >= _states.Count) { break; } ret.Add(index); saveUsed -= (ulong)_states.Values[index].Length; } // if there are enough markers to still be over the limit, remove marker frames index = 0; while (saveUsed > (ulong)Settings.DiskSaveCapacityMb * 1024 * 1024) { if (!ret.Contains(++index)) { ret.Add(index); } saveUsed -= (ulong)_states.Values[index].Length; } return ret; } } }