From 357d87239bca6f65767cf2bdd414e170df63e7fd Mon Sep 17 00:00:00 2001 From: adelikat Date: Wed, 5 Aug 2020 20:39:15 -0500 Subject: [PATCH] Rewrite TasStateManager (#2274) * zwinder state manager wip * stuff * fixes * slightly better defaults * stuff * re-delete TasStateManager - re-added by my bad rebase attempt * add back in saving of a default.tasproj, we are a lot of refactors away from not having to do this * Make zwinder loadstate a factory method * progress on wiring up Zwinder to movie shenanigans * zwinder now survives save/load * hack for settings to exist * fix test * shenangians for frame 0, add some asserts to the unit test * Add crappy impl of integrity check for zwinderstatemanager * remove Any() from the IStateManager contract, since it should always have at least 1 state * move ZwinderStateManagerSettings to its own file * use NonState, it's there and I suppose this was the intent * add a test * don't attempt to capture states if we aren't "currentt" * ugh * small cleanup * ZwinderStateManagerSettings - implement necessary copy constructor * wire up Settings updating, get rid of Settings setter, add some documentation to IStateManager * shenanigans to fix Savestate settings UI * Play around with "high priority" It's really a mess because there's no information feeding between high priority and normal priority on what captures should take place... * this fixes the branch gap problem * bump tasproj version to 1.1, warn user and gracefully handle loading an incompatible version * if a movie starts from savestate, stuff the anchored state into TasStateManager instead of a frame zero state * ZwinderBuffer - remove some unused usings Co-authored-by: nattthebear --- .../config/MovieConfig.cs | 4 +- .../movie/MovieConversionExtensions.cs | 4 +- .../movie/MovieService.cs | 39 +- .../movie/MovieSession.cs | 2 +- .../movie/interfaces/IMovieSession.cs | 2 + .../movie/interfaces/ITasMovie.cs | 2 + .../movie/tasproj/IStateManager.cs | 56 +-- .../movie/tasproj/StateManagerDecay.cs | 163 -------- .../movie/tasproj/TasMovie.Editing.cs | 2 + .../movie/tasproj/TasMovie.IO.cs | 19 +- .../movie/tasproj/TasMovie.cs | 26 +- .../movie/tasproj/TasStateManager.cs | 378 ------------------ .../movie/tasproj/TasStateManagerSettings.cs | 67 ---- .../movie/tasproj/ZwinderStateManager.cs | 339 ++++++++++++++++ .../tasproj/ZwinderStateManagerSettings.cs | 77 ++++ .../rewind/ZwinderBuffer.cs | 52 ++- .../tools/TAStudio/BookmarksBranchesBox.cs | 7 +- .../DefaultGreenzoneSettings.Designer.cs | 2 +- .../TAStudio/DefaultGreenzoneSettings.cs | 8 +- .../tools/TAStudio/TAStudio.MenuItems.cs | 15 +- .../tools/TAStudio/TAStudio.Navigation.cs | 2 +- .../tools/TAStudio/TAStudio.cs | 14 +- .../Client.Common/Movie/MovieServiceTests.cs | 23 ++ .../Movie/ZwinderStateManagerTests.cs | 84 ++++ 24 files changed, 693 insertions(+), 694 deletions(-) delete mode 100644 src/BizHawk.Client.Common/movie/tasproj/StateManagerDecay.cs delete mode 100644 src/BizHawk.Client.Common/movie/tasproj/TasStateManager.cs delete mode 100644 src/BizHawk.Client.Common/movie/tasproj/TasStateManagerSettings.cs create mode 100644 src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs create mode 100644 src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManagerSettings.cs create mode 100644 src/BizHawk.Tests/Client.Common/Movie/MovieServiceTests.cs create mode 100644 src/BizHawk.Tests/Client.Common/Movie/ZwinderStateManagerTests.cs diff --git a/src/BizHawk.Client.Common/config/MovieConfig.cs b/src/BizHawk.Client.Common/config/MovieConfig.cs index 0d662bc0be..484939ea62 100644 --- a/src/BizHawk.Client.Common/config/MovieConfig.cs +++ b/src/BizHawk.Client.Common/config/MovieConfig.cs @@ -7,7 +7,7 @@ public bool MoviesOnDisk { get; } public int MovieCompressionLevel { get; } public bool VBAStyleMovieLoadState { get; } - TasStateManagerSettings DefaultTasStateManagerSettings { get; } + ZwinderStateManagerSettings DefaultTasStateManagerSettings { get; } } public class MovieConfig : IMovieConfig @@ -18,6 +18,6 @@ public int MovieCompressionLevel { get; set; } = 2; public bool VBAStyleMovieLoadState { get; set; } - public TasStateManagerSettings DefaultTasStateManagerSettings { get; set; } = new TasStateManagerSettings(); + public ZwinderStateManagerSettings DefaultTasStateManagerSettings { get; set; } = new ZwinderStateManagerSettings(); } } diff --git a/src/BizHawk.Client.Common/movie/MovieConversionExtensions.cs b/src/BizHawk.Client.Common/movie/MovieConversionExtensions.cs index c0b5c7ea5a..a950ba3d6d 100644 --- a/src/BizHawk.Client.Common/movie/MovieConversionExtensions.cs +++ b/src/BizHawk.Client.Common/movie/MovieConversionExtensions.cs @@ -135,7 +135,7 @@ namespace BizHawk.Client.Common } } - tas.TasStateManager.Settings = old.TasStateManager.Settings; + tas.TasStateManager.UpdateSettings(old.TasStateManager.Settings); tas.Save(); return tas; @@ -176,7 +176,7 @@ namespace BizHawk.Client.Common tas.Subtitles.Add(sub); } - tas.TasStateManager.Settings = old.TasStateManager.Settings; + tas.TasStateManager.UpdateSettings(old.TasStateManager.Settings); tas.Save(); return tas; diff --git a/src/BizHawk.Client.Common/movie/MovieService.cs b/src/BizHawk.Client.Common/movie/MovieService.cs index c24892cc56..c8cbbe26b0 100644 --- a/src/BizHawk.Client.Common/movie/MovieService.cs +++ b/src/BizHawk.Client.Common/movie/MovieService.cs @@ -1,5 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using BizHawk.Common.NumberExtensions; namespace BizHawk.Client.Common { @@ -17,5 +19,40 @@ namespace BizHawk.Client.Common { return MovieExtensions.Contains(ext.ToLower().Replace(".", "")); } + + public static bool IsCurrentTasVersion(string movieVersion) + { + var actual = ParseTasMovieVersion(movieVersion); + return actual.HawkFloatEquality(TasMovie.CurrentVersion); + } + + internal static double ParseTasMovieVersion(string movieVersion) + { + if (string.IsNullOrWhiteSpace(movieVersion)) + { + return 1.0F; + } + + var split = movieVersion + .ToLower() + .Split(new[] {"tasproj"}, StringSplitOptions.RemoveEmptyEntries); + + if (split.Length == 1) + { + return 1.0F; + } + + var versionStr = split[1] + .Trim() + .Replace("v", ""); + + var result = double.TryParse(versionStr, out double version); + if (result) + { + return version; + } + + return 1.0F; + } } } diff --git a/src/BizHawk.Client.Common/movie/MovieSession.cs b/src/BizHawk.Client.Common/movie/MovieSession.cs index 83c9423005..e53517c59e 100644 --- a/src/BizHawk.Client.Common/movie/MovieSession.cs +++ b/src/BizHawk.Client.Common/movie/MovieSession.cs @@ -311,7 +311,7 @@ namespace BizHawk.Client.Common return new Bk2Movie(this, path); } - private void PopupMessage(string message) + public void PopupMessage(string message) { _popupCallback?.Invoke(message); } diff --git a/src/BizHawk.Client.Common/movie/interfaces/IMovieSession.cs b/src/BizHawk.Client.Common/movie/interfaces/IMovieSession.cs index 3d0b855e38..8739553da6 100644 --- a/src/BizHawk.Client.Common/movie/interfaces/IMovieSession.cs +++ b/src/BizHawk.Client.Common/movie/interfaces/IMovieSession.cs @@ -78,5 +78,7 @@ namespace BizHawk.Client.Common IMovie Get(string path); string BackupDirectory { get; set; } + + void PopupMessage(string message); } } diff --git a/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs b/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs index b0bd0c9086..9287fe0a53 100644 --- a/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs +++ b/src/BizHawk.Client.Common/movie/interfaces/ITasMovie.cs @@ -23,6 +23,8 @@ namespace BizHawk.Client.Common IStringLog VerificationLog { get; } int LastEditedFrame { get; } + Action GreenzoneInvalidated { get; set; } + string DisplayValue(int frame, string buttonName); void FlagChanges(); void ClearChanges(); diff --git a/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs b/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs index e29c8d40ff..afe68bf0de 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs @@ -9,60 +9,40 @@ namespace BizHawk.Client.Common { /// /// Retrieves the savestate for the given frame, - /// If this frame does not have a state currently, will return an empty array + /// If this frame does not have a state currently, will return an empty array.false + /// Try not to use this as it is not fast. /// /// A savestate for the given frame or an empty array if there isn't one byte[] this[int frame] { get; } - /// - /// Attaches a core to the given state manager instance, this must be done and - /// it must be done only once, a state manager can not and should not exist for more - /// than the lifetime of the core - /// - /// - /// Thrown if attempting to attach a core when one is already attached - /// or if the given core does not meet all required dependencies - /// - void Attach(IEmulator emulator); - - TasStateManagerSettings Settings { get; set; } - - Action InvalidateCallback { set; } + ZwinderStateManagerSettings Settings { get; } /// /// Requests that the current emulator state be captured /// Unless force is true, the state may or may not be captured depending on the logic employed by "green-zone" management /// - void Capture(bool force = false); + void Capture(int frame, IBinaryStateable source, bool force = false); + + // TODO: should this be used for markers? + //void CaptureHighPriority(int frame, IBinaryStateable source); bool HasState(int frame); /// - /// Clears out all savestates after the given frame number + /// Clears out all savestates after or at the given frame number /// bool Invalidate(int frame); // Remove all states, but not the frame 0 state void Clear(); - void Save(BinaryWriter bw); - - void Load(BinaryReader br); - /// /// Get a nearby state. The returned frame must be less (but not equal to???) the passed frame. /// This may not fail; the StateManager strongly holds a frame 0 state to ensure there's always a possible result. /// /// - /// - KeyValuePair GetStateClosestToFrame(int frame); - - /// - /// Returns true iff Count > 0 - /// TODO: Surely this is always true because the frame 0 state is always retained? - /// - /// - bool Any(); + /// This stream may be consumed only once, and before any other calls to statemanager occur + KeyValuePair GetStateClosestToFrame(int frame); /// /// Returns the total number of states currently held by the state manager @@ -77,14 +57,20 @@ namespace BizHawk.Client.Common int Last { get; } /// - /// Adjust internal state saving logic based on changes to Settings + /// Updates the internal state saving logic settings /// - void UpdateStateFrequency(); + void UpdateSettings(ZwinderStateManagerSettings settings); /// - /// Directly remove a state from the given frame, if it exists - /// Should only be called by pruning operations + /// Serializes the current state of the instance for persisting to disk /// - bool Remove(int frame); + void SaveStateHistory(BinaryWriter bw); + + /// + /// Enables the instance to be used. An instance of should not + /// be useable until this method is called + /// + /// + void Engage(byte[] frameZeroState); } } diff --git a/src/BizHawk.Client.Common/movie/tasproj/StateManagerDecay.cs b/src/BizHawk.Client.Common/movie/tasproj/StateManagerDecay.cs deleted file mode 100644 index 9893f03d37..0000000000 --- a/src/BizHawk.Client.Common/movie/tasproj/StateManagerDecay.cs +++ /dev/null @@ -1,163 +0,0 @@ -/**************************************************************************************** - - Algorithm by r57shell & feos, 2018 - - _zeros is the key to GREENZONE DECAY PATTERN. - - In a 16 element example, we evaluate these bitwise numbers to count zeros on the right. - First element is always assumed to be 16, which has all 4 bits set to 0. Each right zero - means that we lower the priority of a state that goes at that index. Priority changes - depending on current frame and amount of states. States with biggest priority get erased - first. With a 4-bit pattern and no initial gap between states, total frame coverage is - about 5 times state count. - - Initial state gap can screw up our patterns, so do all the calculations like the gap - isn't there, and take it back into account afterwards. The algo only works with integral - greenzone, so we make it think it is integral by reducing the frame numbers. Before any - decay logic starts for each state, we check if it has a marker on it (in which case we - don't drop it) or appears inside the state gap (in which case we forcibly drop it). This - step doesn't involve numbers reduction. - - _zeros values are essentially the values of rshiftby here: - bitwise view frame rshiftby priority - 00010000 0 4 1 - 00000001 1 0 15 - 00000010 2 1 7 - 00000011 3 0 13 - 00000100 4 2 3 - 00000101 5 0 11 - 00000110 6 1 5 - 00000111 7 0 9 - 00001000 8 3 1 - 00001001 9 0 7 - 00001010 10 1 3 - 00001011 11 0 5 - 00001100 12 2 1 - 00001101 13 0 3 - 00001110 14 1 1 - 00001111 15 0 1 - -*****************************************************************************************/ -using System.Collections.Generic; - -namespace BizHawk.Client.Common -{ - // TODO: interface me - internal class StateManagerDecay - { - private readonly ITasMovie _movie; - private readonly TasStateManager _tsm; - - private List _zeros; // number of ending zeros in binary representation of the index - private int _bits; // max number of bits for which to calculate _zeros - private int _mask; // to mask index into _zeros, to prevent accessing out of range - - private int _step; // initial gap between states - - public StateManagerDecay(ITasMovie movie, TasStateManager tsm) - { - _movie = movie; - _tsm = tsm; - } - - /// - /// This will strategically remove states based on their alignment with the state gap (and its multiples) and their distance from the current frame. - /// - public void Trigger(int currentEmulatedFrame, int statesToDecay) - { - int baseStateIndex = _tsm.GetStateIndexByFrame(currentEmulatedFrame); - int baseStateFrame = _tsm.GetStateFrameByIndex(baseStateIndex) / _step; // reduce to step integral TODO: do we actually want this? - // key: priority value: frame - List> decayPriorities = new List>(); - - for (int currentStateIndex = 1; currentStateIndex < _tsm.Count; currentStateIndex++) - { - int currentFrame = _tsm.GetStateFrameByIndex(currentStateIndex); - - if (_movie.Markers.IsMarker(currentFrame + 1)) - continue; - if (currentFrame + 1 == _movie.LastEditedFrame) - continue; - - // not aligned to state gap at all - if (currentFrame % _step > 0) - { - if (_tsm.Remove(currentFrame)) - statesToDecay--; - if (statesToDecay == 0) - return; - continue; - } - - // reduce to step integral for the decay logic - currentFrame /= _step; - int zeroCount = _zeros[currentFrame & _mask]; - int priority = (baseStateFrame - currentFrame) >> zeroCount; - decayPriorities.Add(new KeyValuePair(priority, currentFrame * _step)); - } - - // optimization: if we are only removing 1 state, don't bother sorting the whole list - if (statesToDecay == 1) - { - int highestPriority = decayPriorities[0].Key; - int toRemove = decayPriorities[0].Value; - for (int i = 1; i < decayPriorities.Count; i++) - { - if (decayPriorities[i].Key > highestPriority) - { - highestPriority = decayPriorities[i].Key; - toRemove = decayPriorities[i].Value; - } - } - if (!_tsm.Remove(toRemove)) - throw new System.Exception("Failed to remove state."); // should never happen - return; - } - else - { - // reverse sort; high priority to remove comes first - decayPriorities.Sort((p2, p1) => p1.Key.CompareTo(p2.Key)); - } - - int index = 0; - while (statesToDecay > 0 && index < decayPriorities.Count) - { - if (_tsm.Remove(decayPriorities[index].Value)) - statesToDecay--; - index++; - } - - // we're very sorry about failing to find states to remove, but we can't go beyond capacity, so remove at least something - while (statesToDecay > 0) - { - if (_tsm.Remove(_tsm.GetStateFrameByIndex(1))) - statesToDecay--; - else // This should never happen, but just in case, we don't want to let memory usage continue to climb. - throw new System.Exception("Failed to remove states."); - } - } - - public void UpdateSettings(int step, int bits) - { - _step = step; - _bits = bits; - _mask = (1 << _bits) - 1; - _zeros = new List { _bits }; - - for (int i = 1; i < (1 << _bits); i++) - { - _zeros.Add(0); - - for (int j = i; j > 0; j >>= 1) - { - if ((j & 1) > 0) - { - break; - } - - _zeros[i]++; - } - } - } - } -} diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs index 3e0cfd11c3..63579e0039 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.Editing.cs @@ -27,6 +27,7 @@ namespace BizHawk.Client.Common if (this.IsRecording()) { TasStateManager.Invalidate(frame + 1); + GreenzoneInvalidated(frame + 1); } if (frame != 0) @@ -49,6 +50,7 @@ namespace BizHawk.Client.Common LagLog.RemoveFrom(frame); TasStateManager.Invalidate(frame); + GreenzoneInvalidated(frame); Markers.TruncateAt(frame); ChangeLog.SetGeneralRedo(); diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs index 7a644e229f..71a75f8fbd 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.IO.cs @@ -43,7 +43,7 @@ namespace BizHawk.Client.Common if (TasStateManager.Settings.SaveStateHistory && !isBackup) { - bs.PutLump(BinaryStateLump.StateHistory, bw => TasStateManager.Save(bw)); + bs.PutLump(BinaryStateLump.StateHistory, bw => TasStateManager.SaveStateHistory(bw)); } } @@ -56,7 +56,7 @@ namespace BizHawk.Client.Common private void ClearTasprojExtras() { LagLog.Clear(); - TasStateManager.Clear(); + TasStateManager?.Clear(); Markers.Clear(); ChangeLog.Clear(); } @@ -64,9 +64,17 @@ namespace BizHawk.Client.Common protected override void LoadFields(ZipStateLoader bl, bool preload) { LoadBk2Fields(bl, preload); + if (!preload) { - LoadTasprojExtras(bl); + if (MovieService.IsCurrentTasVersion(Header[HeaderKeys.MovieVersion])) + { + LoadTasprojExtras(bl); + } + else + { + Session.PopupMessage("The current .tasproj is compatible with this version of BizHawk! .tasproj features failed to load."); + } } } @@ -82,7 +90,8 @@ namespace BizHawk.Client.Common var json = tr.ReadToEnd(); try { - TasStateManager.Settings = JsonConvert.DeserializeObject(json); + var settings = JsonConvert.DeserializeObject(json); + TasStateManager.UpdateSettings(settings); } catch { @@ -160,7 +169,7 @@ namespace BizHawk.Client.Common { bl.GetLump(BinaryStateLump.StateHistory, false, delegate(BinaryReader br, long length) { - TasStateManager.Load(br); + TasStateManager = ZwinderStateManager.Create(br, TasStateManager.Settings); }); } } diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs index aae504c5eb..ebe5dcf32e 100644 --- a/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs +++ b/src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs @@ -12,16 +12,18 @@ namespace BizHawk.Client.Common public new const string Extension = "tasproj"; private IInputPollable _inputPollable; + public const double CurrentVersion = 1.1; + /// loaded core does not implement internal TasMovie(IMovieSession session, string path) : base(session, path) { Branches = new TasBranchCollection(this); ChangeLog = new TasMovieChangeLog(this); - TasStateManager = new TasStateManager(this, session.Settings.DefaultTasStateManagerSettings); - Header[HeaderKeys.MovieVersion] = "BizHawk v2.0 Tasproj v1.0"; + Header[HeaderKeys.MovieVersion] = $"BizHawk v2.0 Tasproj v{CurrentVersion}"; Markers = new TasMovieMarkerList(this); Markers.CollectionChanged += Markers_CollectionChanged; Markers.Add(0, "Power on"); + TasStateManager = new ZwinderStateManager(); } public override void Attach(IEmulator emulator) @@ -37,7 +39,17 @@ namespace BizHawk.Client.Common } _inputPollable = emulator.AsInputPollable(); - TasStateManager.Attach(emulator); + + if (StartsFromSavestate) + { + TasStateManager.Engage(BinarySavestate); + } + else + { + var ms = new MemoryStream(); + emulator.AsStatable().SaveStateBinary(new BinaryWriter(ms)); + TasStateManager.Engage(ms.ToArray()); + } base.Attach(emulator); @@ -71,7 +83,9 @@ namespace BizHawk.Client.Common public TasLagLog LagLog { get; } = new TasLagLog(); public override string PreferredExtension => Extension; - public IStateManager TasStateManager { get; } + public IStateManager TasStateManager { get; private set; } + + public Action GreenzoneInvalidated { get; set; } public ITasMovieRecord this[int index] { @@ -111,6 +125,7 @@ namespace BizHawk.Client.Common { var anyLagInvalidated = LagLog.RemoveFrom(frame); var anyStateInvalidated = TasStateManager.Invalidate(frame + 1); + GreenzoneInvalidated(frame + 1); if (anyLagInvalidated || anyStateInvalidated) { Changes = true; @@ -172,7 +187,7 @@ namespace BizHawk.Client.Common if (!TasStateManager.HasState(Emulator.Frame)) { - TasStateManager.Capture(Emulator.Frame == LastEditedFrame - 1); + TasStateManager.Capture(Emulator.Frame, Emulator.AsStatable(), Emulator.Frame == LastEditedFrame - 1); } } @@ -270,6 +285,7 @@ namespace BizHawk.Client.Common { LagLog.RemoveFrom(timelineBranchFrame.Value); TasStateManager.Invalidate(timelineBranchFrame.Value); + GreenzoneInvalidated(timelineBranchFrame.Value); } return true; diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasStateManager.cs b/src/BizHawk.Client.Common/movie/tasproj/TasStateManager.cs deleted file mode 100644 index 26cdc09d4d..0000000000 --- a/src/BizHawk.Client.Common/movie/tasproj/TasStateManager.cs +++ /dev/null @@ -1,378 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -using BizHawk.Common.NumberExtensions; -using BizHawk.Emulation.Common; - -namespace BizHawk.Client.Common -{ - /// - /// Captures savestates and manages the logic of adding, retrieving, - /// invalidating/clearing of states. Also does memory management and limiting of states - /// - public class TasStateManager : IStateManager - { - private const int MinFrequency = 1; - private const int MaxFrequency = 16; - - private IStatable _core; - private IEmulator _emulator; - - private StateManagerDecay _decay; - private readonly ITasMovie _movie; - - private SortedList _states = new SortedList(); - private double _expectedStateSizeInMb; - - private ulong _used; - private int _stateFrequency; - - private int MaxStates => (int)(Settings.CapacityMb / _expectedStateSizeInMb + 1); - private int FileStateGap => 1 << Settings.FileStateGap; - - /// loaded core expects savestate size of 0 B - public TasStateManager(ITasMovie movie, TasStateManagerSettings settings) - { - _movie = movie; - Settings = new TasStateManagerSettings(settings); - - if (_movie.StartsFromSavestate) - { - SetState(0, _movie.BinarySavestate); - } - } - - public void Attach(IEmulator emulator) - { - if (!emulator.HasSavestates()) - { - throw new InvalidOperationException($"A core must be able to provide an {nameof(IStatable)} service"); - } - - _emulator = emulator; - _core = emulator.AsStatable(); - - _decay = new StateManagerDecay(_movie, this); - - _expectedStateSizeInMb = _core.CloneSavestate().Length / (double)(1024 * 1024); - if (_expectedStateSizeInMb.HawkFloatEquality(0)) - { - throw new InvalidOperationException("Savestate size can not be zero!"); - } - - // don't erase states if they exist already (already loaded) - if ((_states == null) || (_states.Capacity == 0)) { _states = new SortedList(MaxStates); } - - UpdateStateFrequency(); - } - - public Action InvalidateCallback { get; set; } - - public TasStateManagerSettings Settings { get; set; } - - public byte[] this[int frame] - { - get - { - if (frame == 0) - { - return InitialState; - } - - if (_states.ContainsKey(frame)) - { - return _states[frame]; - } - - return new byte[0]; - } - } - - public int Count => _states.Count; - - public int Last => _states.Count > 0 - ? _states.Last().Key - : 0; - - private byte[] InitialState => - _movie.StartsFromSavestate - ? _movie.BinarySavestate - : _states[0]; - - public bool Any() - { - if (_movie.StartsFromSavestate) - { - return _states.Count > 0; - } - - return _states.Count > 1; - } - - public void UpdateStateFrequency() - { - _stateFrequency = ((int)(_expectedStateSizeInMb * 1024 / Settings.MemStateGapDividerKB)) - .Clamp(MinFrequency, MaxFrequency); - - _decay.UpdateSettings(_stateFrequency, 6); - LimitStateCount(); - } - - public void Capture(bool force = false) - { - bool shouldCapture; - int frame = _emulator.Frame; - - if (_movie.StartsFromSavestate && frame == 0) // Never capture frame 0 on savestate anchored movies since we have it anyway - { - shouldCapture = false; - } - else if (force) - { - shouldCapture = true; - } - else if (frame == 0) // For now, long term, TasMovie should have a .StartState property, and a .tasproj file for the start state in non-savestate anchored movies - { - shouldCapture = true; - } - else if (IsMarkerState(frame)) - { - shouldCapture = true; // Markers should always get priority - } - else - { - shouldCapture = frame % _stateFrequency == 0; - } - - if (shouldCapture) - { - SetState(frame, (byte[])_core.SaveStateBinary().Clone(), skipRemoval: false); - } - } - - public void Clear() - { - if (_states.Any()) - { - // For power-on movies, we can't lose frame 0; - byte[] power = null; - if (!_movie.StartsFromSavestate) - { - power = _states[0]; - } - - _states.Clear(); - - if (power != null) - { - SetState(0, power); - _used = (ulong)power.Length; - } - - _movie.FlagChanges(); - } - } - - public bool HasState(int frame) - { - if (_movie.StartsFromSavestate && frame == 0) - { - return true; - } - - return _states.ContainsKey(frame); - } - - /// true iff any frames were invalidated - public bool Invalidate(int frame) - { - if (!Any()) return false; - if (frame == 0) frame = 1; // Never invalidate frame 0 - var statesToRemove = _states.Where(s => s.Key >= frame).ToList(); - foreach (var state in statesToRemove) Remove(state.Key); - InvalidateCallback?.Invoke(frame); - return statesToRemove.Count != 0; - } - - public bool Remove(int frame) - { - int index = _states.IndexOfKey(frame); - - if (frame < 1 || index < 1) - { - return false; - } - - var state = _states[frame]; - - _used -= (ulong)state.Length; - - _states.RemoveAt(index); - - return true; - } - - // Map: - // 4 bytes - total savestate count - // [Foreach state] - // 4 bytes - frame - // 4 bytes - length of savestate - // 0 - n savestate - public void Save(BinaryWriter bw) - { - List noSave = ExcludeStates(); - bw.Write(_states.Count - noSave.Count); - - for (int i = 0; i < _states.Count; i++) - { - if (noSave.Contains(i)) - { - continue; - } - - bw.Write(_states.Keys[i]); - bw.Write(_states.Values[i].Length); - bw.Write(_states.Values[i]); - } - } - - public void Load(BinaryReader br) - { - _states.Clear(); - - try - { - int nstates = br.ReadInt32(); - - for (int i = 0; i < nstates; i++) - { - int frame = br.ReadInt32(); - int len = br.ReadInt32(); - byte[] data = br.ReadBytes(len); - - // whether we should allow state removal check here is an interesting question - // nothing was edited yet, so it might make sense to show the project untouched first - SetState(frame, data); - } - } - catch (EndOfStreamException) - { - } - } - - public KeyValuePair GetStateClosestToFrame(int frame) - { - var s = _states.LastOrDefault(state => state.Key < frame); - if (s.Key > 0) - { - return s; - } - - return new KeyValuePair(0, InitialState); - } - - public int GetStateIndexByFrame(int frame) - { - return _states.IndexOfKey(GetStateClosestToFrame(frame).Key); - } - - public int GetStateFrameByIndex(int index) - { - return _states.Keys[index]; - } - - private bool IsMarkerState(int frame) - { - return _movie.Markers.IsMarker(frame + 1); - } - - private void SetState(int frame, byte[] state, bool skipRemoval = true) - { - if (!skipRemoval) // skipRemoval: false only when capturing new states - { - LimitStateCount(); // Remove before adding so this state won't be removed. - } - - if (_states.ContainsKey(frame)) - { - _states[frame] = state; - } - else - { - _used += (ulong)state.Length; - _states.Add(frame, state); - } - } - - // Deletes states to follow the state storage size limits. - // Used after changing the settings too. - private void LimitStateCount() - { - if (Count + 1 > MaxStates) - { - _decay.Trigger(_emulator.Frame, Count + 1 - MaxStates); - } - } - - private List ExcludeStates() - { - List ret = new List(); - ulong saveUsed = _used; - - // respect state gap no matter how small the resulting size will be - // still leave marker states - for (int i = 1; i < _states.Count; i++) - { - int frame = GetStateFrameByIndex(i); - - if (IsMarkerState(frame) || frame % FileStateGap < _stateFrequency) - { - continue; - } - - ret.Add(i); - - saveUsed -= (ulong)_states.Values[i].Length; - } - - // if the size is still too big, exclude states form the beginning - // still leave marker states - int index = 0; - while (saveUsed > (ulong)Settings.DiskSaveCapacityMb * 1024 * 1024) - { - do - { - if (++index >= _states.Count) - { - break; - } - } - while (IsMarkerState(GetStateFrameByIndex(index))); - - if (index >= _states.Count) - { - break; - } - - ret.Add(index); - saveUsed -= (ulong)_states.Values[index].Length; - } - - // if there are enough markers to still be over the limit, remove marker frames - index = 0; - while (saveUsed > (ulong)Settings.DiskSaveCapacityMb * 1024 * 1024) - { - if (!ret.Contains(++index)) - { - ret.Add(index); - } - - saveUsed -= (ulong)_states.Values[index].Length; - } - - return ret; - } - } -} diff --git a/src/BizHawk.Client.Common/movie/tasproj/TasStateManagerSettings.cs b/src/BizHawk.Client.Common/movie/tasproj/TasStateManagerSettings.cs deleted file mode 100644 index 2832673c30..0000000000 --- a/src/BizHawk.Client.Common/movie/tasproj/TasStateManagerSettings.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.ComponentModel; - -namespace BizHawk.Client.Common -{ - public class TasStateManagerSettings - { - public TasStateManagerSettings() - { - DiskSaveCapacityMb = 512; - CapacityMb = 512; - DiskCapacityMb = 1; // not working yet - MemStateGapDividerKB = 64; - FileStateGap = 4; - } - - public TasStateManagerSettings(TasStateManagerSettings settings) - { - DiskSaveCapacityMb = settings.DiskSaveCapacityMb; - CapacityMb = settings.CapacityMb; - DiskCapacityMb = settings.DiskCapacityMb; - MemStateGapDividerKB = settings.MemStateGapDividerKB; - FileStateGap = settings.FileStateGap; - } - - /// - /// Whether or not to save state history information to disk - /// - [DisplayName("Save History")] - [Description("Whether or not to use savestate history")] - public bool SaveStateHistory => DiskSaveCapacityMb != 0; - - /// - /// Gets or sets the size limit to use when saving the TAS project to disk. - /// - [DisplayName("Save Capacity (in megabytes)")] - [Description("The size limit to use when saving the tas project to disk.")] - public int DiskSaveCapacityMb { get; set; } - - /// - /// Gets or sets the total amount of memory to devote to state history in megabytes - /// - [DisplayName("Capacity (in megabytes)")] - [Description("The size limit of the state history buffer. When this limit is reached it will start moving to disk.")] - public int CapacityMb { get; set; } - - /// - /// Gets or sets the total amount of disk space to devote to state history in megabytes - /// - [DisplayName("Disk Capacity (in megabytes)")] - [Description("The size limit of the state history buffer on the disk. When this limit is reached it will start removing previous savestates")] - public int DiskCapacityMb { get; set; } - - /// - /// Gets or sets the divider that determines memory state gap - /// - [DisplayName("Divider for memory state interval")] - [Description("The actual state gap in frames is calculated as ExpectedStateSizeMB * 1024 / div")] - public int MemStateGapDividerKB { get; set; } - - /// - /// Gets or sets the amount of states to skip during project saving - /// - [DisplayName("State interval for .tasproj")] - [Description("The actual state gap in frames is calculated as Nth power on 2")] - public int FileStateGap { get; set; } - } -} diff --git a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs new file mode 100644 index 0000000000..7ce7e7fd28 --- /dev/null +++ b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using BizHawk.Emulation.Common; + +namespace BizHawk.Client.Common +{ + public class ZwinderStateManager : IStateManager + { + 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 int _ancientInterval; + + public ZwinderStateManager(ZwinderStateManagerSettings settings) + { + Settings = settings; + + _current = new ZwinderBuffer(new RewindConfig + { + UseCompression = settings.CurrentUseCompression, + BufferSize = settings.CurrentBufferSize, + TargetFrameLength = settings.CurrentTargetFrameLength + }); + _recent = new ZwinderBuffer(new RewindConfig + { + UseCompression = settings.RecentUseCompression, + BufferSize = settings.RecentBufferSize, + TargetFrameLength = settings.RecentTargetFrameLength + }); + + _highPriority = new ZwinderBuffer(new RewindConfig + { + UseCompression = settings.PriorityUseCompression, + BufferSize = settings.PriorityBufferSize, + TargetFrameLength = settings.PriorityTargetFrameLength + }); + + _ancientInterval = settings.AncientStateInterval; + _originalState = NonState; + } + + public ZwinderStateManager() + :this(new ZwinderStateManagerSettings()) + { + } + + public void Engage(byte[] frameZeroState) + { + _originalState = (byte[])frameZeroState.Clone(); + } + + private ZwinderStateManager(ZwinderBuffer current, ZwinderBuffer recent, ZwinderBuffer highPriority, byte[] frameZeroState, int ancientInterval) + { + _originalState = (byte[])frameZeroState.Clone(); + _current = current; + _recent = recent; + _highPriority = highPriority; + _ancientInterval = ancientInterval; + } + + public byte[] this[int frame] + { + get + { + var kvp = GetStateClosestToFrame(frame + 1); + if (kvp.Key != frame) + return NonState; + var ms = new MemoryStream(); + kvp.Value.CopyTo(ms); + return ms.ToArray(); + } + } + + // 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; + + private class StateInfo + { + public int Frame { get; } + public Func Read { get; } + public StateInfo(ZwinderBuffer.StateInformation si) + { + Frame = si.Frame; + Read = si.GetReadStream; + } + public StateInfo(KeyValuePair kvp) + :this(kvp.Key, kvp.Value) + { + } + public StateInfo(int frame, byte[] data) + { + Frame = frame; + Read = () => new MemoryStream(data, false); + } + } + + /// + /// Enumerate all states, excepting high priority, in reverse order + /// + /// + private IEnumerable NormalStates() + { + for (var i = _current.Count - 1; i >= 0; i--) + { + yield return new StateInfo(_current.GetState(i)); + } + for (var i = _recent.Count - 1; i >= 0; i--) + { + 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() + { + for (var i = _highPriority.Count - 1; i >= 0; i--) + { + yield return new StateInfo(_highPriority.GetState(i)); + } + } + + /// + /// Enumerate all states in reverse order + /// + private 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(); + } + } + } + + public int Last => AllStates().First().Frame; + + public void Capture(int frame, IBinaryStateable source, bool force = false) + { + if (frame <= Last) + { + CaptureHighPriority(frame, source); + } + + _current.Capture(frame, + s => source.SaveStateBinary(new BinaryWriter(s)), + index => + { + var state = _current.GetState(index); + _recent.Capture(state.Frame, + s => state.GetReadStream().CopyTo(s), + index2 => + { + var state2 = _recent.GetState(index2); + var from = _ancient.Count > 0 ? _ancient[_ancient.Count - 1].Key : 0; + if (state2.Frame - from >= _ancientInterval) + { + var ms = new MemoryStream(); + state2.GetReadStream().CopyTo(ms); + _ancient.Add(new KeyValuePair(state2.Frame, ms.ToArray())); + } + }); + }, + force); + } + + public void CaptureHighPriority(int frame, IBinaryStateable source) + { + _highPriority.Capture(frame, s => source.SaveStateBinary(new BinaryWriter(s))); + } + + public void Clear() + { + _current.InvalidateEnd(0); + _recent.InvalidateEnd(0); + _highPriority.InvalidateEnd(0); + _ancient.Clear(); + } + + public KeyValuePair GetStateClosestToFrame(int frame) + { + if (frame <= 0) + throw new ArgumentOutOfRangeException(nameof(frame)); + + var si = AllStates().First(s => s.Frame < frame); + return new KeyValuePair(si.Frame, si.Read()); + } + + public bool HasState(int frame) + { + return AllStates().Any(s => s.Frame == frame); + } + + private bool InvalidateHighPriority(int frame) + { + for (var i = 0; i < _highPriority.Count; i++) + { + if (_highPriority.GetState(i).Frame >= frame) + { + _highPriority.InvalidateEnd(i); + return true; + } + } + return false; + } + + 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) + { + _recent.InvalidateEnd(i); + _current.InvalidateEnd(0); + return true; + } + } + for (var i = 0; i < _current.Count; i++) + { + if (_current.GetState(i).Frame >= frame) + { + _current.InvalidateEnd(i); + return true; + } + } + return false; + } + + public void UpdateSettings(ZwinderStateManagerSettings settings) => Settings = settings; + + public bool Invalidate(int frame) + { + if (frame <= 0) + throw new ArgumentOutOfRangeException(nameof(frame)); + var b1 = InvalidateNormal(frame); + var b2 = InvalidateHighPriority(frame); + return b1 || b2; + } + + public static ZwinderStateManager Create(BinaryReader br, ZwinderStateManagerSettings settings) + { + var current = ZwinderBuffer.Create(br); + var recent = ZwinderBuffer.Create(br); + var highPriority = ZwinderBuffer.Create(br); + + var original = br.ReadBytes(br.ReadInt32()); + + var ancientInterval = br.ReadInt32(); + + var ret = new ZwinderStateManager(current, recent, highPriority, original, ancientInterval) + { + Settings = settings + }; + + var ancientCount = br.ReadInt32(); + for (var i = 0; i < ancientCount; i++) + { + var key = br.ReadInt32(); + var length = br.ReadInt32(); + var data = br.ReadBytes(length); + ret._ancient.Add(new KeyValuePair(key, data)); + } + + return ret; + } + + public void SaveStateHistory(BinaryWriter bw) + { + _current.SaveStateBinary(bw); + _recent.SaveStateBinary(bw); + _highPriority.SaveStateBinary(bw); + + bw.Write(_originalState.Length); + bw.Write(_originalState); + + bw.Write(_ancientInterval); + + bw.Write(_ancient.Count); + foreach (var s in _ancient) + { + bw.Write(s.Key); + bw.Write(s.Value.Length); + bw.Write(s.Value); + } + } + } +} diff --git a/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManagerSettings.cs b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManagerSettings.cs new file mode 100644 index 0000000000..9d39642911 --- /dev/null +++ b/src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManagerSettings.cs @@ -0,0 +1,77 @@ +using System.ComponentModel; + +namespace BizHawk.Client.Common +{ + public class ZwinderStateManagerSettings + { + public ZwinderStateManagerSettings() { } + + public ZwinderStateManagerSettings(ZwinderStateManagerSettings settings) + { + CurrentUseCompression = settings.CurrentUseCompression; + CurrentBufferSize = settings.CurrentBufferSize; + CurrentTargetFrameLength = settings.CurrentTargetFrameLength; + + RecentUseCompression = settings.RecentUseCompression; + RecentBufferSize = settings.RecentBufferSize; + RecentTargetFrameLength = settings.RecentTargetFrameLength; + + PriorityUseCompression = settings.PriorityUseCompression; + PriorityBufferSize = settings.PriorityBufferSize; + PriorityTargetFrameLength = settings.PriorityTargetFrameLength; + + AncientStateInterval = settings.AncientStateInterval; + SaveStateHistory = settings.SaveStateHistory; + } + + /// + /// Buffer settings when navigating near now + /// + [DisplayName("Current - Use Compression")] + public bool CurrentUseCompression { get; set; } + + [DisplayName("Current - Buffer Size")] + [Description("Max amount of buffer space to use in MB")] + public int CurrentBufferSize { get; set; } = 64; + + [DisplayName("Current - Target Frame Length")] + [Description("Desired frame length (number of emulated frames you can go back before running out of buffer)")] + public int CurrentTargetFrameLength { get; set; } = 1000; + + /// + /// Buffer settings when navigating directly before the Current buffer + /// + [DisplayName("Recent - Use Compression")] + public bool RecentUseCompression { get; set; } + + [DisplayName("Recent - Buffer Size")] + [Description("Max amount of buffer space to use in MB")] + public int RecentBufferSize { get; set; } = 64; + + [DisplayName("Recent - Target Frame Length")] + [Description("Desired frame length (number of emulated frames you can go back before running out of buffer)")] + public int RecentTargetFrameLength { get; set; } = 10000; + + /// + /// Priority States for special use cases + /// + [DisplayName("Priority - Use Compression")] + public bool PriorityUseCompression { get; set; } + + [DisplayName("Priority - Buffer Size")] + [Description("Max amount of buffer space to use in MB")] + public int PriorityBufferSize { get; set; } = 64; + + [DisplayName("Priority - 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; + + [DisplayName("Ancient State Interval")] + [Description("How often to maintain states when outside of Current and Recent intervals")] + public int AncientStateInterval { get; set; } = 5000; + + [DisplayName("Save Savestate History")] + [Description("Whether or not to save savestate history into .tasproj files")] + public bool SaveStateHistory { get; set; } = true; + } +} diff --git a/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs b/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs index 31585a1194..3610c448e7 100644 --- a/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs +++ b/src/BizHawk.Client.Common/rewind/ZwinderBuffer.cs @@ -1,14 +1,11 @@ using System; -using System.Collections.Generic; using System.IO; using System.IO.Compression; - using BizHawk.Common; -using BizHawk.Emulation.Common; namespace BizHawk.Client.Common { - public class ZwinderBuffer : IBinaryStateable + public class ZwinderBuffer { /* Main goals: @@ -234,24 +231,53 @@ namespace BizHawk.Client.Common writer.Write(_nextStateIndex); } - public void LoadStateBinary(BinaryReader reader) - { - if (reader.ReadInt64() != Size) throw new InvalidOperationException("Bad format"); - if (reader.ReadInt64() != _sizeMask) throw new InvalidOperationException("Bad format"); - if (reader.ReadInt32() != _targetFrameLength) throw new InvalidOperationException("Bad format"); - if (reader.ReadBoolean() != _useCompression) throw new InvalidOperationException("Bad format"); + // public void LoadStateBinary(BinaryReader reader) + // { + // if (reader.ReadInt64() != Size) + // throw new InvalidOperationException("Bad format"); + // if (reader.ReadInt64() != _sizeMask) + // throw new InvalidOperationException("Bad format"); + // if (reader.ReadInt32() != _targetFrameLength) + // throw new InvalidOperationException("Bad format"); + // if (reader.ReadBoolean() != _useCompression) + // throw new InvalidOperationException("Bad format"); + // LoadStateBodyBinary(reader); + // } + + private void LoadStateBodyBinary(BinaryReader reader) + { reader.Read(_buffer, 0, _buffer.Length); for (var i = 0; i < _states.Length; i++) { - _states[i].Start = reader.Read(); - _states[i].Size = reader.Read(); - _states[i].Frame = reader.Read(); + _states[i].Start = reader.ReadInt64(); + _states[i].Size = reader.ReadInt32(); + _states[i].Frame = reader.ReadInt32(); } _firstStateIndex = reader.ReadInt32(); _nextStateIndex = reader.ReadInt32(); } + public static ZwinderBuffer Create(BinaryReader reader) + { + var size = reader.ReadInt64(); + var sizeMask = reader.ReadInt64(); + var targetFrameLength = reader.ReadInt32(); + var useCompression = reader.ReadBoolean(); + var ret = new ZwinderBuffer(new RewindConfig + { + BufferSize = (int)(size >> 20), + TargetFrameLength = targetFrameLength, + UseCompression = useCompression + }); + if (ret.Size != size || ret._sizeMask != sizeMask) + { + throw new InvalidOperationException("Bad format"); + } + ret.LoadStateBodyBinary(reader); + return ret; + } + private class SaveStateStream : Stream, ISpanStream { /// diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs index 6d929d27e7..b45b689935 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/BookmarksBranchesBox.cs @@ -2,9 +2,11 @@ using System.Collections.Generic; using System.ComponentModel; using System.Drawing; +using System.IO; using System.Linq; using System.Windows.Forms; using BizHawk.Client.Common; +using BizHawk.Emulation.Common; using BizHawk.Client.EmuHawk.Properties; namespace BizHawk.Client.EmuHawk @@ -193,9 +195,8 @@ namespace BizHawk.Client.EmuHawk } Movie.LoadBranch(branch); - var stateInfo = new KeyValuePair(branch.Frame, branch.CoreData); - Tastudio.LoadState(stateInfo); - Movie.TasStateManager.Capture(true); + Tastudio.LoadState(new KeyValuePair(branch.Frame, new MemoryStream(branch.CoreData, false))); + Movie.TasStateManager.Capture(Tastudio.Emulator.Frame, Tastudio.Emulator.AsStatable(), true); QuickBmpFile.Copy(new BitmapBufferVideoProvider(branch.CoreFrameBuffer), Tastudio.VideoProvider); if (Tastudio.Settings.OldControlSchemeForBranches && Tastudio.TasPlaybackBox.RecordingMode) diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/DefaultGreenzoneSettings.Designer.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/DefaultGreenzoneSettings.Designer.cs index ef44512aaa..34290193c9 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/DefaultGreenzoneSettings.Designer.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/DefaultGreenzoneSettings.Designer.cs @@ -86,7 +86,7 @@ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.CancelBtn; - this.ClientSize = new System.Drawing.Size(365, 301); + this.ClientSize = new System.Drawing.Size(400, 301); this.Controls.Add(this.SettingsPropertyGrid); this.Controls.Add(this.DefaultsButton); this.Controls.Add(this.OkBtn); diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/DefaultGreenzoneSettings.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/DefaultGreenzoneSettings.cs index 648ca7e9b3..0d3979022b 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/DefaultGreenzoneSettings.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/DefaultGreenzoneSettings.cs @@ -7,10 +7,10 @@ namespace BizHawk.Client.EmuHawk { public partial class DefaultGreenzoneSettings : Form { - private readonly Action _saveSettings; - private TasStateManagerSettings _settings; + private readonly Action _saveSettings; + private ZwinderStateManagerSettings _settings; - public DefaultGreenzoneSettings(TasStateManagerSettings settings, Action saveSettings) + public DefaultGreenzoneSettings(ZwinderStateManagerSettings settings, Action saveSettings) { InitializeComponent(); Icon = Properties.Resources.TAStudioIcon; @@ -33,7 +33,7 @@ namespace BizHawk.Client.EmuHawk private void DefaultsButton_Click(object sender, EventArgs e) { - _settings = new TasStateManagerSettings(); + _settings = new ZwinderStateManagerSettings(); SettingsPropertyGrid.SelectedObject = _settings; } } diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs index 785382fa52..8cefde9110 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.MenuItems.cs @@ -321,7 +321,7 @@ namespace BizHawk.Client.EmuHawk && (Clipboard.GetDataObject()?.GetDataPresent(DataFormats.StringFormat) ?? false); ClearGreenzoneMenuItem.Enabled = - CurrentTasMovie != null && CurrentTasMovie.TasStateManager.Any(); + CurrentTasMovie != null && CurrentTasMovie.TasStateManager.Count > 1; GreenzoneICheckSeparator.Visible = StateHistoryIntegrityCheckMenuItem.Visible = @@ -997,18 +997,23 @@ namespace BizHawk.Client.EmuHawk UpdateChangesIndicator(); } + private void UpdateStateSettings(ZwinderStateManagerSettings settings) + { + Config.Movies.DefaultTasStateManagerSettings = settings; + CurrentTasMovie.TasStateManager.UpdateSettings(settings); + } + private void StateHistorySettingsMenuItem_Click(object sender, EventArgs e) { new DefaultGreenzoneSettings( - CurrentTasMovie.TasStateManager.Settings, - s => { CurrentTasMovie.TasStateManager.Settings = s; }) + new ZwinderStateManagerSettings(Config.Movies.DefaultTasStateManagerSettings), + UpdateStateSettings) { Location = this.ChildPointToScreen(TasView), Text = "Savestate History Settings", Owner = Owner }.ShowDialog(); - CurrentTasMovie.TasStateManager.UpdateStateFrequency(); UpdateChangesIndicator(); } @@ -1027,7 +1032,7 @@ namespace BizHawk.Client.EmuHawk private void DefaultStateSettingsMenuItem_Click(object sender, EventArgs e) { new DefaultGreenzoneSettings( - new TasStateManagerSettings(Config.Movies.DefaultTasStateManagerSettings), + new ZwinderStateManagerSettings(Config.Movies.DefaultTasStateManagerSettings), s => { Config.Movies.DefaultTasStateManagerSettings = s; }) { Location = this.ChildPointToScreen(TasView), diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Navigation.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Navigation.cs index 4d055de4b1..1d23922fdc 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Navigation.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.Navigation.cs @@ -54,7 +54,7 @@ namespace BizHawk.Client.EmuHawk // Simply getting the last state doesn't work if that state is the frame. // display isn't saved in the state, need to emulate to frame - var lastState = CurrentTasMovie.TasStateManager.GetStateClosestToFrame(frame); + var lastState = CurrentTasMovie.TasStateManager.GetStateClosestToFrame(frame <= 0 ? 1 : frame); if (lastState.Key > Emulator.Frame) { LoadState(lastState); diff --git a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs index 0ce5d091ab..4dd3a2d78d 100644 --- a/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs +++ b/src/BizHawk.Client.EmuHawk/tools/TAStudio/TAStudio.cs @@ -540,7 +540,7 @@ namespace BizHawk.Client.EmuHawk _engaged = false; var newMovie = (ITasMovie)MovieSession.Get(file.FullName); newMovie.BindMarkersToInput = Settings.BindMarkersToInput; - newMovie.TasStateManager.InvalidateCallback = GreenzoneInvalidated; + newMovie.GreenzoneInvalidated = GreenzoneInvalidated; if (!HandleMovieLoadStuff(newMovie)) { @@ -595,9 +595,9 @@ namespace BizHawk.Client.EmuHawk var filename = DefaultTasProjName(); // TODO don't do this, take over any mainform actions that can crash without a filename var tasMovie = (ITasMovie)MovieSession.Get(filename); tasMovie.BindMarkersToInput = Settings.BindMarkersToInput; - - tasMovie.TasStateManager.InvalidateCallback = GreenzoneInvalidated; + + tasMovie.GreenzoneInvalidated = GreenzoneInvalidated; tasMovie.PropertyChanged += TasMovie_OnPropertyChanged; tasMovie.PopulateWithDefaultHeaderValues( @@ -611,7 +611,6 @@ namespace BizHawk.Client.EmuHawk tasMovie.Save(); if (HandleMovieLoadStuff(tasMovie)) { - CurrentTasMovie.TasStateManager.Capture(); // Capture frame 0 always. } // clear all selections @@ -657,7 +656,6 @@ namespace BizHawk.Client.EmuHawk ResumeLayout(); if (result) { - CurrentTasMovie.TasStateManager.Capture(); // Capture frame 0 always. BookMarkControl.UpdateTextColumnWidth(); MarkerControl.UpdateTextColumnWidth(); } @@ -910,7 +908,7 @@ namespace BizHawk.Client.EmuHawk _unpauseAfterSeeking = (fromRewinding || WasRecording) && !MainForm.EmulatorPaused; TastudioPlayMode(); - var closestState = CurrentTasMovie.TasStateManager.GetStateClosestToFrame(frame); + var closestState = CurrentTasMovie.TasStateManager.GetStateClosestToFrame(frame <= 0 ? 1 : frame); if (closestState.Value.Length > 0 && (frame < Emulator.Frame || closestState.Key > Emulator.Frame)) { LoadState(closestState); @@ -960,9 +958,9 @@ namespace BizHawk.Client.EmuHawk } } - public void LoadState(KeyValuePair state) + public void LoadState(KeyValuePair state) { - StatableEmulator.LoadStateBinary(state.Value); + StatableEmulator.LoadStateBinary(new BinaryReader(state.Value)); if (state.Key == 0 && CurrentTasMovie.StartsFromSavestate) { diff --git a/src/BizHawk.Tests/Client.Common/Movie/MovieServiceTests.cs b/src/BizHawk.Tests/Client.Common/Movie/MovieServiceTests.cs new file mode 100644 index 0000000000..d16498f29b --- /dev/null +++ b/src/BizHawk.Tests/Client.Common/Movie/MovieServiceTests.cs @@ -0,0 +1,23 @@ +using BizHawk.Client.Common; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BizHawk.Common.Tests.Client.Common.Movie +{ + [TestClass] + public class MovieServiceTests + { + [TestMethod] + [DataRow(null, 1.0)] + [DataRow("", 1.0)] + [DataRow(" ", 1.0)] + [DataRow("NonsenseString", 1.0)] + [DataRow("BizHawk v2.0", 1.0)] + [DataRow("BizHawk v2.0 Tasproj v1.0", 1.0)] + [DataRow("BizHawk v2.0 Tasproj v1.1", 1.1)] + public void ParseTasMovieVersion(string movieVersion, double expected) + { + var actual = MovieService.ParseTasMovieVersion(movieVersion); + Assert.AreEqual(expected, actual); + } + } +} diff --git a/src/BizHawk.Tests/Client.Common/Movie/ZwinderStateManagerTests.cs b/src/BizHawk.Tests/Client.Common/Movie/ZwinderStateManagerTests.cs new file mode 100644 index 0000000000..8ce24f3ca4 --- /dev/null +++ b/src/BizHawk.Tests/Client.Common/Movie/ZwinderStateManagerTests.cs @@ -0,0 +1,84 @@ +using System.IO; +using BizHawk.Client.Common; +using BizHawk.Emulation.Common; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BizHawk.Common.Tests.Client.Common.Movie +{ + [TestClass] + public class ZwinderStateManagerTests + { + [TestMethod] + public void SaveCreateRoundTrip() + { + var ms = new MemoryStream(); + var zw = new ZwinderStateManager(); + zw.SaveStateHistory(new BinaryWriter(ms)); + var buff = ms.ToArray(); + var rms = new MemoryStream(buff, false); + + var zw2 = ZwinderStateManager.Create(new BinaryReader(rms), new ZwinderStateManagerSettings()); + + // TODO: we could assert more things here to be thorough + Assert.IsNotNull(zw2); + Assert.AreEqual(zw.Settings.CurrentBufferSize, zw2.Settings.CurrentBufferSize); + Assert.AreEqual(zw.Settings.RecentBufferSize, zw2.Settings.RecentBufferSize); + } + + [TestMethod] + public void SomethingSomething() + { + 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 + }); + { + var ms = new MemoryStream(); + ss.SaveStateBinary(new BinaryWriter(ms)); + zw.Engage(ms.ToArray()); + } + for (int frame = 0; frame <= 10440; frame++) + { + ss.Frame = frame; + zw.Capture(frame, ss); + } + var kvp = zw.GetStateClosestToFrame(10440); + var actual = StateSource.GetFrameNumberInState(kvp.Value); + Assert.AreEqual(kvp.Key, actual); + Assert.IsTrue(actual < 10440); + } + + private class StateSource : IBinaryStateable + { + public int Frame { get; set; } + public byte[] PaddingData { get; set; } = new byte[0]; + public void LoadStateBinary(BinaryReader reader) + { + Frame = reader.ReadInt32(); + reader.Read(PaddingData, 0, PaddingData.Length); + } + + public void SaveStateBinary(BinaryWriter writer) + { + writer.Write(Frame); + writer.Write(PaddingData); + } + + public static int GetFrameNumberInState(Stream stream) + { + var ss = new StateSource(); + ss.LoadStateBinary(new BinaryReader(stream)); + return ss.Frame; + } + } + } +}