State manager decay (#1128)
* Finally use exponential decay algorithm for greenzone * Works in both directions (unlike fceux taseditor) * Stop using last accesses states for anything * When dropping states per project save, account for state gap better * Use _states.Keys[i] instead of _states.ElementAt{i).Key in speedy parts, because it's apparently tons faster * Use StateIsMarker() where it's supposed to be used * Markers are still left untouched * Branch states are still dead (probably forever)
This commit is contained in:
parent
9804a0901a
commit
bf8c21663d
|
@ -184,6 +184,7 @@
|
|||
</Compile>
|
||||
<Compile Include="movie\bk2\StringLogs.cs" />
|
||||
<Compile Include="movie\import\PXMImport.cs" />
|
||||
<Compile Include="movie\tasproj\StateManagerDecay.cs" />
|
||||
<Compile Include="movie\tasproj\StateManagerState.cs" />
|
||||
<Compile Include="movie\tasproj\TasBranch.cs" />
|
||||
<Compile Include="movie\tasproj\TasMovie.History.cs" />
|
||||
|
@ -330,4 +331,4 @@
|
|||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
</Project>
|
||||
</Project>
|
|
@ -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<int> _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<int>();
|
||||
_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]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
InvalidateCallback?.Invoke(index);
|
||||
}
|
||||
|
||||
|
||||
internal NDBDatabase NdbDatabase { get; set; }
|
||||
private Guid _guid = Guid.NewGuid();
|
||||
private SortedList<int, StateManagerState> _states = new SortedList<int, StateManagerState>();
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -110,7 +109,7 @@ namespace BizHawk.Client.Common
|
|||
|
||||
NdbDatabase = new NDBDatabase(StatePath, Settings.DiskCapacitymb * 1024 * 1024, (int)_expectedStateSize);
|
||||
}
|
||||
|
||||
|
||||
public TasStateManagerSettings Settings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
@ -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
|
|||
/// </summary>
|
||||
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<KeyValuePair<int, StateManagerState>> 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<int> noSave = ExcludeStates();
|
||||
|
||||
bw.Write(_states.Count - noSave.Count);
|
||||
|
||||
for (int i = 0; i < _states.Count; i++)
|
||||
{
|
||||
if (noSave.Contains(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
KeyValuePair<int, StateManagerState> 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
|
|||
/// <returns></returns>
|
||||
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
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue