Majorly refactor ZwinderStatemanager to address # 2287 (#2321)
* rename highpriority to ReGreenZone, and add a comment, to better document what it is used for * rename again, from regreenzone to gapFiller, rename settings too, make gap frame length 1000 instead of 10000 * oops * merge original state with ancient, since we can never invalidate frame 0 anyway, it can safely be stored here * unremove nonstate * change ancient to reserved, in preparation for marker and branch states to go here, add more comments * capture branch states as reserved, reconsider gap logic to account for the fact that a reserved state might be greater than the last current/recent state * do not capture to reserved states if the state is already rreserved * add a callback to check if a state is "reserved", client code wil return whether it is a branch or marker state. Wire up reserved logic into eviction logic. If reserved, go to reserved list, else evict * add API for evicting reserved states, and wire it up to marker removal * just in case * a bit of renaming, add a unit test for an edge case that was broken with the Last property, add unit tests to cover it * Revert "a bit of renaming, add a unit test for an edge case that was broken with the Last property, add unit tests to cover it" This reverts commit b0d01ffacb058eb26c68a7fdccb0010d3bca40b2. * fix AllStates using Concat() and OrderBy(), add unit tests for HasState and GetStateClosestToFrame() * Fix InvalidateAfter and add tests * make HasState() a lot faster * durp * convert reserved to a Dictionary * fix count being off by 1 due to no longer correct assumption of there being a separate frame zero state * a few cleanups * clean up tests and use less ram, fix a few things that I broke that unit tests caught, yay unit tests * implement IDisposable and use in unit tests * fix SaveCreateroundTrip (for me at least), by using a smaller buffer allocation, also be pedantic and use zw.Settings in zw2 to ensure they match * some tests for Count * attempt to cache which states have frames, doesn't work, ZwinderBuffer on the last state before it wraps, doesn't behave as I expect, dunno if it is intended * fix typo when evicting recent to reserved, cleanups, make unit test work * oops * cleanup and account for Gaps in unit test * use StateCache for HasState, fix unit test accordingly * use statecache to check if a frame exists during Capture, and do this first, before gap logic * fix reserved logic in Clear, add a unit test for Clear * fix Engage bug that was breaking loading movies, remove CaptureReserved from the API and instead, call the reserved callback in Capture * use state.Size to minimize memory thrashing in AddToReserved() * cleanup some comments * when loading a tasproj from disk, build up the state cache, without this commit, loading an existing movie was unuseable * reserve the frame before markers, not hte marker itself, users expect instant navigation to markers, and since we always navigate 1 frame before the target frame to emulate and get a frame buffer, this is the frame that must be reserved
This commit is contained in:
parent
fb6924bd83
commit
1b0139ebc3
|
@ -22,6 +22,11 @@ namespace BizHawk.Client.Common
|
|||
/// </summary>
|
||||
void Capture(int frame, IStatable source, bool force = false);
|
||||
|
||||
/// <summary>
|
||||
/// Commands the state manager to remove a reserved state for the given frame, if it is exists
|
||||
/// </summary>
|
||||
void EvictReserved(int frame);
|
||||
|
||||
bool HasState(int frame);
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -167,7 +167,7 @@ namespace BizHawk.Client.Common
|
|||
|
||||
bl.GetLump(BinaryStateLump.StateHistory, false, delegate(BinaryReader br, long length)
|
||||
{
|
||||
TasStateManager = ZwinderStateManager.Create(br, TasStateManager.Settings);
|
||||
TasStateManager = ZwinderStateManager.Create(br, TasStateManager.Settings, IsReserved);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
|
||||
using System.Linq;
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
|
@ -23,7 +23,7 @@ namespace BizHawk.Client.Common
|
|||
Markers = new TasMovieMarkerList(this);
|
||||
Markers.CollectionChanged += Markers_CollectionChanged;
|
||||
Markers.Add(0, "Power on");
|
||||
TasStateManager = new ZwinderStateManager();
|
||||
TasStateManager = new ZwinderStateManager(IsReserved);
|
||||
}
|
||||
|
||||
public override void Attach(IEmulator emulator)
|
||||
|
@ -338,5 +338,15 @@ namespace BizHawk.Client.Common
|
|||
|
||||
public void ClearChanges() => Changes = false;
|
||||
public void FlagChanges() => Changes = true;
|
||||
|
||||
private bool IsReserved(int frame)
|
||||
{
|
||||
|
||||
// Why the frame before?
|
||||
// because we always navigate to the frame before and emulate 1 frame so that we ensure a proper frame buffer on the screen
|
||||
// users want instant navigation to markers, so to do this, we need to reserve the frame before the marker, not the marker itself
|
||||
return Markers.Any(m => m.Frame - 1 == frame)
|
||||
|| Branches.Any(b => b.Frame == frame); // Branches should already be in the reserved list, but it doesn't hurt to check
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -206,6 +206,7 @@ namespace BizHawk.Client.Common
|
|||
return;
|
||||
}
|
||||
|
||||
_movie.TasStateManager.EvictReserved(item.Frame);
|
||||
_movie.ChangeLog.AddMarkerChange(null, item.Frame, item.Message);
|
||||
|
||||
base.Remove(item);
|
||||
|
@ -220,6 +221,7 @@ namespace BizHawk.Client.Common
|
|||
if (match.Invoke(m))
|
||||
{
|
||||
_movie.ChangeLog.AddMarkerChange(null, m.Frame, m.Message);
|
||||
_movie.TasStateManager.EvictReserved(m.Frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,18 +6,28 @@ using BizHawk.Emulation.Common;
|
|||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
public class ZwinderStateManager : IStateManager
|
||||
public class ZwinderStateManager : IStateManager, IDisposable
|
||||
{
|
||||
private static readonly byte[] NonState = new byte[0];
|
||||
|
||||
private byte[] _originalState;
|
||||
private readonly ZwinderBuffer _current;
|
||||
private readonly ZwinderBuffer _recent;
|
||||
private readonly ZwinderBuffer _highPriority;
|
||||
private readonly List<KeyValuePair<int, byte[]>> _ancient = new List<KeyValuePair<int, byte[]>>();
|
||||
private readonly Func<int, bool> _reserveCallback;
|
||||
internal readonly SortedSet<int> StateCache = new SortedSet<int>();
|
||||
private ZwinderBuffer _current;
|
||||
private ZwinderBuffer _recent;
|
||||
|
||||
// Used to re-fill gaps when still replaying input, but in a non-current area, also needed when switching branches
|
||||
private ZwinderBuffer _gapFiller;
|
||||
|
||||
// These never decay, but can be invalidated, they are for reserved states
|
||||
// such as markers and branches, but also we naturally evict states from recent to reserved, based
|
||||
// on _ancientInterval
|
||||
private Dictionary<int, byte[]> _reserved = new Dictionary<int, byte[]>();
|
||||
|
||||
// When recent states are evicted this interval is used to determine if we need to reserve the state
|
||||
// We always want to keep some states throughout the movie
|
||||
private readonly int _ancientInterval;
|
||||
|
||||
public ZwinderStateManager(ZwinderStateManagerSettings settings)
|
||||
internal ZwinderStateManager(ZwinderStateManagerSettings settings, Func<int, bool> reserveCallback)
|
||||
{
|
||||
Settings = settings;
|
||||
|
||||
|
@ -34,34 +44,39 @@ namespace BizHawk.Client.Common
|
|||
TargetFrameLength = settings.RecentTargetFrameLength
|
||||
});
|
||||
|
||||
_highPriority = new ZwinderBuffer(new RewindConfig
|
||||
_gapFiller = new ZwinderBuffer(new RewindConfig
|
||||
{
|
||||
UseCompression = settings.PriorityUseCompression,
|
||||
BufferSize = settings.PriorityBufferSize,
|
||||
TargetFrameLength = settings.PriorityTargetFrameLength
|
||||
UseCompression = settings.GapsUseCompression,
|
||||
BufferSize = settings.GapsBufferSize,
|
||||
TargetFrameLength = settings.GapsTargetFrameLength
|
||||
});
|
||||
|
||||
_ancientInterval = settings.AncientStateInterval;
|
||||
_originalState = NonState;
|
||||
_reserveCallback = reserveCallback;
|
||||
}
|
||||
|
||||
public ZwinderStateManager()
|
||||
:this(new ZwinderStateManagerSettings())
|
||||
/// <param name="reserveCallback">Called when deciding to evict a state for the given frame, if true is returned, the state will be reserved</param>
|
||||
public ZwinderStateManager(Func<int, bool> reserveCallback)
|
||||
: this(new ZwinderStateManagerSettings(), reserveCallback)
|
||||
{
|
||||
}
|
||||
|
||||
public void Engage(byte[] frameZeroState)
|
||||
{
|
||||
_originalState = (byte[])frameZeroState.Clone();
|
||||
if (!_reserved.ContainsKey(0))
|
||||
{
|
||||
_reserved.Add(0, frameZeroState);
|
||||
StateCache.Add(0);
|
||||
}
|
||||
}
|
||||
|
||||
private ZwinderStateManager(ZwinderBuffer current, ZwinderBuffer recent, ZwinderBuffer highPriority, byte[] frameZeroState, int ancientInterval)
|
||||
private ZwinderStateManager(ZwinderBuffer current, ZwinderBuffer recent, ZwinderBuffer gapFiller, int ancientInterval, Func<int, bool> reserveCallback)
|
||||
{
|
||||
_originalState = (byte[])frameZeroState.Clone();
|
||||
_current = current;
|
||||
_recent = recent;
|
||||
_highPriority = highPriority;
|
||||
_gapFiller = gapFiller;
|
||||
_ancientInterval = ancientInterval;
|
||||
_reserveCallback = reserveCallback;
|
||||
}
|
||||
|
||||
public byte[] this[int frame]
|
||||
|
@ -70,7 +85,10 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
var kvp = GetStateClosestToFrame(frame);
|
||||
if (kvp.Key != frame)
|
||||
{
|
||||
return NonState;
|
||||
}
|
||||
|
||||
var ms = new MemoryStream();
|
||||
kvp.Value.CopyTo(ms);
|
||||
return ms.ToArray();
|
||||
|
@ -80,9 +98,9 @@ namespace BizHawk.Client.Common
|
|||
// TODO: private set, refactor LoadTasprojExtras to hold onto a settings object and pass it in to Create() method
|
||||
public ZwinderStateManagerSettings Settings { get; set; }
|
||||
|
||||
public int Count => _current.Count + _recent.Count + _highPriority.Count + _ancient.Count + 1;
|
||||
public int Count => _current.Count + _recent.Count + _gapFiller.Count + _reserved.Count;
|
||||
|
||||
private class StateInfo
|
||||
internal class StateInfo
|
||||
{
|
||||
public int Frame { get; }
|
||||
public Func<Stream> Read { get; }
|
||||
|
@ -91,10 +109,7 @@ namespace BizHawk.Client.Common
|
|||
Frame = si.Frame;
|
||||
Read = si.GetReadStream;
|
||||
}
|
||||
public StateInfo(KeyValuePair<int, byte[]> kvp)
|
||||
:this(kvp.Key, kvp.Value)
|
||||
{
|
||||
}
|
||||
|
||||
public StateInfo(int frame, byte[] data)
|
||||
{
|
||||
Frame = frame;
|
||||
|
@ -102,11 +117,8 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate all states, excepting high priority, in reverse order
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<StateInfo> NormalStates()
|
||||
// Enumerate all current and recent states in reverse order
|
||||
private IEnumerable<StateInfo> CurrentAndRecentStates()
|
||||
{
|
||||
for (var i = _current.Count - 1; i >= 0; i--)
|
||||
{
|
||||
|
@ -116,108 +128,165 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
yield return new StateInfo(_recent.GetState(i));
|
||||
}
|
||||
for (var i = _ancient.Count - 1; i >= 0; i--)
|
||||
{
|
||||
yield return new StateInfo(_ancient[i]);
|
||||
}
|
||||
yield return new StateInfo(0, _originalState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate high priority states in reverse order
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<StateInfo> HighPriorityStates()
|
||||
// Enumerate all gap states in reverse order
|
||||
private IEnumerable<StateInfo> GapStates()
|
||||
{
|
||||
for (var i = _highPriority.Count - 1; i >= 0; i--)
|
||||
for (var i = _gapFiller.Count - 1; i >= 0; i--)
|
||||
{
|
||||
yield return new StateInfo(_highPriority.GetState(i));
|
||||
yield return new StateInfo(_gapFiller.GetState(i));
|
||||
}
|
||||
}
|
||||
|
||||
// Enumerate all reserved states in reverse order
|
||||
private IEnumerable<StateInfo> ReservedStates()
|
||||
{
|
||||
foreach (var key in _reserved.Keys.OrderByDescending(k => k))
|
||||
{
|
||||
yield return new StateInfo(key, _reserved[key]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enumerate all states in reverse order
|
||||
/// </summary>
|
||||
private IEnumerable<StateInfo> AllStates()
|
||||
internal IEnumerable<StateInfo> AllStates()
|
||||
{
|
||||
var l1 = NormalStates().GetEnumerator();
|
||||
var l2 = HighPriorityStates().GetEnumerator();
|
||||
var l1More = l1.MoveNext();
|
||||
var l2More = l2.MoveNext();
|
||||
while (l1More || l2More)
|
||||
{
|
||||
if (l1More)
|
||||
{
|
||||
if (l2More)
|
||||
{
|
||||
if (l1.Current.Frame > l2.Current.Frame)
|
||||
{
|
||||
yield return l1.Current;
|
||||
l1More = l1.MoveNext();
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return l2.Current;
|
||||
l2More = l2.MoveNext();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return l1.Current;
|
||||
l1More = l1.MoveNext();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return l2.Current;
|
||||
l2More = l2.MoveNext();
|
||||
}
|
||||
}
|
||||
return CurrentAndRecentStates()
|
||||
.Concat(GapStates())
|
||||
.Concat(ReservedStates())
|
||||
.OrderByDescending(s => s.Frame);
|
||||
}
|
||||
|
||||
public int Last => AllStates().First().Frame;
|
||||
|
||||
private int LastRing => CurrentAndRecentStates().FirstOrDefault()?.Frame ?? 0;
|
||||
|
||||
internal void CaptureReserved(int frame, IStatable source)
|
||||
{
|
||||
if (_reserved.ContainsKey(frame))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var ms = new MemoryStream();
|
||||
source.SaveStateBinary(new BinaryWriter(ms));
|
||||
_reserved.Add(frame, ms.ToArray());
|
||||
StateCache.Add(frame);
|
||||
}
|
||||
|
||||
private void AddToReserved(ZwinderBuffer.StateInformation state)
|
||||
{
|
||||
if (_reserved.ContainsKey(state.Frame))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bb = new byte[state.Size];
|
||||
var ms = new MemoryStream(bb);
|
||||
state.GetReadStream().CopyTo(ms);
|
||||
_reserved.Add(state.Frame, bb);
|
||||
StateCache.Add(state.Frame);
|
||||
}
|
||||
|
||||
public void EvictReserved(int frame)
|
||||
{
|
||||
if (frame == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Frame 0 can not be evicted.");
|
||||
}
|
||||
|
||||
_reserved.Remove(frame);
|
||||
StateCache.Remove(frame);
|
||||
}
|
||||
|
||||
public void Capture(int frame, IStatable source, bool force = false)
|
||||
{
|
||||
if (frame <= Last)
|
||||
// We already have this state, no need to capture
|
||||
if (StateCache.Contains(frame))
|
||||
{
|
||||
CaptureHighPriority(frame, source);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_reserveCallback(frame))
|
||||
{
|
||||
CaptureReserved(frame, source);
|
||||
return;
|
||||
}
|
||||
|
||||
// We do not want to consider reserved states for a notion of Last
|
||||
// reserved states can include future states in the case of branch states
|
||||
if (frame <= LastRing)
|
||||
{
|
||||
CaptureGap(frame, source);
|
||||
return;
|
||||
}
|
||||
|
||||
_current.Capture(frame,
|
||||
s => source.SaveStateBinary(new BinaryWriter(s)),
|
||||
s =>
|
||||
{
|
||||
source.SaveStateBinary(new BinaryWriter(s));
|
||||
StateCache.Add(frame);
|
||||
},
|
||||
index =>
|
||||
{
|
||||
var state = _current.GetState(index);
|
||||
StateCache.Remove(state.Frame);
|
||||
|
||||
// If this is a reserved state, go ahead and reserve instead of potentially trying to force it into recent, for further eviction logic later
|
||||
if (_reserveCallback(state.Frame))
|
||||
{
|
||||
AddToReserved(state);
|
||||
return;
|
||||
}
|
||||
|
||||
_recent.Capture(state.Frame,
|
||||
s => state.GetReadStream().CopyTo(s),
|
||||
s =>
|
||||
{
|
||||
state.GetReadStream().CopyTo(s);
|
||||
StateCache.Add(state.Frame);
|
||||
},
|
||||
index2 =>
|
||||
{
|
||||
var state2 = _recent.GetState(index2);
|
||||
var from = _ancient.Count > 0 ? _ancient[_ancient.Count - 1].Key : 0;
|
||||
if (state2.Frame - from >= _ancientInterval)
|
||||
StateCache.Remove(state2.Frame);
|
||||
|
||||
var from = _reserved.Count > 0 ? _reserved.Max(kvp => kvp.Key) : 0;
|
||||
|
||||
var isReserved = _reserveCallback(state2.Frame);
|
||||
|
||||
// Add to reserved if reserved, or if it matches an "ancient" state consideration
|
||||
if (isReserved || state2.Frame - from >= _ancientInterval)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
state2.GetReadStream().CopyTo(ms);
|
||||
_ancient.Add(new KeyValuePair<int, byte[]>(state2.Frame, ms.ToArray()));
|
||||
AddToReserved(state2);
|
||||
}
|
||||
});
|
||||
},
|
||||
force);
|
||||
}
|
||||
|
||||
public void CaptureHighPriority(int frame, IStatable source)
|
||||
private void CaptureGap(int frame, IStatable source)
|
||||
{
|
||||
_highPriority.Capture(frame, s => source.SaveStateBinary(new BinaryWriter(s)));
|
||||
_gapFiller.Capture(
|
||||
frame, s =>
|
||||
{
|
||||
StateCache.Add(frame);
|
||||
source.SaveStateBinary(new BinaryWriter(s));
|
||||
},
|
||||
index => StateCache.Remove(index));
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_current.InvalidateEnd(0);
|
||||
_recent.InvalidateEnd(0);
|
||||
_highPriority.InvalidateEnd(0);
|
||||
_ancient.Clear();
|
||||
_gapFiller.InvalidateEnd(0);
|
||||
StateCache.Clear();
|
||||
StateCache.Add(0);
|
||||
_reserved = _reserved
|
||||
.Where(kvp => kvp.Key == 0)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
}
|
||||
|
||||
public KeyValuePair<int, Stream> GetStateClosestToFrame(int frame)
|
||||
|
@ -231,16 +300,16 @@ namespace BizHawk.Client.Common
|
|||
|
||||
public bool HasState(int frame)
|
||||
{
|
||||
return AllStates().Any(s => s.Frame == frame);
|
||||
return StateCache.Contains(frame);
|
||||
}
|
||||
|
||||
private bool InvalidateHighPriority(int frame)
|
||||
private bool InvalidateGaps(int frame)
|
||||
{
|
||||
for (var i = 0; i < _highPriority.Count; i++)
|
||||
for (var i = 0; i < _gapFiller.Count; i++)
|
||||
{
|
||||
if (_highPriority.GetState(i).Frame > frame)
|
||||
if (_gapFiller.GetState(i).Frame > frame)
|
||||
{
|
||||
_highPriority.InvalidateEnd(i);
|
||||
_gapFiller.InvalidateEnd(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -249,16 +318,6 @@ namespace BizHawk.Client.Common
|
|||
|
||||
private bool InvalidateNormal(int frame)
|
||||
{
|
||||
for (var i = 0; i < _ancient.Count; i++)
|
||||
{
|
||||
if (_ancient[i].Key > frame)
|
||||
{
|
||||
_ancient.RemoveRange(i, _ancient.Count - i);
|
||||
_recent.InvalidateEnd(0);
|
||||
_current.InvalidateEnd(0);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < _recent.Count; i++)
|
||||
{
|
||||
if (_recent.GetState(i).Frame > frame)
|
||||
|
@ -268,6 +327,7 @@ namespace BizHawk.Client.Common
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < _current.Count; i++)
|
||||
{
|
||||
if (_current.GetState(i).Frame > frame)
|
||||
|
@ -279,6 +339,16 @@ namespace BizHawk.Client.Common
|
|||
return false;
|
||||
}
|
||||
|
||||
private bool InvalidateReserved(int frame)
|
||||
{
|
||||
var origCount = _reserved.Count;
|
||||
_reserved = _reserved
|
||||
.Where(kvp => kvp.Key <= frame)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
return _reserved.Count < origCount;
|
||||
}
|
||||
|
||||
public void UpdateSettings(ZwinderStateManagerSettings settings) => Settings = settings;
|
||||
|
||||
public bool InvalidateAfter(int frame)
|
||||
|
@ -286,21 +356,21 @@ namespace BizHawk.Client.Common
|
|||
if (frame < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(frame));
|
||||
var b1 = InvalidateNormal(frame);
|
||||
var b2 = InvalidateHighPriority(frame);
|
||||
return b1 || b2;
|
||||
var b2 = InvalidateGaps(frame);
|
||||
var b3 = InvalidateReserved(frame);
|
||||
StateCache.RemoveWhere(s => s > frame);
|
||||
return b1 || b2 || b3;
|
||||
}
|
||||
|
||||
public static ZwinderStateManager Create(BinaryReader br, ZwinderStateManagerSettings settings)
|
||||
public static ZwinderStateManager Create(BinaryReader br, ZwinderStateManagerSettings settings, Func<int, bool> reserveCallback)
|
||||
{
|
||||
var current = ZwinderBuffer.Create(br);
|
||||
var recent = ZwinderBuffer.Create(br);
|
||||
var highPriority = ZwinderBuffer.Create(br);
|
||||
|
||||
var original = br.ReadBytes(br.ReadInt32());
|
||||
var gaps = ZwinderBuffer.Create(br);
|
||||
|
||||
var ancientInterval = br.ReadInt32();
|
||||
|
||||
var ret = new ZwinderStateManager(current, recent, highPriority, original, ancientInterval)
|
||||
var ret = new ZwinderStateManager(current, recent, gaps, ancientInterval, reserveCallback)
|
||||
{
|
||||
Settings = settings
|
||||
};
|
||||
|
@ -311,7 +381,13 @@ namespace BizHawk.Client.Common
|
|||
var key = br.ReadInt32();
|
||||
var length = br.ReadInt32();
|
||||
var data = br.ReadBytes(length);
|
||||
ret._ancient.Add(new KeyValuePair<int, byte[]>(key, data));
|
||||
ret._reserved.Add(key, data);
|
||||
}
|
||||
|
||||
var allStates = ret.AllStates().ToList();
|
||||
foreach (var state in allStates)
|
||||
{
|
||||
ret.StateCache.Add(state.Frame);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -321,20 +397,29 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
_current.SaveStateBinary(bw);
|
||||
_recent.SaveStateBinary(bw);
|
||||
_highPriority.SaveStateBinary(bw);
|
||||
|
||||
bw.Write(_originalState.Length);
|
||||
bw.Write(_originalState);
|
||||
_gapFiller.SaveStateBinary(bw);
|
||||
|
||||
bw.Write(_ancientInterval);
|
||||
|
||||
bw.Write(_ancient.Count);
|
||||
foreach (var s in _ancient)
|
||||
bw.Write(_reserved.Count);
|
||||
foreach (var s in _reserved)
|
||||
{
|
||||
bw.Write(s.Key);
|
||||
bw.Write(s.Value.Length);
|
||||
bw.Write(s.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_current?.Dispose();
|
||||
_current = null;
|
||||
|
||||
_recent?.Dispose();
|
||||
_recent = null;
|
||||
|
||||
_gapFiller?.Dispose();
|
||||
_gapFiller = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,9 +16,9 @@ namespace BizHawk.Client.Common
|
|||
RecentBufferSize = settings.RecentBufferSize;
|
||||
RecentTargetFrameLength = settings.RecentTargetFrameLength;
|
||||
|
||||
PriorityUseCompression = settings.PriorityUseCompression;
|
||||
PriorityBufferSize = settings.PriorityBufferSize;
|
||||
PriorityTargetFrameLength = settings.PriorityTargetFrameLength;
|
||||
GapsUseCompression = settings.GapsUseCompression;
|
||||
GapsBufferSize = settings.GapsBufferSize;
|
||||
GapsTargetFrameLength = settings.GapsTargetFrameLength;
|
||||
|
||||
AncientStateInterval = settings.AncientStateInterval;
|
||||
}
|
||||
|
@ -54,16 +54,16 @@ namespace BizHawk.Client.Common
|
|||
/// <summary>
|
||||
/// Priority States for special use cases
|
||||
/// </summary>
|
||||
[DisplayName("Priority - Use Compression")]
|
||||
public bool PriorityUseCompression { get; set; }
|
||||
[DisplayName("Gaps - Use Compression")]
|
||||
public bool GapsUseCompression { get; set; }
|
||||
|
||||
[DisplayName("Priority - Buffer Size")]
|
||||
[DisplayName("Gaps - Buffer Size")]
|
||||
[Description("Max amount of buffer space to use in MB")]
|
||||
public int PriorityBufferSize { get; set; } = 64;
|
||||
public int GapsBufferSize { get; set; } = 64;
|
||||
|
||||
[DisplayName("Priority - Target Frame Length")]
|
||||
[DisplayName("Gaps - Target Frame Length")]
|
||||
[Description("Desired frame length (number of emulated frames you can go back before running out of buffer)")]
|
||||
public int PriorityTargetFrameLength { get; set; } = 10000;
|
||||
public int GapsTargetFrameLength { get; set; } = 1000;
|
||||
|
||||
[DisplayName("Ancient State Interval")]
|
||||
[Description("How often to maintain states when outside of Current and Recent intervals")]
|
||||
|
|
|
@ -191,7 +191,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
Movie.LoadBranch(branch);
|
||||
Tastudio.LoadState(new KeyValuePair<int, Stream>(branch.Frame, new MemoryStream(branch.CoreData, false)));
|
||||
Movie.TasStateManager.Capture(Tastudio.Emulator.Frame, Tastudio.Emulator.AsStatable(), true);
|
||||
Movie.TasStateManager.Capture(Tastudio.Emulator.Frame, Tastudio.Emulator.AsStatable());
|
||||
QuickBmpFile.Copy(new BitmapBufferVideoProvider(branch.CoreFrameBuffer), Tastudio.VideoProvider);
|
||||
|
||||
if (Tastudio.Settings.OldControlSchemeForBranches && Tastudio.TasPlaybackBox.RecordingMode)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using BizHawk.Client.Common;
|
||||
using BizHawk.Emulation.Common;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
@ -8,16 +9,46 @@ namespace BizHawk.Tests.Client.Common.Movie
|
|||
[TestClass]
|
||||
public class ZwinderStateManagerTests
|
||||
{
|
||||
private ZwinderStateManager CreateSmallZwinder(IStatable ss)
|
||||
{
|
||||
var zw = new ZwinderStateManager(new ZwinderStateManagerSettings
|
||||
{
|
||||
CurrentBufferSize = 1,
|
||||
CurrentTargetFrameLength = 10000,
|
||||
|
||||
RecentBufferSize = 1,
|
||||
RecentTargetFrameLength = 100000,
|
||||
|
||||
AncientStateInterval = 50000
|
||||
}, f => false);
|
||||
|
||||
var ms = new MemoryStream();
|
||||
ss.SaveStateBinary(new BinaryWriter(ms));
|
||||
zw.Engage(ms.ToArray());
|
||||
return zw;
|
||||
}
|
||||
|
||||
private IStatable CreateStateSource() => new StateSource {PaddingData = new byte[1000]};
|
||||
|
||||
[TestMethod]
|
||||
public void SaveCreateRoundTrip()
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
var zw = new ZwinderStateManager();
|
||||
var zw = new ZwinderStateManager(new ZwinderStateManagerSettings
|
||||
{
|
||||
CurrentBufferSize = 16,
|
||||
CurrentTargetFrameLength = 10000,
|
||||
|
||||
RecentBufferSize = 16,
|
||||
RecentTargetFrameLength = 100000,
|
||||
|
||||
AncientStateInterval = 50000
|
||||
}, f => false);
|
||||
zw.SaveStateHistory(new BinaryWriter(ms));
|
||||
var buff = ms.ToArray();
|
||||
var rms = new MemoryStream(buff, false);
|
||||
|
||||
var zw2 = ZwinderStateManager.Create(new BinaryReader(rms), new ZwinderStateManagerSettings());
|
||||
var zw2 = ZwinderStateManager.Create(new BinaryReader(rms), zw.Settings, f => false);
|
||||
|
||||
// TODO: we could assert more things here to be thorough
|
||||
Assert.IsNotNull(zw2);
|
||||
|
@ -51,7 +82,6 @@ namespace BizHawk.Tests.Client.Common.Movie
|
|||
{
|
||||
var buff = new ZwinderBuffer(new RewindConfig
|
||||
{
|
||||
UseCompression = false,
|
||||
BufferSize = 1,
|
||||
TargetFrameLength = 10
|
||||
});
|
||||
|
@ -88,16 +118,14 @@ namespace BizHawk.Tests.Client.Common.Movie
|
|||
var ss = new StateSource { PaddingData = new byte[1000] };
|
||||
var zw = new ZwinderStateManager(new ZwinderStateManagerSettings
|
||||
{
|
||||
CurrentUseCompression = false,
|
||||
CurrentBufferSize = 1,
|
||||
CurrentTargetFrameLength = 10000,
|
||||
|
||||
RecentUseCompression = false,
|
||||
RecentBufferSize = 1,
|
||||
RecentTargetFrameLength = 100000,
|
||||
|
||||
AncientStateInterval = 50000
|
||||
});
|
||||
}, f => false);
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
ss.SaveStateBinary(new BinaryWriter(ms));
|
||||
|
@ -114,6 +142,282 @@ namespace BizHawk.Tests.Client.Common.Movie
|
|||
Assert.IsTrue(actual <= 10440);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Last_Correct_WhenReservedGreaterThanCurrent()
|
||||
{
|
||||
// Arrange
|
||||
const int futureReservedFrame = 1000;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
zw.CaptureReserved(futureReservedFrame, ss);
|
||||
for (int i = 1; i < 20; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = zw.Last;
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(futureReservedFrame, actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Last_Correct_WhenCurrentIsLast()
|
||||
{
|
||||
// Arrange
|
||||
const int totalCurrentFrames = 20;
|
||||
const int expectedFrameGap = 9;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = zw.Last;
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(totalCurrentFrames - expectedFrameGap, actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void HasState_Correct_WhenReservedGreaterThanCurrent()
|
||||
{
|
||||
// Arrange
|
||||
const int futureReservedFrame = 1000;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
zw.CaptureReserved(futureReservedFrame, ss);
|
||||
for (int i = 1; i < 20; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = zw.HasState(futureReservedFrame);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void HasState_Correct_WhenCurrentIsLast()
|
||||
{
|
||||
// Arrange
|
||||
const int totalCurrentFrames = 20;
|
||||
const int expectedFrameGap = 9;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = zw.HasState(totalCurrentFrames - expectedFrameGap);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetStateClosestToFrame_Correct_WhenReservedGreaterThanCurrent()
|
||||
{
|
||||
// Arrange
|
||||
const int futureReservedFrame = 1000;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
zw.CaptureReserved(futureReservedFrame, ss);
|
||||
for (int i = 1; i < 10; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = zw.GetStateClosestToFrame(futureReservedFrame + 1);
|
||||
|
||||
// Assert
|
||||
Assert.IsNotNull(actual);
|
||||
Assert.AreEqual(futureReservedFrame, actual.Key);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetStateClosestToFrame_Correct_WhenCurrentIsLast()
|
||||
{
|
||||
// Arrange
|
||||
const int totalCurrentFrames = 20;
|
||||
const int expectedFrameGap = 9;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = zw.GetStateClosestToFrame(totalCurrentFrames);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(totalCurrentFrames - expectedFrameGap, actual.Key);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InvalidateAfter_Correct_WhenReservedGreaterThanCurrent()
|
||||
{
|
||||
// Arrange
|
||||
const int futureReservedFrame = 1000;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
zw.CaptureReserved(futureReservedFrame, ss);
|
||||
for (int i = 1; i < 10; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
zw.InvalidateAfter(futureReservedFrame - 1);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(zw.HasState(futureReservedFrame));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InvalidateAfter_Correct_WhenCurrentIsLast()
|
||||
{
|
||||
// Arrange
|
||||
const int totalCurrentFrames = 10;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
zw.InvalidateAfter(totalCurrentFrames - 1);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(zw.HasState(totalCurrentFrames));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Count_NoReserved()
|
||||
{
|
||||
// Arrange
|
||||
const int totalCurrentFrames = 20;
|
||||
const int expectedFrameGap = 10;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = zw.Count;
|
||||
|
||||
// Assert
|
||||
var expected = (totalCurrentFrames / expectedFrameGap) + 1;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Count_WithReserved()
|
||||
{
|
||||
// Arrange
|
||||
const int totalCurrentFrames = 20;
|
||||
const int expectedFrameGap = 10;
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
zw.CaptureReserved(1000, ss);
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = zw.Count;
|
||||
|
||||
// Assert
|
||||
var expected = (totalCurrentFrames / expectedFrameGap) + 2;
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StateCache()
|
||||
{
|
||||
var ss = CreateStateSource();
|
||||
var zw = new ZwinderStateManager(new ZwinderStateManagerSettings
|
||||
{
|
||||
CurrentBufferSize = 2,
|
||||
CurrentTargetFrameLength = 1000,
|
||||
RecentBufferSize = 2,
|
||||
RecentTargetFrameLength = 1000,
|
||||
AncientStateInterval = 100
|
||||
}, f => false);
|
||||
|
||||
for (int i = 0; i < 1000; i += 200)
|
||||
{
|
||||
zw.CaptureReserved(i, ss);
|
||||
}
|
||||
|
||||
for (int i = 400; i < 1000; i += 400)
|
||||
{
|
||||
zw.EvictReserved(i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10000; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
zw.Capture(101, ss);
|
||||
|
||||
var allStates = zw.AllStates()
|
||||
.Select(s => s.Frame)
|
||||
.ToList();
|
||||
|
||||
for (int i = 0; i < 10000; i++)
|
||||
{
|
||||
var actual = zw.HasState(i);
|
||||
var expected = allStates.Contains(i);
|
||||
Assert.AreEqual(expected, actual);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Clear_KeepsZeroState()
|
||||
{
|
||||
// Arrange
|
||||
var ss = CreateStateSource();
|
||||
using var zw = CreateSmallZwinder(ss);
|
||||
|
||||
zw.CaptureReserved(1000, ss);
|
||||
for (int i = 1; i < 10; i++)
|
||||
{
|
||||
zw.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
zw.Clear();
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(1, zw.AllStates().Count());
|
||||
Assert.AreEqual(0, zw.AllStates().Single().Frame);
|
||||
}
|
||||
|
||||
private class StateSource : IStatable
|
||||
{
|
||||
public int Frame { get; set; }
|
||||
|
|
Loading…
Reference in New Issue