From 1b0139ebc306c64e476274b4c883e5a181e3244d Mon Sep 17 00:00:00 2001 From: adelikat Date: Sun, 23 Aug 2020 17:12:33 -0500 Subject: [PATCH] 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 --- .../movie/tasproj/IStateManager.cs | 5 + .../movie/tasproj/TasMovie.IO.cs | 2 +- .../movie/tasproj/TasMovie.cs | 14 +- .../movie/tasproj/TasMovieMarker.cs | 2 + .../movie/tasproj/ZwinderStateManager.cs | 319 +++++++++++------- .../tasproj/ZwinderStateManagerSettings.cs | 18 +- .../tools/TAStudio/BookmarksBranchesBox.cs | 2 +- .../Movie/ZwinderStateManagerTests.cs | 316 ++++++++++++++++- 8 files changed, 542 insertions(+), 136 deletions(-) diff --git a/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs b/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs index 6fcd012262..c8000adb54 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs @@ -22,6 +22,11 @@ namespace BizHawk.Client.Common /// void Capture(int frame, IStatable source, bool force = false); + /// + /// Commands the state manager to remove a reserved state for the given frame, if it is exists + /// + void EvictReserved(int frame); + bool HasState(int frame); /// diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs index 0c83e77fa5..8891d5d9aa 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs @@ -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); }); } } diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs index bed621527b..3b48da59b9 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs @@ -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 + } } } diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovieMarker.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovieMarker.cs index dfd47b7b6b..edc8682b3d 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovieMarker.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovieMarker.cs @@ -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); } } diff --git a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs index 10582edc76..17329eefa7 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs @@ -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> _ancient = new List>(); + private readonly Func _reserveCallback; + internal readonly SortedSet StateCache = new SortedSet(); + 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 _reserved = new Dictionary(); + + // 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 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()) + /// Called when deciding to evict a state for the given frame, if true is returned, the state will be reserved + public ZwinderStateManager(Func 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 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 Read { get; } @@ -91,10 +109,7 @@ namespace BizHawk.Client.Common Frame = si.Frame; Read = si.GetReadStream; } - public StateInfo(KeyValuePair kvp) - :this(kvp.Key, kvp.Value) - { - } + public StateInfo(int frame, byte[] data) { Frame = frame; @@ -102,11 +117,8 @@ namespace BizHawk.Client.Common } } - /// - /// Enumerate all states, excepting high priority, in reverse order - /// - /// - private IEnumerable NormalStates() + // Enumerate all current and recent states in reverse order + private IEnumerable 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); } - /// - /// Enumerate high priority states in reverse order - /// - /// - private IEnumerable HighPriorityStates() + // Enumerate all gap states in reverse order + private IEnumerable 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 ReservedStates() + { + foreach (var key in _reserved.Keys.OrderByDescending(k => k)) + { + yield return new StateInfo(key, _reserved[key]); } } /// /// Enumerate all states in reverse order /// - private IEnumerable AllStates() + internal IEnumerable 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(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 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 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(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; + } } } diff --git a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManagerSettings.cs b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManagerSettings.cs index 9d1d228ef6..4fb605084f 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManagerSettings.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManagerSettings.cs @@ -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 /// /// Priority States for special use cases /// - [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")] diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs index a0703d373a..36d34aa519 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs @@ -191,7 +191,7 @@ namespace BizHawk.Client.EmuHawk Movie.LoadBranch(branch); Tastudio.LoadState(new KeyValuePair(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) diff --git a/src/BizHawk.Tests/Client.Common/Movie/ZwinderStateManagerTests.cs b/src/BizHawk.Tests/Client.Common/Movie/ZwinderStateManagerTests.cs index 0333a9e1eb..8683d2c028 100644 --- a/src/BizHawk.Tests/Client.Common/Movie/ZwinderStateManagerTests.cs +++ b/src/BizHawk.Tests/Client.Common/Movie/ZwinderStateManagerTests.cs @@ -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; }