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:
adelikat 2020-08-23 17:12:33 -05:00 committed by GitHub
parent fb6924bd83
commit 1b0139ebc3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 542 additions and 136 deletions

View File

@ -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>

View File

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

View File

@ -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
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

@ -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")]

View File

@ -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)

View File

@ -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; }