using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Drawing; using BizHawk.Common; 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 : IDisposable { // 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 { get { return Global.Emulator.AsStatable(); } } public Action InvalidateCallback { get; set; } private void CallInvalidateCallback(int index) { if (InvalidateCallback != null) { InvalidateCallback(index); } } private List lowPriorityStates = new List(); internal NDBDatabase ndbdatabase; private Guid guid = Guid.NewGuid(); private SortedList States = new SortedList(); private string statePath { get { var basePath = PathManager.MakeAbsolutePath(Global.Config.PathEntries["Global", "TAStudio states"].Path, null); return Path.Combine(basePath, guid.ToString()); } } private bool _isMountedForWrite; private readonly TasMovie _movie; private ulong _expectedStateSize = 0; private int _minFrequency = VersionInfo.DeveloperBuild ? 2 : 1; private const int _maxFrequency = 16; private int StateFrequency { get { int freq = (int)(_expectedStateSize / 65536); if (freq < _minFrequency) { return _minFrequency; } if (freq > _maxFrequency) { return _maxFrequency; } return freq; } } private int maxStates { get { return (int)(Settings.Cap / _expectedStateSize) + (int)((ulong)Settings.DiskCapacitymb * 1024 * 1024 / _expectedStateSize); } } private int _stateGap { get { return 1 << Settings.StateGap; } } public TasStateManager(TasMovie movie) { _movie = movie; Settings = new TasStateManagerSettings(Global.Config.DefaultTasProjSettings); accessed = new List(); if (_movie.StartsFromSavestate) SetState(0, _movie.BinarySavestate); } public void Dispose() { if (ndbdatabase != null) ndbdatabase.Dispose(); //States and BranchStates don't need cleaning because they would only contain an ndbdatabase entry which was demolished by the above } /// /// Mounts this instance for write access. Prior to that it's read-only /// public void MountWriteAccess() { if (_isMountedForWrite) return; _isMountedForWrite = true; int limit = 0; _expectedStateSize = (ulong)Core.SaveStateBinary().Length; if (_expectedStateSize > 0) { limit = maxStates; } States = new SortedList(limit); if (_expectedStateSize > int.MaxValue) throw new InvalidOperationException(); ndbdatabase = new NDBDatabase(statePath, Settings.DiskCapacitymb * 1024 * 1024, (int)_expectedStateSize); } public TasStateManagerSettings Settings { get; set; } /// /// Retrieves the savestate for the given frame, /// If this frame does not have a state currently, will return an empty array /// /// A savestate for the given frame or an empty array if there isn't one public KeyValuePair this[int frame] { get { if (frame == 0) { return new KeyValuePair(0, InitialState); } if (States.ContainsKey(frame)) { StateAccessed(frame); return new KeyValuePair(frame, States[frame].State); } return new KeyValuePair(-1, new byte[0]); } } private List accessed; public byte[] InitialState { get { if (_movie.StartsFromSavestate) { return _movie.BinarySavestate; } return States[0].State; } } /// /// Requests that the current emulator state be captured /// Unless force is true, the state may or may not be captured depending on the logic employed by "greenzone" management /// public void Capture(bool force = false) { bool shouldCapture = false; int frame = Global.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 = force; } 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 (_movie.Markers.IsMarker(frame + 1)) { shouldCapture = true; // Markers shoudl always get priority } else { shouldCapture = frame % StateFrequency == 0; } if (shouldCapture) { SetState(frame, (byte[])Core.SaveStateBinary().Clone(), skipRemoval: false); } } private void MaybeRemoveStates() { // Loop, because removing a state that has a duplicate won't save any space while (Used + _expectedStateSize > Settings.Cap || DiskUsed > (ulong)Settings.DiskCapacitymb * 1024 * 1024) { Point shouldRemove = StateToRemove(); RemoveState(shouldRemove.X, shouldRemove.Y); } if (Used > Settings.Cap) { int lastMemState = -1; do { lastMemState++; } while (States[accessed[lastMemState].Frame] == null); MoveStateToDisk(accessed[lastMemState].Frame); } } /// /// X is the frame of the state, Y is the branch (-1 for current). /// private Point StateToRemove() { // X is frame, Y is branch Point shouldRemove = new Point(-1, -1); if (BranchStates.Any() && Settings.EraseBranchStatesFirst) { var kvp = BranchStates.Count() > 1 ? BranchStates.ElementAt(1) : BranchStates.ElementAt(0); shouldRemove.X = kvp.Key; shouldRemove.Y = kvp.Value.Keys[0]; return shouldRemove; } int i = 0; int markerSkips = maxStates / 2; // lowPrioritySates (e.g. states with only lag frames between them) do { if (lowPriorityStates.Count > i) shouldRemove = findState(lowPriorityStates[i]); else break; // Keep marker states markerSkips--; if (markerSkips < 0) shouldRemove.X = -1; i++; } while (StateIsMarker(shouldRemove.X, shouldRemove.Y) && markerSkips > -1 || shouldRemove.X == 0); // by last accessed markerSkips = maxStates / 2; if (shouldRemove.X < 1) { i = 0; do { if (accessed.Count > i) shouldRemove = findState(accessed[i]); else break; // Keep marker states markerSkips--; if (markerSkips < 0) shouldRemove.X = -1; i++; } while (StateIsMarker(shouldRemove.X, shouldRemove.Y) && markerSkips > -1 || shouldRemove.X == 0); } if (shouldRemove.X < 1) // only found marker states above { if (BranchStates.Any() && !Settings.EraseBranchStatesFirst) { var kvp = BranchStates.Count() > 1 ? BranchStates.ElementAt(1) : BranchStates.ElementAt(0); shouldRemove.X = kvp.Key; shouldRemove.Y = kvp.Value.Keys[0]; } else { StateManagerState s = States.Values[1]; shouldRemove.X = s.Frame; shouldRemove.Y = -1; } } return shouldRemove; } private bool StateIsMarker(int frame, int branch) { if (frame == -1) return false; if (branch == -1) return _movie.Markers.IsMarker(States[frame].Frame + 1); else { if (_movie.GetBranch(_movie.BranchIndexByHash(branch)).Markers == null) return _movie.Markers.IsMarker(States[frame].Frame + 1); else return _movie.GetBranch(branch).Markers.Any(m => m.Frame + 1 == frame); } } private bool AllLag(int from, int upTo) { if (upTo >= Global.Emulator.Frame) { upTo = Global.Emulator.Frame - 1; if (!Global.Emulator.AsInputPollable().IsLagFrame) return false; } for (int i = from; i < upTo; i++) { if (_movie[i].Lagged == false) return false; } return true; } private void MoveStateToDisk(int index) { Used -= (ulong)States[index].Length; States[index].MoveToDisk(); } private void MoveStateToMemory(int index) { States[index].MoveToRAM(); Used += (ulong)States[index].Length; } internal void SetState(int frame, byte[] state, bool skipRemoval = true) { if (!skipRemoval) // skipRemoval: false only when capturing new states MaybeRemoveStates(); // Remove before adding so this state won't be removed. if (States.ContainsKey(frame)) { if (stateHasDuplicate(frame, -1) != -2) Used += (ulong)state.Length; States[frame].State = state; } else { Used += (ulong)state.Length; States.Add(frame, new StateManagerState(this, state, frame)); } StateAccessed(frame); int i = States.IndexOfKey(frame); if (i > 0 && AllLag(States.Keys[i - 1], States.Keys[i])) { lowPriorityStates.Add(States[frame]); } } private void RemoveState(int frame, int branch = -1) { if (branch == -1) accessed.Remove(States[frame]); else if (accessed.Contains(BranchStates[frame][branch]) && !Settings.EraseBranchStatesFirst) accessed.Remove(BranchStates[frame][branch]); StateManagerState state; bool hasDuplicate = stateHasDuplicate(frame, branch) != -2; if (branch == -1) { state = States[frame]; if (States[frame].IsOnDisk) States[frame].Dispose(); else Used -= (ulong)States[frame].Length; States.RemoveAt(States.IndexOfKey(frame)); } else { state = BranchStates[frame][branch]; if (BranchStates[frame][branch].IsOnDisk) BranchStates[frame][branch].Dispose(); else //Used -= (ulong)BranchStates[frame][branch].Length; BranchStates[frame].RemoveAt(BranchStates[frame].IndexOfKey(branch)); if (BranchStates[frame].Count == 0) BranchStates.Remove(frame); } if (!hasDuplicate) lowPriorityStates.Remove(state); } private void StateAccessed(int frame) { if (frame == 0 && _movie.StartsFromSavestate) return; StateManagerState state = States[frame]; bool removed = accessed.Remove(state); accessed.Add(state); if (States[frame].IsOnDisk) { if (!States[accessed[0].Frame].IsOnDisk) MoveStateToDisk(accessed[0].Frame); MoveStateToMemory(frame); } if (!removed && accessed.Count > maxStates) accessed.RemoveAt(0); } public bool HasState(int frame) { if (_movie.StartsFromSavestate && frame == 0) { return true; } return States.ContainsKey(frame); } /// /// Clears out all savestates after the given frame number /// public bool Invalidate(int frame) { bool anyInvalidated = false; if (Any()) { if (!_movie.StartsFromSavestate && frame == 0) // Never invalidate frame 0 on a non-savestate-anchored movie { frame = 1; } List> statesToRemove = States.Where(x => x.Key >= frame).ToList(); anyInvalidated = statesToRemove.Any(); foreach (KeyValuePair state in statesToRemove) RemoveState(state.Key); CallInvalidateCallback(frame); } return anyInvalidated; } /// /// Clears all state information /// /// public void Clear() { States.Clear(); accessed.Clear(); Used = 0; clearDiskStates(); } public void ClearStateHistory() { if (States.Any()) { StateManagerState power = States.Values.FirstOrDefault(s => s.Frame == 0); StateAccessed(power.Frame); States.Clear(); accessed.Clear(); SetState(0, power.State); Used = (ulong)power.State.Length; clearDiskStates(); } } private void clearDiskStates() { if (ndbdatabase != null) ndbdatabase.Clear(); } /// /// Deletes/moves states to follow the state storage size limits. /// Used after changing the settings. /// public void LimitStateCount() { while (Used + DiskUsed > Settings.CapTotal) { Point s = StateToRemove(); RemoveState(s.X, s.Y); } int index = -1; while (DiskUsed > (ulong)Settings.DiskCapacitymb * 1024uL * 1024uL) { do { index++; } while (!accessed[index].IsOnDisk); accessed[index].MoveToRAM(); } if (Used > Settings.Cap) MaybeRemoveStates(); } private List ExcludeStates() { List ret = new List(); ulong saveUsed = Used + DiskUsed; // respect state gap no matter how small the resulting size will be // still leave marker states for (int i = 1; i < States.Count; i++) { if (_movie.Markers.IsMarker(States.ElementAt(i).Key + 1) || States.ElementAt(i).Key % _stateGap == 0) continue; ret.Add(i); if (States.ElementAt(i).Value.IsOnDisk) saveUsed -= _expectedStateSize; else saveUsed -= (ulong)States.ElementAt(i).Value.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 { index++; if (index >= States.Count) break; } while (_movie.Markers.IsMarker(States.ElementAt(index).Key + 1)); if (index >= States.Count) break; ret.Add(index); if (States.ElementAt(index).Value.IsOnDisk) saveUsed -= _expectedStateSize; else saveUsed -= (ulong)States.ElementAt(index).Value.Length; } // if there are enough markers to still be over the limit, remove marker frames index = 0; while (saveUsed > (ulong)Settings.DiskSaveCapacitymb * 1024 * 1024) { index++; if (!ret.Contains(index)) ret.Add(index); if (States.ElementAt(index).Value.IsOnDisk) saveUsed -= _expectedStateSize; else saveUsed -= (ulong)States.ElementAt(index).Value.Length; } return ret; } 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; StateAccessed(States.ElementAt(i).Key); KeyValuePair kvp = States.ElementAt(i); bw.Write(kvp.Key); bw.Write(kvp.Value.Length); bw.Write(kvp.Value.State); //_movie.ReportProgress(100d / States.Count * 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); //States.Add(frame, data); //Used += len; } } catch (EndOfStreamException) { } } public void SaveBranchStates(BinaryWriter bw) { bw.Write(BranchStates.Count); foreach (var s in BranchStates) { bw.Write(s.Key); bw.Write(s.Value.Count); foreach (var t in s.Value) { bw.Write(t.Key); t.Value.Write(bw); } } } public void LoadBranchStates(BinaryReader br) { try { int c = br.ReadInt32(); BranchStates = new SortedList>(c); while (c > 0) { int key = br.ReadInt32(); int c2 = br.ReadInt32(); var list = new SortedList(c2); while (c2 > 0) { int key2 = br.ReadInt32(); var state = StateManagerState.Read(br, this); list.Add(key2, state); c2--; } BranchStates.Add(key, list); c--; } } catch (EndOfStreamException) { } } public KeyValuePair GetStateClosestToFrame(int frame) { var s = States.LastOrDefault(state => state.Key < frame); return this[s.Key]; } // Map: // 4 bytes - total savestate count //[Foreach state] // 4 bytes - frame // 4 bytes - length of savestate // 0 - n savestate private ulong _used; private ulong Used { get { return _used; } set { if (value > 0xf000000000000000) System.Diagnostics.Debug.Fail("ulong Used underfow!"); else _used = value; } } private ulong DiskUsed { get { if (ndbdatabase == null) return 0; else return (ulong)ndbdatabase.Consumed; } } public int StateCount { get { return States.Count; } } public bool Any() { if (_movie.StartsFromSavestate) { return States.Count > 0; } return States.Count > 1; } public int LastKey { get { if (States.Count == 0) { return 0; } return States.Last().Key; } } public int LastEmulatedFrame { get { if (StateCount > 0) { return LastKey; } return 0; } } #region "Branches" private SortedList> BranchStates = new SortedList>(); /// /// Checks if the state at frame in the given branch (-1 for current) has any duplicates. /// /// Index of the branch (-1 for current) of the first match. If no match, returns -2. private int stateHasDuplicate(int frame, int branchHash) { StateManagerState stateToMatch; // Get the state instance if (branchHash == -1) stateToMatch = States[frame]; else { if (!BranchStates[frame].ContainsKey(branchHash)) return -2; stateToMatch = BranchStates[frame][branchHash]; // Check the current branch for duplicate. if (States.ContainsKey(frame) && States[frame] == stateToMatch) return -1; } // Check if there are any branch states for the given frame. if (!BranchStates.ContainsKey(frame) || BranchStates[frame] == null || branchHash == -1) return -2; // Loop through branch states for the given frame. SortedList stateList = BranchStates[frame]; for (int i = 0; i < stateList.Count(); i++) { // Don't check the branch containing the state to match. if (i == _movie.BranchIndexByHash(branchHash)) continue; if (stateList.Values[i] == stateToMatch) return i; } return -2; // No match. } private Point findState(StateManagerState s) { Point ret = new Point(0, -1); ret.X = s.Frame; if (!States.ContainsValue(s)) { if (BranchStates.ContainsKey(s.Frame)) { int index = BranchStates[s.Frame].Values.IndexOf(s); ret.Y = BranchStates[s.Frame].Keys.ElementAt(index); } if (ret.Y == -1) return new Point(-1, -2); } return ret; } public void AddBranch() { int branchHash = _movie.BranchHashByIndex(_movie.BranchCount - 1); foreach (KeyValuePair kvp in States) { if (!BranchStates.ContainsKey(kvp.Key)) BranchStates.Add(kvp.Key, new SortedList()); SortedList stateList = BranchStates[kvp.Key]; if (stateList == null) // when does this happen? { stateList = new SortedList(); BranchStates[kvp.Key] = stateList; } stateList.Add(branchHash, kvp.Value); // We aren't creating any new states, just adding a reference to an already-existing one; so Used doesn't need to be updated. } } public void RemoveBranch(int index) { int branchHash = _movie.BranchHashByIndex(index); foreach (KeyValuePair> kvp in BranchStates.ToList()) { SortedList stateList = kvp.Value; if (stateList == null) continue; /* if (stateList.ContainsKey(branchHash)) { if (stateHasDuplicate(kvp.Key, branchHash) == -2) { if (!stateList[branchHash].IsOnDisk) Used -= (ulong)stateList[branchHash].Length; } } */ stateList.Remove(branchHash); if (stateList.Count == 0) BranchStates.Remove(kvp.Key); } } public void UpdateBranch(int index) { if (index == -1) // backup branch is outsider return; int branchHash = _movie.BranchHashByIndex(index); // RemoveBranch foreach (KeyValuePair> kvp in BranchStates.ToList()) { SortedList stateList = kvp.Value; if (stateList == null) continue; /* if (stateList.ContainsKey(branchHash)) { if (stateHasDuplicate(kvp.Key, branchHash) == -2) { if (!stateList[branchHash].IsOnDisk) Used -= (ulong)stateList[branchHash].Length; } } */ stateList.Remove(branchHash); if (stateList.Count == 0) BranchStates.Remove(kvp.Key); } // AddBranch foreach (KeyValuePair kvp in States) { if (!BranchStates.ContainsKey(kvp.Key)) BranchStates.Add(kvp.Key, new SortedList()); SortedList stateList = BranchStates[kvp.Key]; if (stateList == null) { stateList = new SortedList(); BranchStates[kvp.Key] = stateList; } stateList.Add(branchHash, kvp.Value); } } public void LoadBranch(int index) { if (index == -1) // backup branch is outsider return; int branchHash = _movie.BranchHashByIndex(index); //Invalidate(0); // Not a good way of doing it? foreach (KeyValuePair> kvp in BranchStates) { if (kvp.Key == 0 && States.ContainsKey(0)) continue; // TODO: It might be a better idea to just not put state 0 in BranchStates. if (kvp.Value.ContainsKey(branchHash)) SetState(kvp.Key, kvp.Value[branchHash].State); } } #endregion } }