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 <goyuken@gmail.com>
This commit is contained in:
adelikat 2020-08-05 20:39:15 -05:00 committed by GitHub
parent fcbe8333a6
commit 357d87239b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 693 additions and 694 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -78,5 +78,7 @@ namespace BizHawk.Client.Common
IMovie Get(string path);
string BackupDirectory { get; set; }
void PopupMessage(string message);
}
}

View File

@ -23,6 +23,8 @@ namespace BizHawk.Client.Common
IStringLog VerificationLog { get; }
int LastEditedFrame { get; }
Action<int> GreenzoneInvalidated { get; set; }
string DisplayValue(int frame, string buttonName);
void FlagChanges();
void ClearChanges();

View File

@ -9,60 +9,40 @@ namespace BizHawk.Client.Common
{
/// <summary>
/// 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.
/// </summary>
/// <returns>A savestate for the given frame or an empty array if there isn't one</returns>
byte[] this[int frame] { get; }
/// <summary>
/// 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
/// </summary>
/// <exception cref="InvalidOperationException">
/// Thrown if attempting to attach a core when one is already attached
/// or if the given core does not meet all required dependencies
/// </exception>
void Attach(IEmulator emulator);
TasStateManagerSettings Settings { get; set; }
Action<int> InvalidateCallback { set; }
ZwinderStateManagerSettings Settings { get; }
/// <summary>
/// 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
/// </summary>
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);
/// <summary>
/// Clears out all savestates after the given frame number
/// Clears out all savestates after or at the given frame number
/// </summary>
bool Invalidate(int frame);
// Remove all states, but not the frame 0 state
void Clear();
void Save(BinaryWriter bw);
void Load(BinaryReader br);
/// <summary>
/// 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.
/// </summary>
/// <param name="frame"></param>
/// <returns></returns>
KeyValuePair<int, byte[]> GetStateClosestToFrame(int frame);
/// <summary>
/// Returns true iff Count > 0
/// TODO: Surely this is always true because the frame 0 state is always retained?
/// </summary>
/// <returns></returns>
bool Any();
/// <returns>This stream may be consumed only once, and before any other calls to statemanager occur</returns>
KeyValuePair<int, Stream> GetStateClosestToFrame(int frame);
/// <summary>
/// Returns the total number of states currently held by the state manager
@ -77,14 +57,20 @@ namespace BizHawk.Client.Common
int Last { get; }
/// <summary>
/// Adjust internal state saving logic based on changes to Settings
/// Updates the internal state saving logic settings
/// </summary>
void UpdateStateFrequency();
void UpdateSettings(ZwinderStateManagerSettings settings);
/// <summary>
/// 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
/// </summary>
bool Remove(int frame);
void SaveStateHistory(BinaryWriter bw);
/// <summary>
/// Enables the instance to be used. An instance of <see cref="IStateManager"/> should not
/// be useable until this method is called
/// </summary>
/// <param name="frameZeroState"></param>
void Engage(byte[] frameZeroState);
}
}

View File

@ -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<int> _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;
}
/// <summary>
/// This will strategically remove states based on their alignment with the state gap (and its multiples) and their distance from the current frame.
/// </summary>
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<KeyValuePair<int, int>> decayPriorities = new List<KeyValuePair<int, int>>();
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<int, int>(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<int> { _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]++;
}
}
}
}
}

View File

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

View File

@ -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<TasStateManagerSettings>(json);
var settings = JsonConvert.DeserializeObject<ZwinderStateManagerSettings>(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);
});
}
}

View File

@ -12,16 +12,18 @@ namespace BizHawk.Client.Common
public new const string Extension = "tasproj";
private IInputPollable _inputPollable;
public const double CurrentVersion = 1.1;
/// <exception cref="InvalidOperationException">loaded core does not implement <see cref="IStatable"/></exception>
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<int> 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;

View File

@ -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
{
/// <summary>
/// Captures savestates and manages the logic of adding, retrieving,
/// invalidating/clearing of states. Also does memory management and limiting of states
/// </summary>
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<int, byte[]> _states = new SortedList<int, byte[]>();
private double _expectedStateSizeInMb;
private ulong _used;
private int _stateFrequency;
private int MaxStates => (int)(Settings.CapacityMb / _expectedStateSizeInMb + 1);
private int FileStateGap => 1 << Settings.FileStateGap;
/// <exception cref="InvalidOperationException">loaded core expects savestate size of <c>0 B</c></exception>
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<int, byte[]>(MaxStates); }
UpdateStateFrequency();
}
public Action<int> 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);
}
/// <returns>true iff any frames were invalidated</returns>
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<int> 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<int, byte[]> GetStateClosestToFrame(int frame)
{
var s = _states.LastOrDefault(state => state.Key < frame);
if (s.Key > 0)
{
return s;
}
return new KeyValuePair<int, byte[]>(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<int> ExcludeStates()
{
List<int> ret = new List<int>();
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;
}
}
}

View File

@ -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;
}
/// <summary>
/// Whether or not to save state history information to disk
/// </summary>
[DisplayName("Save History")]
[Description("Whether or not to use savestate history")]
public bool SaveStateHistory => DiskSaveCapacityMb != 0;
/// <summary>
/// Gets or sets the size limit to use when saving the TAS project to disk.
/// </summary>
[DisplayName("Save Capacity (in megabytes)")]
[Description("The size limit to use when saving the tas project to disk.")]
public int DiskSaveCapacityMb { get; set; }
/// <summary>
/// Gets or sets the total amount of memory to devote to state history in megabytes
/// </summary>
[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; }
/// <summary>
/// Gets or sets the total amount of disk space to devote to state history in megabytes
/// </summary>
[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; }
/// <summary>
/// Gets or sets the divider that determines memory state gap
/// </summary>
[DisplayName("Divider for memory state interval")]
[Description("The actual state gap in frames is calculated as ExpectedStateSizeMB * 1024 / div")]
public int MemStateGapDividerKB { get; set; }
/// <summary>
/// Gets or sets the amount of states to skip during project saving
/// </summary>
[DisplayName("State interval for .tasproj")]
[Description("The actual state gap in frames is calculated as Nth power on 2")]
public int FileStateGap { get; set; }
}
}

View File

@ -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<KeyValuePair<int, byte[]>> _ancient = new List<KeyValuePair<int, byte[]>>();
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<Stream> Read { get; }
public StateInfo(ZwinderBuffer.StateInformation si)
{
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;
Read = () => new MemoryStream(data, false);
}
}
/// <summary>
/// Enumerate all states, excepting high priority, in reverse order
/// </summary>
/// <returns></returns>
private IEnumerable<StateInfo> 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);
}
/// <summary>
/// Enumerate high priority states in reverse order
/// </summary>
/// <returns></returns>
private IEnumerable<StateInfo> HighPriorityStates()
{
for (var i = _highPriority.Count - 1; i >= 0; i--)
{
yield return new StateInfo(_highPriority.GetState(i));
}
}
/// <summary>
/// Enumerate all states in reverse order
/// </summary>
private 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();
}
}
}
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<int, byte[]>(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<int, Stream> GetStateClosestToFrame(int frame)
{
if (frame <= 0)
throw new ArgumentOutOfRangeException(nameof(frame));
var si = AllStates().First(s => s.Frame < frame);
return new KeyValuePair<int, Stream>(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<int, byte[]>(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);
}
}
}
}

View File

@ -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;
}
/// <summary>
/// Buffer settings when navigating near now
/// </summary>
[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;
/// <summary>
/// Buffer settings when navigating directly before the Current buffer
/// </summary>
[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;
/// <summary>
/// Priority States for special use cases
/// </summary>
[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;
}
}

View File

@ -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
{
/// <summary>

View File

@ -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<int, byte[]>(branch.Frame, branch.CoreData);
Tastudio.LoadState(stateInfo);
Movie.TasStateManager.Capture(true);
Tastudio.LoadState(new KeyValuePair<int, Stream>(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)

View File

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

View File

@ -7,10 +7,10 @@ namespace BizHawk.Client.EmuHawk
{
public partial class DefaultGreenzoneSettings : Form
{
private readonly Action<TasStateManagerSettings> _saveSettings;
private TasStateManagerSettings _settings;
private readonly Action<ZwinderStateManagerSettings> _saveSettings;
private ZwinderStateManagerSettings _settings;
public DefaultGreenzoneSettings(TasStateManagerSettings settings, Action<TasStateManagerSettings> saveSettings)
public DefaultGreenzoneSettings(ZwinderStateManagerSettings settings, Action<ZwinderStateManagerSettings> 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;
}
}

View File

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

View File

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

View File

@ -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<int, byte[]> state)
public void LoadState(KeyValuePair<int, Stream> state)
{
StatableEmulator.LoadStateBinary(state.Value);
StatableEmulator.LoadStateBinary(new BinaryReader(state.Value));
if (state.Key == 0 && CurrentTasMovie.StartsFromSavestate)
{

View File

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

View File

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