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");
}