diff --git a/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/BizHawk.Client.Common/BizHawk.Client.Common.csproj index 5da4f7f414..9816bf2bfb 100644 --- a/BizHawk.Client.Common/BizHawk.Client.Common.csproj +++ b/BizHawk.Client.Common/BizHawk.Client.Common.csproj @@ -184,6 +184,7 @@ + @@ -330,4 +331,4 @@ --> - + \ No newline at end of file diff --git a/BizHawk.Client.Common/movie/tasproj/StateManagerDecay.cs b/BizHawk.Client.Common/movie/tasproj/StateManagerDecay.cs new file mode 100644 index 0000000000..da1ec21b27 --- /dev/null +++ b/BizHawk.Client.Common/movie/tasproj/StateManagerDecay.cs @@ -0,0 +1,167 @@ +/**************************************************************************************** + + Algorithm by r57shell & feos, 2018 + + _zeros is the key to GREENZONE DECAY PATTERN. + + In a 16 element example, we evaluate these bitwise numbers to count zeros on the right. + First element is always assumed to be 16, which has all 4 bits set to 0. Each right zero + means that we lower the priority of a state that goes at that index. Priority changes + depending on current frame and amount of states. States with biggest priority get erased + first. With a 4-bit battern and no initial gap between states, total frame coverage is + about 5 times state count. Initial state gap can screw up our patterns, so do all + calculations like gap isn't there, and take it back into account afterwards. + + _zeros values are essentialy the values of rshiftby here: + bitwise view frame rshiftby priority + 00010000 0 4 1 + 00000001 1 0 15 + 00000010 2 1 7 + 00000011 3 0 13 + 00000100 4 2 3 + 00000101 5 0 11 + 00000110 6 1 5 + 00000111 7 0 9 + 00001000 8 3 1 + 00001001 9 0 7 + 00001010 10 1 3 + 00001011 11 0 5 + 00001100 12 2 1 + 00001101 13 0 3 + 00001110 14 1 1 + 00001111 15 0 1 + +*****************************************************************************************/ +using System.Collections.Generic; + +namespace BizHawk.Client.Common +{ + internal class StateManagerDecay + { + private TasStateManager _tsm; // access tsm methods to make life easier + private List _zeros; // amount of least significant zeros in bitwise view (also max pattern step) + private int _bits; // size of _zeros is 2 raised to the power of _bits + private int _mask; // for remainder calculation using bitwise instead of division + private int _base; // repeat count (like fceux's capacity). only used by aligned formula + private int _capacity; // total amount of savestates + private int _step; // initial memory state gap + private bool _align; // extra care about fine alignment. TODO: do we want it? + + public StateManagerDecay(TasStateManager tsm) + { + _tsm = tsm; + _align = false; + } + + public void Trigger(int decayStates) + { + for (; decayStates > 0 && _tsm.StateCount > 1;) + { + int baseStateIndex = _tsm.GetStateIndexByFrame(Global.Emulator.Frame); + int baseStateFrame = _tsm.GetStateFrameByIndex(baseStateIndex) / _step; + int forwardPriority = -1000000; + int backwardPriority = -1000000; + int forwardFrame = -1; + int backwardFrame = -1; + + for (int currentStateIndex = 1; currentStateIndex < baseStateIndex; currentStateIndex++) + { + int currentFrame = _tsm.GetStateFrameByIndex(currentStateIndex) / _step; + + if (_tsm.StateIsMarker(currentFrame * _step)) + { + continue; + } + + int zeroCount = _zeros[currentFrame & _mask]; + int priority = ((baseStateFrame - currentFrame) >> zeroCount); + + if (_align) + { + priority -= ((_base * ((1 << zeroCount) * 2 - 1)) >> zeroCount); + } + + if (priority > forwardPriority) + { + forwardPriority = priority; + forwardFrame = currentFrame; + } + } + + for (int currentStateIndex = _tsm.StateCount - 1; currentStateIndex > baseStateIndex; currentStateIndex--) + { + int currentFrame = _tsm.GetStateFrameByIndex(currentStateIndex) / _step; + + if (_tsm.StateIsMarker(currentFrame * _step)) + { + continue; + } + + int zeroCount = _zeros[currentFrame & _mask]; + int priority = ((currentFrame - baseStateFrame) >> zeroCount); + + if (_align) + { + priority -= ((_base * ((1 << zeroCount) * 2 - 1)) >> zeroCount); + } + + if (priority > backwardPriority) + { + backwardPriority = priority; + backwardFrame = currentFrame; + } + } + + if (forwardFrame > -1 && backwardFrame > -1) + { + if (baseStateFrame - forwardFrame > backwardFrame - baseStateFrame) + { + _tsm.RemoveState(forwardFrame * _step); + } + else + { + _tsm.RemoveState(backwardFrame * _step); + } + + decayStates--; + } + else if (forwardFrame > -1) + { + _tsm.RemoveState(forwardFrame * _step); + decayStates--; + } + else if (backwardFrame > -1) + { + _tsm.RemoveState(backwardFrame * _step); + decayStates--; + } + } + } + + public void UpdateSettings(int capacity, int step, int bits) + { + _capacity = capacity; + _step = step; + _bits = bits; + _mask = (1 << _bits) - 1; + _base = (_capacity + _bits / 2) / (_bits + 1); + _zeros = new List(); + _zeros.Add(_bits); + + for (int i = 1; i < (1 << _bits); i++) + { + _zeros.Add(0); + + for (int j = i; j > 0; j >>= 1) + { + if ((j & 1) > 0) + { + break; + } + + _zeros[i]++; + } + } + } + } +} diff --git a/BizHawk.Client.Common/movie/tasproj/TasStateManager.cs b/BizHawk.Client.Common/movie/tasproj/TasStateManager.cs index d549cb9aab..cd1ff55c1c 100644 --- a/BizHawk.Client.Common/movie/tasproj/TasStateManager.cs +++ b/BizHawk.Client.Common/movie/tasproj/TasStateManager.cs @@ -26,7 +26,7 @@ namespace BizHawk.Client.Common { InvalidateCallback?.Invoke(index); } - + internal NDBDatabase NdbDatabase { get; set; } private Guid _guid = Guid.NewGuid(); private SortedList _states = new SortedList(); @@ -40,12 +40,10 @@ namespace BizHawk.Client.Common } } - private long _stateCleanupTime; - private readonly long _stateCleanupPeriod = 10000; - private bool _isMountedForWrite; private readonly TasMovie _movie; + private StateManagerDecay _decay; private ulong _expectedStateSize; private int _stateFrequency; private readonly int _minFrequency = 1; @@ -53,7 +51,6 @@ namespace BizHawk.Client.Common private int _maxStates => (int)(Settings.Cap / _expectedStateSize) + (int)((ulong)Settings.DiskCapacitymb * 1024 * 1024 / _expectedStateSize); private int _fileStateGap => 1 << Settings.FileStateGap; - private int _greenzoneDecayCall = 0; public TasStateManager(TasMovie movie) { @@ -65,7 +62,7 @@ namespace BizHawk.Client.Common SetState(0, _movie.BinarySavestate); } - _stateCleanupTime = DateTime.Now.Ticks + _stateCleanupPeriod; + _decay = new StateManagerDecay(this); } public void Dispose() @@ -79,6 +76,8 @@ namespace BizHawk.Client.Common _stateFrequency = NumberExtensions.Clamp( ((int)_expectedStateSize / Settings.MemStateGapDivider / 1024), _minFrequency, _maxFrequency); + + _decay.UpdateSettings(_maxStates, _stateFrequency, 4); } /// @@ -110,7 +109,7 @@ namespace BizHawk.Client.Common NdbDatabase = new NDBDatabase(StatePath, Settings.DiskCapacitymb * 1024 * 1024, (int)_expectedStateSize); } - + public TasStateManagerSettings Settings { get; set; } /// @@ -156,8 +155,8 @@ namespace BizHawk.Client.Common public void Capture(bool force = false) { bool shouldCapture; - 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; @@ -170,7 +169,7 @@ namespace BizHawk.Client.Common { shouldCapture = true; } - else if (_movie.Markers.IsMarker(frame + 1)) + else if (StateIsMarker(frame)) { shouldCapture = true; // Markers shoudl always get priority } @@ -253,7 +252,7 @@ namespace BizHawk.Client.Common return anyInvalidated; } - private bool StateIsMarker(int frame) + public bool StateIsMarker(int frame) { if (frame == -1) { @@ -263,8 +262,8 @@ namespace BizHawk.Client.Common return _movie.Markers.IsMarker(frame + 1); } - private void RemoveState(int frame) - { + public void RemoveState(int frame) + { int index = _states.IndexOfKey(frame); if (frame < 1 || index < 1) @@ -292,89 +291,9 @@ namespace BizHawk.Client.Common /// public void LimitStateCount() { - if (Used + _expectedStateSize > Settings.Cap || DiskUsed > (ulong)Settings.DiskCapacitymb * 1024 * 1024) + if (StateCount + 1 > _maxStates || DiskUsed > (ulong)Settings.DiskCapacitymb * 1024 * 1024) { - // feos: this GREENZONE DECAY algo is critically important (and crazy), so I'll explain it fully here - // we force decay gap between memory-based states that increases for every new region - // regions start from the state right above the current frame (or right below for forward decay) - // we use powers of 2 to determine decay gap size and region length - // amount of regions and their lengths depend on how many powers of 2 we want to use - // we use 5 powers of 2, from 0 to 4. decay gap goes 0, 1, 3, 7, 15 (in reality, not perfectly so) - // 1 decay gap unit is 1 frame * minimal state frequency - // first region has no decay gaps, the length of that region in fceux is called "greenzone capacity" - // every next region is twice longer than its predecessor, but it has the same amount of states (approximately) - // states beyond last region are erased, except for state at frame 0 - // algo works in both directions, alternating between them on every call - // it removes as many states is its pattern needs, which allows for cooldown before cap is about to get hit again - // todo: this is still imperfect, even though probably usable already - - _greenzoneDecayCall++; - - int regionStates = _maxStates / 5; - int baseIndex = GetStateIndexByFrame(Global.Emulator.Frame); - int direction = 1; // negative for forward decay - - if (_greenzoneDecayCall % 2 == 0) - { - baseIndex++; - direction = -1; - } - - int lastStateFrame = -1; - - for (int mult = 2, currentStateIndex = baseIndex - regionStates * direction; mult <= 16; mult *= 2) - { - int gap = _stateFrequency * mult; - int regionFrames = regionStates * gap; - - for (; ; currentStateIndex -= direction) - { - // are we out of states yet? - if (direction > 0 && currentStateIndex <= 1 || - direction < 0 && currentStateIndex >= _states.Count - 1) - return; - - int nextStateIndex = currentStateIndex - direction; - NumberExtensions.Clamp(nextStateIndex, 1, _states.Count - 1); - - int currentStateFrame = GetStateFrameByIndex(currentStateIndex); - int nextStateFrame = GetStateFrameByIndex(nextStateIndex); - int frameDiff = Math.Max(currentStateFrame, nextStateFrame) - Math.Min(currentStateFrame, nextStateFrame); - lastStateFrame = currentStateFrame; - - if (frameDiff < gap) - { - RemoveState(nextStateFrame); - - // when going forward, we don't remove the state before current - // but current changes anyway, so compensate for that here - if (direction < 0) - currentStateIndex--; - } - else - { - regionFrames -= frameDiff; - if (regionFrames <= 0) - break; - } - } - } - - // finish off whatever we've missed - if (lastStateFrame > -1) - { - List> leftoverStates; - - if (direction > 0) - leftoverStates = _states.Where(s => s.Key > 0 && s.Key < lastStateFrame).ToList(); - else - leftoverStates = _states.Where(s => s.Key > lastStateFrame && s.Key < LastEmulatedFrame).ToList(); - - foreach (var state in leftoverStates) - { - RemoveState(state.Key); - } - } + _decay.Trigger(StateCount + 1 - _maxStates); } } @@ -387,21 +306,22 @@ namespace BizHawk.Client.Common // 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 % _fileStateGap == 0) + int frame = GetStateFrameByIndex(i); + + if (StateIsMarker(frame) || frame % _fileStateGap < _stateFrequency) { continue; } ret.Add(i); - if (_states.ElementAt(i).Value.IsOnDisk) + if (_states.Values[i].IsOnDisk) { saveUsed -= _expectedStateSize; } else { - saveUsed -= (ulong)_states.ElementAt(i).Value.Length; + saveUsed -= (ulong)_states.Values[i].Length; } } @@ -412,13 +332,12 @@ namespace BizHawk.Client.Common { do { - index++; - if (index >= _states.Count) + if (++index >= _states.Count) { break; } } - while (_movie.Markers.IsMarker(_states.ElementAt(index).Key + 1)); + while (StateIsMarker(GetStateFrameByIndex(index))); if (index >= _states.Count) { @@ -427,13 +346,13 @@ namespace BizHawk.Client.Common ret.Add(index); - if (_states.ElementAt(index).Value.IsOnDisk) + if (_states.Values[index].IsOnDisk) { saveUsed -= _expectedStateSize; } else { - saveUsed -= (ulong)_states.ElementAt(index).Value.Length; + saveUsed -= (ulong)_states.Values[index].Length; } } @@ -441,19 +360,18 @@ namespace BizHawk.Client.Common index = 0; while (saveUsed > (ulong)Settings.DiskSaveCapacitymb * 1024 * 1024) { - index++; - if (!ret.Contains(index)) + if (!ret.Contains(++index)) { ret.Add(index); } - if (_states.ElementAt(index).Value.IsOnDisk) + if (_states.Values[index].IsOnDisk) { saveUsed -= _expectedStateSize; } else { - saveUsed -= (ulong)_states.ElementAt(index).Value.Length; + saveUsed -= (ulong)_states.Values[index].Length; } } @@ -472,18 +390,24 @@ namespace BizHawk.Client.Common } } + // 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; } - + KeyValuePair kvp = _states.ElementAt(i); bw.Write(kvp.Key); bw.Write(kvp.Value.Length); @@ -491,18 +415,14 @@ namespace BizHawk.Client.Common } } - // Map: - // 4 bytes - total savestate count - // [Foreach state] - // 4 bytes - frame - // 4 bytes - length of savestate - // 0 - n savestate public void Load(BinaryReader br) { _states.Clear(); + try { int nstates = br.ReadInt32(); + for (int i = 0; i < nstates; i++) { int frame = br.ReadInt32(); @@ -543,7 +463,9 @@ namespace BizHawk.Client.Common /// public int GetStateFrameByIndex(int index) { - return _states.ElementAt(index).Key; + // feos: this is called super often by decay + // this method is hundred times faster than _states.ElementAt(index).Key + return _states.Keys[index]; } private ulong _used; @@ -606,7 +528,7 @@ namespace BizHawk.Client.Common } } - public int LastEmulatedFrame + public int LastStatedFrame { get { diff --git a/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs b/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs index 1f9cb3447a..e7f5d1156f 100644 --- a/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs +++ b/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs @@ -764,7 +764,7 @@ namespace BizHawk.Client.EmuHawk GoToFrame(0); int lastState = 0; - int goToFrame = CurrentTasMovie.TasStateManager.LastEmulatedFrame; + int goToFrame = CurrentTasMovie.TasStateManager.LastStatedFrame; do { Mainform.FrameAdvance(); diff --git a/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs b/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs index 82247a2f96..da7fa2c0dd 100644 --- a/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs +++ b/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs @@ -807,6 +807,7 @@ namespace BizHawk.Client.EmuHawk TasView.Refresh(); + //SetSplicer(); CurrentTasMovie.FlushInputCache(); CurrentTasMovie.UseInputCache = false; @@ -939,6 +940,7 @@ namespace BizHawk.Client.EmuHawk SplicerStatusLabel.Text = "Selected: " + TasView.SelectedRows.Count() + " frame" + (TasView.SelectedRows.Count() == 1 ? "" : "s") + + //", State count: " + CurrentTasMovie.TasStateManager.StateCount.ToString() + ", Clipboard: " + (_tasClipboard.Any() ? _tasClipboard.Count + " frame" + (_tasClipboard.Count == 1 ? "" : "s") : "empty"); }