Merge 370f52cf6a
into 56f3a2d692
This commit is contained in:
commit
3272a6526f
|
@ -8,7 +8,7 @@
|
|||
public int MovieCompressionLevel { get; }
|
||||
public bool VBAStyleMovieLoadState { get; }
|
||||
public bool PlaySoundOnMovieEnd { get; set; }
|
||||
ZwinderStateManagerSettings DefaultTasStateManagerSettings { get; }
|
||||
IStateManagerSettings DefaultTasStateManagerSettings { get; }
|
||||
}
|
||||
|
||||
public class MovieConfig : IMovieConfig
|
||||
|
@ -20,6 +20,6 @@
|
|||
public bool VBAStyleMovieLoadState { get; set; }
|
||||
public bool PlaySoundOnMovieEnd { get; set; }
|
||||
|
||||
public ZwinderStateManagerSettings DefaultTasStateManagerSettings { get; set; } = new ZwinderStateManagerSettings();
|
||||
public IStateManagerSettings DefaultTasStateManagerSettings { get; set; } = new PagedStateManager.PagedSettings();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ namespace BizHawk.Client.Common
|
|||
bool BindMarkersToInput { get; set; }
|
||||
|
||||
IMovieChangeLog ChangeLog { get; }
|
||||
IStateManager TasStateManager { get; }
|
||||
IStateManager TasStateManager { get; set; }
|
||||
Func<string> InputRollSettingsForSave { get; set; }
|
||||
string InputRollSettings { get; }
|
||||
ITasMovieRecord this[int index] { get; }
|
||||
|
|
|
@ -6,15 +6,7 @@ namespace BizHawk.Client.Common
|
|||
{
|
||||
public interface IStateManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the savestate for the given frame,
|
||||
/// 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; }
|
||||
|
||||
ZwinderStateManagerSettings Settings { get; }
|
||||
IStateManagerSettings Settings { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Requests that the current emulator state be captured
|
||||
|
@ -52,14 +44,20 @@ namespace BizHawk.Client.Common
|
|||
|
||||
/// <summary>
|
||||
/// Updates the internal state saving logic settings
|
||||
/// May create a new state manager
|
||||
/// </summary>
|
||||
void UpdateSettings(ZwinderStateManagerSettings settings, bool keepOldStates = false);
|
||||
IStateManager UpdateSettings(IStateManagerSettings settings, bool keepOldStates = false);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes the current state of the instance for persisting to disk
|
||||
/// </summary>
|
||||
void SaveStateHistory(BinaryWriter bw);
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the state of the instance that was persisted to disk
|
||||
/// </summary>
|
||||
void LoadStateHistory(BinaryReader br);
|
||||
|
||||
/// <summary>
|
||||
/// Enables the instance to be used. An instance of <see cref="IStateManager"/> should not
|
||||
/// be useable until this method is called
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
namespace BizHawk.Client.Common
|
||||
{
|
||||
public interface IStateManagerSettings
|
||||
{
|
||||
IStateManager CreateManager(Func<int, bool> reserveCallback);
|
||||
|
||||
IStateManagerSettings Clone();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using System.IO;
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
internal class StatableStream : IStatable
|
||||
{
|
||||
public bool AvoidRewind => false;
|
||||
public void LoadStateBinary(BinaryReader reader) => throw new NotImplementedException();
|
||||
|
||||
private Stream _stream;
|
||||
private int _length;
|
||||
public StatableStream(Stream stream, int length)
|
||||
{
|
||||
_stream = stream;
|
||||
_length = length;
|
||||
}
|
||||
public void SaveStateBinary(BinaryWriter writer)
|
||||
{
|
||||
int copied = 0;
|
||||
const int bufferSize = 81920; // It's the default of CopyTo's buffer size
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
while (copied < _length - bufferSize)
|
||||
{
|
||||
if (_stream.Read(buffer, 0, bufferSize) != bufferSize)
|
||||
throw new Exception("Unexpected end of stream.");
|
||||
writer.Write(buffer);
|
||||
copied += bufferSize;
|
||||
}
|
||||
int remaining = _length - copied;
|
||||
if (_stream.Read(buffer, 0, remaining) != remaining)
|
||||
throw new Exception("Unexpected end of stream.");
|
||||
writer.Write(buffer, 0, remaining);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -135,50 +135,51 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
});
|
||||
|
||||
var settings = new ZwinderStateManagerSettings();
|
||||
IStateManagerSettings settings = Session.Settings.DefaultTasStateManagerSettings;
|
||||
bl.GetLump(BinaryStateLump.StateHistorySettings, abort: false, tr =>
|
||||
{
|
||||
var json = tr.ReadToEnd();
|
||||
try
|
||||
{
|
||||
settings = JsonConvert.DeserializeObject<ZwinderStateManagerSettings>(json);
|
||||
settings = JsonConvert.DeserializeObject<IStateManagerSettings>(json, new JsonSerializerSettings()
|
||||
{
|
||||
TypeNameHandling = TypeNameHandling.Objects,
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Do nothing, and use default settings instead
|
||||
settings = Session.Settings.DefaultTasStateManagerSettings;
|
||||
}
|
||||
});
|
||||
|
||||
TasStateManager?.Dispose();
|
||||
bool badHistory = false;
|
||||
var hasHistory = bl.GetLump(BinaryStateLump.StateHistory, abort: false, br =>
|
||||
{
|
||||
try
|
||||
{
|
||||
TasStateManager = ZwinderStateManager.Create(br, settings, IsReserved);
|
||||
TasStateManager = settings.CreateManager(IsReserved);
|
||||
TasStateManager.LoadStateHistory(br);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Continue with a fresh manager. If state history got corrupted, the file is still very much useable
|
||||
// and we would want the user to be able to load, and regenerate their state history
|
||||
// however, we still have an issue of how state history got corrupted
|
||||
TasStateManager = new ZwinderStateManager(
|
||||
Session.Settings.DefaultTasStateManagerSettings,
|
||||
IsReserved);
|
||||
badHistory = true;
|
||||
Session.PopupMessage("State history was corrupted, clearing and working with a fresh history.");
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasHistory)
|
||||
if (!hasHistory || badHistory)
|
||||
{
|
||||
try
|
||||
{
|
||||
TasStateManager = new ZwinderStateManager(settings, IsReserved);
|
||||
TasStateManager = settings.CreateManager(IsReserved);
|
||||
}
|
||||
catch
|
||||
{
|
||||
TasStateManager = new ZwinderStateManager(
|
||||
Session.Settings.DefaultTasStateManagerSettings,
|
||||
IsReserved);
|
||||
TasStateManager = Session.Settings.DefaultTasStateManagerSettings.CreateManager(IsReserved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ namespace BizHawk.Client.Common
|
|||
|
||||
_inputPollable = emulator.AsInputPollable();
|
||||
|
||||
TasStateManager ??= new ZwinderStateManager(Session.Settings.DefaultTasStateManagerSettings, IsReserved);
|
||||
TasStateManager ??= Session.Settings.DefaultTasStateManagerSettings.CreateManager(IsReserved);
|
||||
if (StartsFromSavestate)
|
||||
{
|
||||
TasStateManager.Engage(BinarySavestate);
|
||||
|
@ -81,7 +81,7 @@ namespace BizHawk.Client.Common
|
|||
public TasLagLog LagLog { get; } = new TasLagLog();
|
||||
|
||||
public override string PreferredExtension => Extension;
|
||||
public IStateManager TasStateManager { get; private set; }
|
||||
public IStateManager TasStateManager { get; set; }
|
||||
|
||||
public Action<int> GreenzoneInvalidated { get; set; }
|
||||
|
||||
|
|
|
@ -36,12 +36,6 @@ namespace BizHawk.Client.Common
|
|||
_reserveCallback = reserveCallback;
|
||||
}
|
||||
|
||||
/// <param name="reserveCallback">Called when deciding to evict a state for the given frame, if true is returned, the state will be reserved</param>
|
||||
public ZwinderStateManager(Func<int, bool> reserveCallback)
|
||||
: this(new ZwinderStateManagerSettings(), reserveCallback)
|
||||
{
|
||||
}
|
||||
|
||||
public void Engage(byte[] frameZeroState)
|
||||
{
|
||||
if (!_reserved.ContainsKey(0))
|
||||
|
@ -63,38 +57,38 @@ namespace BizHawk.Client.Common
|
|||
RebuildReserved();
|
||||
}
|
||||
|
||||
public byte[] this[int frame]
|
||||
{
|
||||
get
|
||||
{
|
||||
var (f, dataStream) = GetStateClosestToFrame(frame);
|
||||
if (f != frame)
|
||||
{
|
||||
dataStream.Dispose();
|
||||
return NonState;
|
||||
}
|
||||
|
||||
var data = dataStream.ReadAllBytes();
|
||||
dataStream.Dispose();
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
public ZwinderStateManagerSettings Settings { get; private set; }
|
||||
IStateManagerSettings IStateManager.Settings => Settings;
|
||||
|
||||
public void UpdateSettings(ZwinderStateManagerSettings settings, bool keepOldStates = false)
|
||||
public IStateManager UpdateSettings(IStateManagerSettings settings, bool keepOldStates = false)
|
||||
{
|
||||
bool makeNewReserved = Settings?.AncientStoreType != settings.AncientStoreType;
|
||||
Settings = settings;
|
||||
if (settings is not ZwinderStateManagerSettings zSettings)
|
||||
{
|
||||
IStateManager newManager = settings.CreateManager(_reserveCallback);
|
||||
newManager.Engage(GetStateClosestToFrame(0).Value.ReadAllBytes());
|
||||
if (keepOldStates)
|
||||
{
|
||||
foreach (int frame in StateCache)
|
||||
{
|
||||
Stream ss = GetStateClosestToFrame(frame).Value;
|
||||
newManager.Capture(frame, new StatableStream(ss, (int)ss.Length));
|
||||
}
|
||||
}
|
||||
Dispose();
|
||||
return newManager;
|
||||
}
|
||||
|
||||
_current = UpdateBuffer(_current, settings.Current(), keepOldStates);
|
||||
_recent = UpdateBuffer(_recent, settings.Recent(), keepOldStates);
|
||||
_gapFiller = UpdateBuffer(_gapFiller, settings.GapFiller(), keepOldStates);
|
||||
bool makeNewReserved = Settings?.AncientStoreType != zSettings.AncientStoreType;
|
||||
Settings = zSettings;
|
||||
|
||||
_current = UpdateBuffer(_current, zSettings.Current(), keepOldStates);
|
||||
_recent = UpdateBuffer(_recent, zSettings.Recent(), keepOldStates);
|
||||
_gapFiller = UpdateBuffer(_gapFiller, zSettings.GapFiller(), keepOldStates);
|
||||
|
||||
if (keepOldStates)
|
||||
{
|
||||
// For ancients, let's throw out states if doing so still satisfies the ancient state interval.
|
||||
if (settings.AncientStateInterval > _ancientInterval)
|
||||
if (zSettings.AncientStateInterval > _ancientInterval)
|
||||
{
|
||||
List<int> reservedFrames = _reserved.Keys.ToList();
|
||||
reservedFrames.Sort();
|
||||
|
@ -103,7 +97,7 @@ namespace BizHawk.Client.Common
|
|||
if (_reserveCallback(reservedFrames[i]))
|
||||
continue;
|
||||
|
||||
if (reservedFrames[i + 1] - reservedFrames[i - 1] <= settings.AncientStateInterval)
|
||||
if (reservedFrames[i + 1] - reservedFrames[i - 1] <= zSettings.AncientStateInterval)
|
||||
{
|
||||
EvictReserved(reservedFrames[i]);
|
||||
reservedFrames.RemoveAt(i);
|
||||
|
@ -130,8 +124,10 @@ namespace BizHawk.Client.Common
|
|||
if (makeNewReserved)
|
||||
RebuildReserved();
|
||||
|
||||
_ancientInterval = settings.AncientStateInterval;
|
||||
_ancientInterval = zSettings.AncientStateInterval;
|
||||
RebuildStateCache();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private void RebuildReserved()
|
||||
|
@ -523,19 +519,14 @@ namespace BizHawk.Client.Common
|
|||
return b1 || b2 || b3;
|
||||
}
|
||||
|
||||
public static ZwinderStateManager Create(BinaryReader br, ZwinderStateManagerSettings settings, Func<int, bool> reserveCallback)
|
||||
public void LoadStateHistory(BinaryReader br)
|
||||
{
|
||||
// Initial format had no version number, but I think it's a safe bet no valid file has buffer size 2^56 or more so this should work.
|
||||
int version = br.ReadByte();
|
||||
if (version == 0) throw new Exception("Unsupported GreenZone version.");
|
||||
|
||||
var current = ZwinderBuffer.Create(br, settings.Current(), version == 0);
|
||||
var recent = ZwinderBuffer.Create(br, settings.Recent());
|
||||
var gaps = ZwinderBuffer.Create(br, settings.GapFiller());
|
||||
|
||||
if (version == 0)
|
||||
settings.AncientStateInterval = br.ReadInt32();
|
||||
|
||||
var ret = new ZwinderStateManager(current, recent, gaps, reserveCallback, settings);
|
||||
_current.Load(br);
|
||||
_recent.Load(br);
|
||||
_gapFiller.Load(br);
|
||||
|
||||
var ancientCount = br.ReadInt32();
|
||||
for (var i = 0; i < ancientCount; i++)
|
||||
|
@ -543,12 +534,10 @@ namespace BizHawk.Client.Common
|
|||
var key = br.ReadInt32();
|
||||
var length = br.ReadInt32();
|
||||
var data = br.ReadBytes(length);
|
||||
ret._reserved.Add(key, data);
|
||||
_reserved.Add(key, data);
|
||||
}
|
||||
|
||||
ret.RebuildStateCache();
|
||||
|
||||
return ret;
|
||||
RebuildStateCache();
|
||||
}
|
||||
|
||||
public void SaveStateHistory(BinaryWriter bw)
|
||||
|
|
|
@ -5,7 +5,7 @@ using BizHawk.Common;
|
|||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
public class ZwinderStateManagerSettings
|
||||
public class ZwinderStateManagerSettings : IStateManagerSettings
|
||||
{
|
||||
public ZwinderStateManagerSettings() { }
|
||||
|
||||
|
@ -109,6 +109,13 @@ namespace BizHawk.Client.Common
|
|||
[Description("Where to keep the reserved states.")]
|
||||
public IRewindSettings.BackingStoreType AncientStoreType { get; set; } = IRewindSettings.BackingStoreType.Memory;
|
||||
|
||||
public IStateManager CreateManager(Func<int, bool> reserveCallback)
|
||||
{
|
||||
return new ZwinderStateManager(this, reserveCallback);
|
||||
}
|
||||
|
||||
public IStateManagerSettings Clone() => new ZwinderStateManagerSettings(this);
|
||||
|
||||
// Just to simplify some other code.
|
||||
public RewindConfig Current()
|
||||
{
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using BizHawk.Common;
|
||||
|
||||
|
@ -343,8 +342,12 @@ namespace BizHawk.Client.Common
|
|||
}
|
||||
}
|
||||
|
||||
private void LoadStateBodyBinary(BinaryReader reader)
|
||||
public void Load(BinaryReader reader)
|
||||
{
|
||||
int version = reader.ReadByte();
|
||||
if (version != 1)
|
||||
throw new InvalidOperationException("Bad format");
|
||||
|
||||
_firstStateIndex = 0;
|
||||
_nextStateIndex = reader.ReadInt32();
|
||||
long nextByte = 0;
|
||||
|
@ -359,43 +362,6 @@ namespace BizHawk.Client.Common
|
|||
MemoryBlockUtils.CopySome(reader.BaseStream, _backingStore, nextByte);
|
||||
}
|
||||
|
||||
public static ZwinderBuffer Create(BinaryReader reader, RewindConfig rewindConfig, bool hackyV0 = false)
|
||||
{
|
||||
ZwinderBuffer ret;
|
||||
|
||||
// Initial format had no version number, but I think it's a safe bet no valid file has buffer size 2^56 or more so this should work.
|
||||
int version = hackyV0 ? 0 : reader.ReadByte();
|
||||
if (version == 0)
|
||||
{
|
||||
byte[] sizeArr = new byte[8];
|
||||
reader.Read(sizeArr, 1, 7);
|
||||
var size = MemoryMarshal.Read<long>(sizeArr);
|
||||
var sizeMask = reader.ReadInt64();
|
||||
var targetFrameLength = reader.ReadInt32();
|
||||
var useCompression = reader.ReadBoolean();
|
||||
ret = new ZwinderBuffer(new RewindConfig
|
||||
{
|
||||
BufferSize = (int)(size >> 20),
|
||||
UseFixedRewindInterval = false,
|
||||
TargetFrameLength = targetFrameLength,
|
||||
TargetRewindInterval = 5,
|
||||
AllowOutOfOrderStates = false,
|
||||
UseCompression = useCompression,
|
||||
});
|
||||
if (ret.Size != size || ret._sizeMask != sizeMask)
|
||||
{
|
||||
throw new InvalidOperationException("Bad format");
|
||||
}
|
||||
}
|
||||
else if (version == 1)
|
||||
ret = new ZwinderBuffer(rewindConfig);
|
||||
else
|
||||
throw new InvalidOperationException("Bad format");
|
||||
|
||||
ret.LoadStateBodyBinary(reader);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private sealed class SaveStateStream : Stream, ISpanStream
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
@ -0,0 +1,665 @@
|
|||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using BizHawk.Common;
|
||||
using BizHawk.Common.IOExtensions;
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// This class will manage savestates for TAStudio, with a very similar API as ZwinderStateManager.
|
||||
/// This manager intends to address a shortcoming of Zwinder, which is that it could not accept states out of order.
|
||||
/// Allowing states to be added out of order has two primary benefits:
|
||||
/// 1) Users who load a branch, emulate forward a little, then rewind can still use the full buffer space.
|
||||
/// 2) It becomes much easier to "thin out" states (keeping only a subset of them) for saving and to then use them again on load.
|
||||
///
|
||||
/// Out of order states also means we do not need separately allocated buffers for "current", "recent", etc.
|
||||
/// This gives us some more flexibility, but still there won't be any one-size-fits-all strategy for state managment.
|
||||
/// For the initial implementation I will be using similar settings as Zwinder has but this may change in the future.
|
||||
///
|
||||
/// Additionally, this approach allows us to take the main goals of ZwinderBuffer into a state manager:
|
||||
/// 1. No copies, ever. States are deposited directly to, and read directly from, one giant buffer. (assuming no TempFile storage which is not currently implemented)
|
||||
/// 2. Support for arbitrary and changeable state sizes.
|
||||
/// </summary>
|
||||
public class PagedStateManager : IStateManager, IDisposable
|
||||
{
|
||||
public PagedSettings Settings { get; private set; }
|
||||
IStateManagerSettings IStateManager.Settings => Settings;
|
||||
public class PagedSettings : IStateManagerSettings
|
||||
{
|
||||
// Instead of the user giving a set of memory limits, the user will give just one.
|
||||
// That will be the limit for ALL managed states combined.
|
||||
// We'll have three "groups" of states:
|
||||
// 1) "new": This is similar to Zwinder's "current". It will hold the most recent (highest frame number) states.
|
||||
// 2) "mid": This is similar to Zwinder's "recent". It will hold states older than "new".
|
||||
// 3) "old": This is similar to Zwinder's "ancient". It will hold states that should never be thrown out for being "old" (low frame number relative to the ones in "new").
|
||||
// Instead of having a group like Zwinder's "gap" we will take from the "mid" group.
|
||||
|
||||
// The defaults given here are largely arbitrary. There is no one-size-fits-all and so we make no attempt to fine-tune the defaults.
|
||||
// Users should be encouraged to update the settings if the defaults don't work well for their use case.
|
||||
// For best results, FB (FramesBetween) for mid should be an integer multiple of FB for new. Same for old to mid.
|
||||
|
||||
// Ideally the default would be higher, or based on actual available RAM.
|
||||
// But, auto-save is on by default and saves all states so having more would make that very annoying.
|
||||
[DisplayName("Memory Limit (MB)")]
|
||||
[Description("The amount of RAM to use for savestates. Bigger values gives more states but makes saving take longer.")]
|
||||
[Range(1, 32768)]
|
||||
public int TotalMemoryLimitMB { get; set; } = 1024;
|
||||
|
||||
[DisplayName("Frames Between New States")]
|
||||
[Description("How many frames from one state to the next, for the newest (highst-frame) states.")]
|
||||
[Range(1, int.MaxValue)]
|
||||
public int FramesBetweenNewStates { get; set; } = 4;
|
||||
[DisplayName("Frames Between Middle States")]
|
||||
[Description("How many frames from one state to the next, for the middle states. For best results, this should be a whole number multiple of 'Frames Between New States'.")]
|
||||
[Range(1, int.MaxValue)]
|
||||
public int FramesBetweenMidStates { get; set; } = 20;
|
||||
[DisplayName("Frames Between Old States")]
|
||||
[Description("How many frames from one state to the next, for old states. TAStudio will try to keep old states for the entire duration of the movie. For best results, this should be a whole number multiple of 'Frames Between Middle States'.")]
|
||||
[Range(1, int.MaxValue)]
|
||||
public int FramesBetweenOldStates { get; set; } = 400;
|
||||
|
||||
/// <summary>
|
||||
/// What is the ratio for the number of states in the "new" group vs. in the "mid" group?
|
||||
/// The "old" group will use whatever it uses and the leftover is given to "new" and "mid" according to this ratio.
|
||||
/// </summary>
|
||||
[DisplayName("New To Middle Ratio")]
|
||||
[Description("How many states to consider as 'new' states compared to 'middle' states. A larger value will result in more states using 'Frames Between New States' and a smaller value will have more states using 'Frames Between Middle States'.")]
|
||||
[Range(0.0, double.MaxValue)]
|
||||
public double NewToMidRatio { get; set; } = 2.0;
|
||||
|
||||
[DisplayName("Frames Between Saved States")]
|
||||
[Description("How many frames from one state to the next, when saving a .tasproj. Higher values will result in faster saves.")]
|
||||
[Range(1, int.MaxValue)]
|
||||
public int FramesBetweenSavedStates { get; set; } = 100;
|
||||
|
||||
[DisplayName("Save Marker States")]
|
||||
[Description("If true, states for markers will be saved regardless of what 'Frames Between Saved States' says.")]
|
||||
public bool ForceSaveMarkerStates { get; set; } = false;
|
||||
|
||||
|
||||
public PagedSettings() { }
|
||||
public PagedSettings(PagedSettings other)
|
||||
{
|
||||
TotalMemoryLimitMB = other.TotalMemoryLimitMB;
|
||||
NewToMidRatio = other.NewToMidRatio;
|
||||
|
||||
FramesBetweenNewStates = other.FramesBetweenNewStates;
|
||||
FramesBetweenMidStates = other.FramesBetweenMidStates;
|
||||
FramesBetweenOldStates = other.FramesBetweenOldStates;
|
||||
FramesBetweenSavedStates = other.FramesBetweenSavedStates;
|
||||
|
||||
ForceSaveMarkerStates = other.ForceSaveMarkerStates;
|
||||
}
|
||||
|
||||
public IStateManager CreateManager(Func<int, bool> reserveCallback)
|
||||
{
|
||||
return new PagedStateManager(this, reserveCallback);
|
||||
}
|
||||
|
||||
public IStateManagerSettings Clone() => new PagedSettings(this);
|
||||
}
|
||||
|
||||
public int Count => _states.Count;
|
||||
|
||||
public int Last => _states.Max.Frame;
|
||||
|
||||
private const int PAGE_SIZE = 4096;
|
||||
private const int PAGE_DATA_SIZE = PAGE_SIZE - 4; // PAGE_SIZE minus metadata
|
||||
private readonly MemoryBlock _buffer;
|
||||
private int _firstFree;
|
||||
|
||||
private enum StateGroup
|
||||
{
|
||||
New,
|
||||
Mid,
|
||||
Old,
|
||||
}
|
||||
|
||||
private struct StateInfo: IComparable<StateInfo>
|
||||
{
|
||||
public int FirstPage;
|
||||
public int LastPage;
|
||||
public int Size;
|
||||
public int Frame = -1;
|
||||
|
||||
public StateInfo() { }
|
||||
|
||||
// Use only to aid searching the list!
|
||||
public StateInfo(int f) { Frame = f; }
|
||||
|
||||
public int CompareTo(StateInfo other) => Frame.CompareTo(other.Frame);
|
||||
}
|
||||
/* Our collection of states needs to perform well in all of these tasks:
|
||||
* 1) Inserting states at any point. This will most commonly be at the end, but not always.
|
||||
* 2) Removing states at any point. This will most commonly be at the start, but not always.
|
||||
* 3) Finding the state on a given frame.
|
||||
* 4) Finding the closest state after a given frame.
|
||||
* 5) Finding the closest state before a given frame.
|
||||
*
|
||||
* A note on the implementation: SortedSet provides a method GetViewBetween and this is how we do (4) and (5).
|
||||
* One would expect this to be fast, but it is actually O(n) where n is the number of elements in the view.
|
||||
* To avoid this ludicrous performance shortcoming, calls to GetViewBetween should provide as narrow a range as possible.
|
||||
* This is fixed in newer versions of .net. If we later require a recent version of .net we can simplify our code here.
|
||||
*/
|
||||
private readonly SortedSet<StateInfo> _states = new();
|
||||
private readonly SortedSet<StateInfo> _midStates = new();
|
||||
private readonly SortedSet<StateInfo> _newStates = new();
|
||||
|
||||
private readonly Func<int, bool> _reserveCallback;
|
||||
|
||||
private bool _bufferIsFull = false;
|
||||
private int _newPagesUsed = 0;
|
||||
private int _midPagesUsed = 0;
|
||||
|
||||
private class PagedStream : Stream
|
||||
{
|
||||
private readonly StateInfo _info;
|
||||
|
||||
private readonly PagedStateManager _manager;
|
||||
private readonly MemoryBlock _parentBlock;
|
||||
private readonly Stream _blockStream;
|
||||
private int _nextBlockId;
|
||||
private long _endOfPage;
|
||||
|
||||
private int _bytesSeen;
|
||||
|
||||
public PagedStream(StateInfo stateInfo, PagedStateManager manager, bool forRead)
|
||||
{
|
||||
_info = stateInfo;
|
||||
_manager = manager;
|
||||
_parentBlock = manager._buffer;
|
||||
_blockStream = _parentBlock.GetStream(_parentBlock.Start, _parentBlock.Size, !forRead);
|
||||
|
||||
_blockStream.Position = (long)_info.FirstPage * PAGE_SIZE;
|
||||
_endOfPage = _blockStream.Position + PAGE_SIZE;
|
||||
_nextBlockId = Marshal.ReadInt32((IntPtr)(_parentBlock.Start + (ulong)_blockStream.Position));
|
||||
_blockStream.Position += 4;
|
||||
|
||||
_bytesSeen = 0;
|
||||
}
|
||||
|
||||
public override bool CanRead => true;
|
||||
|
||||
public override bool CanSeek => false;
|
||||
|
||||
public override bool CanWrite => true;
|
||||
|
||||
public override long Length => _info.Size;
|
||||
|
||||
public override int Read(byte[] buffer, int offset, int count)
|
||||
{
|
||||
if (count + _bytesSeen > _info.Size)
|
||||
count = _info.Size - _bytesSeen;
|
||||
if (count == 0) return 0;
|
||||
|
||||
int readCount = 0;
|
||||
while (_blockStream.Position + count > _endOfPage)
|
||||
{
|
||||
int bytesToEndOfPage = (int)(_endOfPage - _blockStream.Position);
|
||||
if (_blockStream.Read(buffer, offset, bytesToEndOfPage) != bytesToEndOfPage)
|
||||
throw new Exception("Unexpected end of buffer in PagedStreamReader.");
|
||||
_bytesSeen += bytesToEndOfPage;
|
||||
|
||||
_blockStream.Position = (long)_nextBlockId * PAGE_SIZE;
|
||||
_endOfPage = _blockStream.Position + PAGE_SIZE;
|
||||
_nextBlockId = Marshal.ReadInt32((IntPtr)(_parentBlock.Start + (ulong)_blockStream.Position));
|
||||
_blockStream.Position += 4;
|
||||
|
||||
readCount += bytesToEndOfPage;
|
||||
offset += bytesToEndOfPage;
|
||||
count -= bytesToEndOfPage;
|
||||
}
|
||||
|
||||
int lastReadCount = _blockStream.Read(buffer, offset, count);
|
||||
_bytesSeen += lastReadCount;
|
||||
return readCount + lastReadCount;
|
||||
}
|
||||
|
||||
public override long Position
|
||||
{
|
||||
get => _bytesSeen;
|
||||
set => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override void Flush() { }
|
||||
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
|
||||
public override void SetLength(long value) => throw new NotImplementedException();
|
||||
public override void Write(byte[] buffer, int offset, int count)
|
||||
{
|
||||
while (_blockStream.Position + count > _endOfPage)
|
||||
{
|
||||
int bytesToEndOfPage = (int)(_endOfPage - _blockStream.Position);
|
||||
_blockStream.Write(buffer, offset, bytesToEndOfPage);
|
||||
_bytesSeen += bytesToEndOfPage;
|
||||
|
||||
if (_nextBlockId == -1)
|
||||
{
|
||||
_nextBlockId = _manager.FreePage(_info.Frame);
|
||||
// Update the linked list.
|
||||
Marshal.WriteInt32((IntPtr)((long)_parentBlock.Start + _endOfPage - PAGE_SIZE), _nextBlockId);
|
||||
}
|
||||
|
||||
_blockStream.Position = (long)_nextBlockId * PAGE_SIZE;
|
||||
_endOfPage = _blockStream.Position + PAGE_SIZE;
|
||||
_nextBlockId = Marshal.ReadInt32((IntPtr)(_parentBlock.Start + (ulong)_blockStream.Position));
|
||||
_blockStream.Position += 4;
|
||||
|
||||
offset += bytesToEndOfPage;
|
||||
count -= bytesToEndOfPage;
|
||||
}
|
||||
|
||||
_blockStream.Write(buffer, offset, count);
|
||||
_bytesSeen += count;
|
||||
}
|
||||
|
||||
public struct FinalizedInfo
|
||||
{
|
||||
public int LastPage;
|
||||
public int NextPage;
|
||||
public int BytesWritten;
|
||||
}
|
||||
public FinalizedInfo FinishWrite()
|
||||
{
|
||||
IntPtr ptr = (IntPtr)((long)_parentBlock.Start + _endOfPage - PAGE_SIZE);
|
||||
int nextPage = Marshal.ReadInt32(ptr);
|
||||
Marshal.WriteInt32(ptr, -1);
|
||||
|
||||
return new()
|
||||
{
|
||||
LastPage = (int)(_endOfPage / PAGE_SIZE) - 1,
|
||||
BytesWritten = _bytesSeen,
|
||||
NextPage = nextPage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public PagedStateManager(PagedSettings settings, Func<int, bool> reserveCallback)
|
||||
{
|
||||
Settings = settings;
|
||||
int pageCount = settings.TotalMemoryLimitMB * 1024 / 4;
|
||||
_buffer = new MemoryBlock((ulong)pageCount * PAGE_SIZE);
|
||||
_buffer.Protect(_buffer.Start, _buffer.Size, MemoryBlock.Protection.RW);
|
||||
|
||||
_reserveCallback = reserveCallback;
|
||||
|
||||
// Set up the pages. This is a single link list.
|
||||
// The links go like this:
|
||||
// 1) Within a state, each page points to the next page for that state.
|
||||
// 2) The last page for a state points to an invalid page.
|
||||
// 3) The free pages (which they all are, initially) are all linked together, order is arbitrary.
|
||||
// 4) The last free page points to an invalid page.
|
||||
|
||||
// When writing a state, we always begin at the first free page from the free list.
|
||||
// When we invalidate a state, we put its pages at the front of the free list.
|
||||
|
||||
for (int i = 0; i < pageCount - 1; i++)
|
||||
{
|
||||
Marshal.WriteInt32((IntPtr)((long)_buffer.Start + (long)i * PAGE_SIZE), i + 1);
|
||||
}
|
||||
Marshal.WriteInt32((IntPtr)((long)_buffer.Start + (long)(pageCount - 1) * PAGE_SIZE), -1);
|
||||
_firstFree = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will removing the state on this frame leave us with a gap larger than <see cref="PagedSettings.FramesBetweenOldStates"/>?
|
||||
/// </summary>
|
||||
private bool ShouldKeepForOld(int frame)
|
||||
{
|
||||
Debug.Assert(_states.Contains(new(frame)), "Do not ask if we should keep a non-existent state.");
|
||||
|
||||
if (frame == 0) return true; // must keep state on frame 0
|
||||
if (_states.Max.Frame == frame)
|
||||
{
|
||||
// There is no future state, so there is no gap between states for us to measure.
|
||||
// We're probably unreserving for a marker removal. Allow it to be removed, since that's simpler.
|
||||
return false;
|
||||
}
|
||||
|
||||
StateInfo nextState = _states.GetViewBetween(new(frame + 1), new(frame + Settings.FramesBetweenOldStates)).Min;
|
||||
if (nextState.Frame == 0) return true;
|
||||
StateInfo previousState = _states.GetViewBetween(new(frame - Settings.FramesBetweenOldStates), new(frame - 1)).Max;
|
||||
|
||||
return nextState.Frame - previousState.Frame > Settings.FramesBetweenOldStates;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a state from the buffer and returns the first newly-freed page.
|
||||
/// This should be used when saving a state and running out of already free pages.
|
||||
/// </summary>
|
||||
private int FreePage(int frame)
|
||||
{
|
||||
Debug.Assert(!_states.Contains(new(frame)), "Invalid use of FreePage. The frame we are capturing a state for should not already have a state.");
|
||||
|
||||
_bufferIsFull = true;
|
||||
|
||||
while (true)
|
||||
{
|
||||
// A very special case: We have no mid or new states.
|
||||
if (_newStates.Count == 0 && _midStates.Count == 0)
|
||||
{
|
||||
if (_states.Count < 2) throw new Exception("Unable to capture a single state. This probably means your Memory Limit setting is too low.");
|
||||
// Avoiding the performance issues of GetViewBetween, when we have no idea what frame the second state is on.
|
||||
var enumerator = _states.GetEnumerator();
|
||||
enumerator.MoveNext();
|
||||
enumerator.MoveNext();
|
||||
StateInfo stateToKick = enumerator.Current;
|
||||
_states.Remove(stateToKick);
|
||||
return stateToKick.FirstPage;
|
||||
}
|
||||
|
||||
// Kick from new if the ratio limit is met.
|
||||
if (_newStates.Count != 0 &&
|
||||
(_midPagesUsed == 0 || (double)_newPagesUsed / _midPagesUsed > Settings.NewToMidRatio))
|
||||
{
|
||||
StateInfo stateToKick = _newStates.Min;
|
||||
int pages = (stateToKick.Size + PAGE_DATA_SIZE - 1) / PAGE_DATA_SIZE;
|
||||
_newPagesUsed -= pages;
|
||||
_newStates.Remove(stateToKick);
|
||||
|
||||
// Kicking a state from new means checking if it belongs in mid.
|
||||
StateInfo newestOlderState = _states.GetViewBetween(new(stateToKick.Frame - Settings.FramesBetweenMidStates), new(stateToKick.Frame - 1)).Max;
|
||||
bool recategorizeAsMid = stateToKick.Frame - newestOlderState.Frame >= Settings.FramesBetweenMidStates;
|
||||
// If it does, we re-categorize it and try kicking the next one from new.
|
||||
if (recategorizeAsMid)
|
||||
{
|
||||
_midPagesUsed += pages;
|
||||
_midStates.Add(stateToKick);
|
||||
}
|
||||
else if (_reserveCallback(stateToKick.Frame))
|
||||
{
|
||||
// Recategorize as old. Nothing to do here.
|
||||
}
|
||||
else
|
||||
{
|
||||
_states.Remove(stateToKick);
|
||||
return stateToKick.FirstPage;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// At this point, we know _midStates.Count != 0 and we are kicking from mid.
|
||||
// Which one to kick? This depends.
|
||||
// 1) If there are mid states past the one being captured, we take the oldest of those.
|
||||
// This is so we can capture states while re-playing old sections.
|
||||
// 2) Otherwise, we kick the oldest mid state.
|
||||
|
||||
// TODO: Can this GetViewBetween be made non-slow for viewing gaps?
|
||||
StateInfo oldestNewerState = _midStates.GetViewBetween(new(frame), new(int.MaxValue)).Min;
|
||||
StateInfo stateToKick = oldestNewerState.Frame != 0 ? oldestNewerState : _midStates.Min;
|
||||
|
||||
// Kicking a state means checking if it belongs in old.
|
||||
bool recategorizeAsOld = ShouldKeepForOld(stateToKick.Frame) || _reserveCallback(stateToKick.Frame);
|
||||
int pages = (stateToKick.Size + PAGE_DATA_SIZE - 1) / PAGE_DATA_SIZE;
|
||||
_midPagesUsed -= pages;
|
||||
_midStates.Remove(stateToKick);
|
||||
if (!recategorizeAsOld)
|
||||
{
|
||||
_states.Remove(stateToKick);
|
||||
return stateToKick.FirstPage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void InternalCapture(int frame, IStatable source, StateGroup destinationGroup)
|
||||
{
|
||||
if (_firstFree == -1) _firstFree = FreePage(frame);
|
||||
StateInfo newState = new StateInfo()
|
||||
{
|
||||
Frame = frame,
|
||||
FirstPage = _firstFree,
|
||||
};
|
||||
|
||||
using PagedStream stream = new(newState, this, false);
|
||||
using BinaryWriter bw = new(stream);
|
||||
source.SaveStateBinary(bw);
|
||||
|
||||
PagedStream.FinalizedInfo finalInfo = stream.FinishWrite();
|
||||
newState.LastPage = finalInfo.LastPage;
|
||||
newState.Size = finalInfo.BytesWritten;
|
||||
_firstFree = finalInfo.NextPage;
|
||||
if (_firstFree == -1)
|
||||
_bufferIsFull = true;
|
||||
|
||||
_states.Add(newState);
|
||||
int pages = (newState.Size + PAGE_DATA_SIZE - 1) / PAGE_DATA_SIZE;
|
||||
if (destinationGroup == StateGroup.Mid)
|
||||
{
|
||||
_midStates.Add(newState);
|
||||
_midPagesUsed += pages;
|
||||
}
|
||||
else if (destinationGroup == StateGroup.New)
|
||||
{
|
||||
_newStates.Add(newState);
|
||||
_newPagesUsed += pages;
|
||||
}
|
||||
}
|
||||
|
||||
public void Capture(int frame, IStatable source, bool force = false)
|
||||
{
|
||||
Debug.Assert(_states.Contains(new(0)), "State manager cannot be used until engaged.");
|
||||
|
||||
if (HasState(frame)) return;
|
||||
|
||||
if (_reserveCallback(frame))
|
||||
{
|
||||
InternalCapture(frame, source, StateGroup.Old);
|
||||
}
|
||||
else if (source.AvoidRewind)
|
||||
{
|
||||
// Zwinder did this, so I will too. I'm not sure it's a good idea but maybe it is.
|
||||
return;
|
||||
}
|
||||
else if (force)
|
||||
{
|
||||
InternalCapture(frame, source, StateGroup.Mid);
|
||||
}
|
||||
else if (!_bufferIsFull || _newStates.Count == 0 || frame > _newStates.Min.Frame)
|
||||
{
|
||||
int max = frame - 1;
|
||||
int min = frame - Settings.FramesBetweenNewStates;
|
||||
int newestOlderState = _states.GetViewBetween(new(min), new(max)).Max.Frame;
|
||||
|
||||
// Special case: If the buffer is entirely full of old states, we don't attempt to capture into new.
|
||||
if (_bufferIsFull && _midStates.Count == 0 && _newStates.Count < 2)
|
||||
{
|
||||
min = frame - Settings.FramesBetweenOldStates;
|
||||
// Now, a quirk: Once we get here ("full of old states") for the first time, there should actually be one new state. Because our last capture went into new, recategorized a new to old, and deleted another new.
|
||||
// In this case, we kinda want to ignore that new state.
|
||||
if (_newStates.Count == 1)
|
||||
{
|
||||
newestOlderState = _states.GetViewBetween(new(min), new(max)).Max.Frame;
|
||||
max = newestOlderState - 1;
|
||||
}
|
||||
if (min <= max) newestOlderState = _states.GetViewBetween(new(min), new(max)).Max.Frame;
|
||||
else newestOlderState = 0;
|
||||
|
||||
if (frame - newestOlderState >= Settings.FramesBetweenOldStates)
|
||||
InternalCapture(frame, source, StateGroup.Old);
|
||||
return;
|
||||
}
|
||||
|
||||
if (frame - newestOlderState >= Settings.FramesBetweenNewStates)
|
||||
InternalCapture(frame, source, StateGroup.New);
|
||||
}
|
||||
else
|
||||
{
|
||||
int max = frame - 1;
|
||||
int min = frame - Settings.FramesBetweenMidStates;
|
||||
StateInfo newestOlderState = _states.GetViewBetween(new(min), new(max)).Max;
|
||||
if (frame - newestOlderState.Frame >= Settings.FramesBetweenMidStates)
|
||||
InternalCapture(frame, source, StateGroup.Mid);
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear() => InvalidateAfter(0);
|
||||
|
||||
public void Dispose() => _buffer.Dispose();
|
||||
|
||||
public void Engage(byte[] frameZeroState) => InternalCapture(0, new StatableArray(frameZeroState), StateGroup.Old);
|
||||
|
||||
public KeyValuePair<int, Stream> GetStateClosestToFrame(int frame)
|
||||
{
|
||||
Debug.Assert(_states.Contains(new(0)), "State manager cannot be used until engaged.");
|
||||
if (frame < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(frame));
|
||||
|
||||
StateInfo info = _states.GetViewBetween(new(0), new(frame)).Max;
|
||||
return new(info.Frame, new PagedStream(info, this, true));
|
||||
}
|
||||
|
||||
public bool HasState(int frame) => _states.Contains(new(frame));
|
||||
|
||||
private void RemoveState(StateInfo state)
|
||||
{
|
||||
Debug.Assert(_states.Contains(state), "Do not attempt to remove a non-existent state.");
|
||||
|
||||
ulong position = _buffer.Start + ((ulong)state.LastPage * PAGE_SIZE);
|
||||
Marshal.WriteInt32((IntPtr)position, _firstFree);
|
||||
_firstFree = state.FirstPage;
|
||||
|
||||
int pages = (state.Size + PAGE_DATA_SIZE - 1) / PAGE_DATA_SIZE;
|
||||
_states.Remove(state);
|
||||
if (_newStates.Remove(state)) _newPagesUsed -= pages;
|
||||
else if (_midStates.Remove(state)) _midPagesUsed -= pages;
|
||||
|
||||
_bufferIsFull = false;
|
||||
}
|
||||
public bool InvalidateAfter(int frame)
|
||||
{
|
||||
// must keep state on frame 0
|
||||
if (frame < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(frame));
|
||||
|
||||
int oldStateCount = Count;
|
||||
|
||||
StateInfo newestState = _states.Max;
|
||||
while (newestState.Frame > frame)
|
||||
{
|
||||
RemoveState(newestState);
|
||||
newestState = _states.Max;
|
||||
}
|
||||
return Count < oldStateCount;
|
||||
}
|
||||
|
||||
public void Unreserve(int frame)
|
||||
{
|
||||
// We need to get the real state info out of the set.
|
||||
StateInfo state = _states.GetViewBetween(new(frame), new(frame)).Min;
|
||||
if (state.Frame == 0) return;
|
||||
|
||||
// Remove the state if it's an old state we don't need.
|
||||
if (!_newStates.Contains(state) && !_midStates.Contains(state) && !ShouldKeepForOld(frame))
|
||||
{
|
||||
RemoveState(state);
|
||||
}
|
||||
}
|
||||
|
||||
public IStateManager UpdateSettings(IStateManagerSettings settings, bool keepOldStates = false)
|
||||
{
|
||||
if (settings is not PagedSettings pSettings || pSettings.TotalMemoryLimitMB != this.Settings.TotalMemoryLimitMB)
|
||||
{
|
||||
IStateManager newManager = settings.CreateManager(_reserveCallback);
|
||||
newManager.Engage(GetStateClosestToFrame(0).Value.ReadAllBytes());
|
||||
if (keepOldStates) foreach (StateInfo state in _states)
|
||||
{
|
||||
Stream s = GetStateClosestToFrame(state.Frame).Value;
|
||||
newManager.Capture(state.Frame, new StatableStream(s, (int)s.Length));
|
||||
}
|
||||
|
||||
Dispose();
|
||||
return newManager;
|
||||
}
|
||||
else
|
||||
{
|
||||
bool recaptureOld = pSettings.FramesBetweenOldStates > this.Settings.FramesBetweenOldStates;
|
||||
this.Settings = pSettings;
|
||||
if (recaptureOld)
|
||||
{
|
||||
foreach (StateInfo state in _states)
|
||||
{
|
||||
if (_newStates.Contains(state)) continue;
|
||||
if (_midStates.Contains(state)) continue;
|
||||
|
||||
if (!_reserveCallback(state.Frame) && !ShouldKeepForOld(state.Frame))
|
||||
RemoveState(state);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
public void SaveStateHistory(BinaryWriter bw)
|
||||
{
|
||||
bw.Write((byte)2); // version
|
||||
|
||||
List<StateInfo> statesToSave = new();
|
||||
foreach (StateInfo state in _states)
|
||||
{
|
||||
statesToSave.Add(state);
|
||||
if (statesToSave.Count > 2)
|
||||
{
|
||||
int diff = statesToSave[statesToSave.Count - 1].Frame - statesToSave[statesToSave.Count - 3].Frame;
|
||||
bool shouldSave = diff > Settings.FramesBetweenSavedStates ||
|
||||
(Settings.ForceSaveMarkerStates && _reserveCallback(state.Frame));
|
||||
|
||||
if (!shouldSave) statesToSave.RemoveAt(statesToSave.Count - 2);
|
||||
}
|
||||
}
|
||||
if (statesToSave.Count > 1)
|
||||
{
|
||||
int diff = statesToSave[statesToSave.Count - 1].Frame - statesToSave[statesToSave.Count - 2].Frame;
|
||||
bool shouldSave = diff >= Settings.FramesBetweenSavedStates ||
|
||||
(Settings.ForceSaveMarkerStates && _reserveCallback(statesToSave[statesToSave.Count - 1].Frame));
|
||||
|
||||
if (!shouldSave) statesToSave.RemoveAt(statesToSave.Count - 1);
|
||||
}
|
||||
|
||||
statesToSave.RemoveAt(0); // No point keeping this one, the movie defines it elsewhere.
|
||||
|
||||
bw.Write(statesToSave.Count);
|
||||
foreach (StateInfo state in statesToSave)
|
||||
{
|
||||
bw.Write(state.Size);
|
||||
bw.Write(state.Frame);
|
||||
GetStateClosestToFrame(state.Frame).Value.CopyTo(bw.BaseStream);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadStateHistory(BinaryReader br)
|
||||
{
|
||||
int version = br.ReadByte();
|
||||
if (version < 2) return; // Not a PagedStateManager.
|
||||
|
||||
// Fake engage, so we can use the capture logic.
|
||||
bool isEngaged = _states.Contains(new(0));
|
||||
if (!isEngaged) Engage([ 0 ]);
|
||||
|
||||
int stateCount = br.ReadInt32();
|
||||
for (int i = 0; i < stateCount; i++)
|
||||
{
|
||||
int size = br.ReadInt32();
|
||||
int frame = br.ReadInt32();
|
||||
Capture(frame, new StatableStream(br.BaseStream, size));
|
||||
}
|
||||
|
||||
// Undo fake engage
|
||||
if (!isEngaged) RemoveState(_states.Min);
|
||||
}
|
||||
|
||||
private class StatableArray : IStatable
|
||||
{
|
||||
public bool AvoidRewind => throw new NotImplementedException();
|
||||
public void LoadStateBinary(BinaryReader reader) => throw new NotImplementedException();
|
||||
|
||||
private byte[] _array;
|
||||
public StatableArray(byte[] array) => _array = array;
|
||||
public void SaveStateBinary(BinaryWriter writer) => writer.Write(_array);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,13 +6,13 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
public partial class GreenzoneSettings : Form, IDialogParent
|
||||
{
|
||||
private readonly Action<ZwinderStateManagerSettings, bool> _saveSettings;
|
||||
private ZwinderStateManagerSettings _settings;
|
||||
private readonly Action<IStateManagerSettings, bool> _saveSettings;
|
||||
private IStateManagerSettings _settings;
|
||||
private readonly bool _isDefault;
|
||||
|
||||
public IDialogController DialogController { get; }
|
||||
|
||||
public GreenzoneSettings(IDialogController dialogController, ZwinderStateManagerSettings settings, Action<ZwinderStateManagerSettings, bool> saveSettings, bool isDefault)
|
||||
public GreenzoneSettings(IDialogController dialogController, IStateManagerSettings settings, Action<IStateManagerSettings, bool> saveSettings, bool isDefault)
|
||||
{
|
||||
DialogController = dialogController;
|
||||
InitializeComponent();
|
||||
|
@ -45,7 +45,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
|
||||
private void DefaultsButton_Click(object sender, EventArgs e)
|
||||
{
|
||||
_settings = new ZwinderStateManagerSettings();
|
||||
_settings = new PagedStateManager.PagedSettings();
|
||||
SettingsPropertyGrid.SelectedObject = _settings;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -649,9 +649,11 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
MainForm.FrameAdvance();
|
||||
|
||||
byte[] greenZone = CurrentTasMovie.TasStateManager[Emulator.Frame];
|
||||
if (greenZone.Length > 0)
|
||||
if (CurrentTasMovie.TasStateManager.HasState(Emulator.Frame))
|
||||
{
|
||||
Stream greenStream = CurrentTasMovie.TasStateManager.GetStateClosestToFrame(Emulator.Frame).Value;
|
||||
byte[] greenZone = new byte[greenStream.Length];
|
||||
greenStream.Read(greenZone);
|
||||
byte[] state = StatableEmulator.CloneSavestate();
|
||||
|
||||
if (!state.SequenceEqual(greenZone))
|
||||
|
@ -901,8 +903,8 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
using GreenzoneSettings form = new(
|
||||
DialogController,
|
||||
new ZwinderStateManagerSettings(CurrentTasMovie.TasStateManager.Settings),
|
||||
(s, k) => { CurrentTasMovie.TasStateManager.UpdateSettings(s, k); },
|
||||
CurrentTasMovie.TasStateManager.Settings.Clone(),
|
||||
(s, k) => { CurrentTasMovie.TasStateManager = CurrentTasMovie.TasStateManager.UpdateSettings(s, k); },
|
||||
false)
|
||||
{
|
||||
Owner = this,
|
||||
|
@ -941,7 +943,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
{
|
||||
using GreenzoneSettings form = new(
|
||||
DialogController,
|
||||
new ZwinderStateManagerSettings(Config.Movies.DefaultTasStateManagerSettings),
|
||||
Config.Movies.DefaultTasStateManagerSettings.Clone(),
|
||||
(s, k) => { Config.Movies.DefaultTasStateManagerSettings = s; },
|
||||
true)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,525 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using BizHawk.Client.Common;
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Tests.Client.Common.Movie
|
||||
{
|
||||
[TestClass]
|
||||
public class PagedStateManagerTests
|
||||
{
|
||||
// Current PagedStateManager uses 4KB pages (4092 bytes of state data per page)
|
||||
private const int PAGE_COUNT = 256;
|
||||
private const int STATE_BYTES_PER_PAGE = 4092;
|
||||
|
||||
private void WithRatioVariety(Action<PagedStateManager> action, IStatable source, List<int>? reserved = null)
|
||||
{
|
||||
reserved ??= new();
|
||||
|
||||
PagedStateManager.PagedSettings settings = MakeDefaultSettings(2);
|
||||
PagedStateManager manager = new(settings, (f) => reserved.Contains(f));
|
||||
manager.Engage(source.CloneSavestate());
|
||||
|
||||
action(manager);
|
||||
manager.InvalidateAfter(0);
|
||||
|
||||
settings.NewToMidRatio = 10;
|
||||
manager = (PagedStateManager)manager.UpdateSettings(settings);
|
||||
action(manager);
|
||||
manager.InvalidateAfter(0);
|
||||
|
||||
settings.NewToMidRatio = 0;
|
||||
manager = (PagedStateManager)manager.UpdateSettings(settings);
|
||||
action(manager);
|
||||
}
|
||||
|
||||
private PagedStateManager.PagedSettings MakeDefaultSettings(double ratio = 2)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
TotalMemoryLimitMB = 1,
|
||||
FramesBetweenNewStates = 1,
|
||||
FramesBetweenMidStates = 4,
|
||||
FramesBetweenOldStates = 12,
|
||||
NewToMidRatio = ratio,
|
||||
};
|
||||
}
|
||||
|
||||
private IStatable CreateStateSource(int size = 8) => new StateSource { PaddingData = new byte[size - 4] };
|
||||
|
||||
[TestMethod]
|
||||
public void CanSaveAndLoad()
|
||||
{
|
||||
IStatable ss = CreateStateSource(8);
|
||||
PagedStateManager manager = new(MakeDefaultSettings(), (f) => false);
|
||||
manager.Settings.FramesBetweenSavedStates = 1;
|
||||
manager.Engage(ss.CloneSavestate());
|
||||
for (int i = 0; i < 20; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
int stateCount = manager.Count;
|
||||
|
||||
MemoryStream ms = new();
|
||||
manager.SaveStateHistory(new BinaryWriter(ms));
|
||||
manager.Dispose();
|
||||
|
||||
manager = new(MakeDefaultSettings(), (f) => false);
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
manager.LoadStateHistory(new BinaryReader(ms));
|
||||
manager.Engage(ss.CloneSavestate());
|
||||
|
||||
Assert.AreEqual(stateCount, manager.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Last_Correct_WhenReservedGreaterThanCurrent()
|
||||
{
|
||||
const int futureReservedFrame = 1000;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
// Arrange
|
||||
manager.Capture(futureReservedFrame, ss);
|
||||
for (int i = 1; i < 20; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
// Act
|
||||
var actual = manager.Last;
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(futureReservedFrame, actual);
|
||||
}, ss, [ futureReservedFrame ]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Last_Correct_WhenNewIsLast()
|
||||
{
|
||||
const int totalCurrentFrames = 20;
|
||||
IStatable ss = CreateStateSource();
|
||||
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
Assert.AreEqual(totalCurrentFrames - 1, manager.Last);
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void HasState_Correct_WhenReservedGreaterThanNew()
|
||||
{
|
||||
const int futureReservedFrame = 1000;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
manager.Capture(futureReservedFrame, ss);
|
||||
for (int i = 1; i < 20; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
Assert.IsTrue(manager.HasState(futureReservedFrame));
|
||||
|
||||
}, ss, [ futureReservedFrame ]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void HasState_Correct_WhenNewIsLast()
|
||||
{
|
||||
const int totalCurrentFrames = 20;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
Assert.IsTrue(manager.HasState(totalCurrentFrames - 1));
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetStateClosestToFrame_Correct_WhenReservedGreaterThanNew()
|
||||
{
|
||||
const int futureReservedFrame = 1000;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
manager.Capture(futureReservedFrame, ss);
|
||||
for (int i = 1; i < 10; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
Assert.AreEqual(futureReservedFrame, manager.GetStateClosestToFrame(futureReservedFrame + 1).Key);
|
||||
}, ss, [ futureReservedFrame ]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetStateClosestToFrame_Correct_WhenNewIsLast()
|
||||
{
|
||||
const int totalCurrentFrames = 20;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
Assert.AreEqual(totalCurrentFrames - 1, manager.GetStateClosestToFrame(totalCurrentFrames).Key);
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InvalidateAfter_Correct_WhenReservedGreaterThanNew()
|
||||
{
|
||||
const int futureReservedFrame = 1000;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
manager.Capture(futureReservedFrame, ss);
|
||||
for (int i = 1; i < 10; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
manager.InvalidateAfter(futureReservedFrame - 1);
|
||||
|
||||
Assert.IsFalse(manager.HasState(futureReservedFrame));
|
||||
}, ss, [ futureReservedFrame ]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InvalidateAfter_Correct_WhenNewIsLast()
|
||||
{
|
||||
const int totalCurrentFrames = 10;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 1; i < totalCurrentFrames; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
manager.InvalidateAfter(totalCurrentFrames - 1);
|
||||
|
||||
Assert.IsFalse(manager.HasState(totalCurrentFrames));
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Count_NoReserved()
|
||||
{
|
||||
const int totalFrames = 20;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 1; i < totalFrames; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
Assert.AreEqual(totalFrames, manager.Count);
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Count_WithReserved()
|
||||
{
|
||||
const int totalFrames = 20;
|
||||
const int reservedFrame = 1000;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
manager.Capture(reservedFrame, ss);
|
||||
for (int i = 1; i < totalFrames; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
Assert.AreEqual(totalFrames + 1, manager.Count);
|
||||
}, ss, [ reservedFrame ]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Clear_KeepsZeroState()
|
||||
{
|
||||
const int reservedFrame = 1000;
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
manager.Capture(reservedFrame, ss);
|
||||
for (int i = 1; i < 10; i++)
|
||||
{
|
||||
manager.Capture(i, ss);
|
||||
}
|
||||
|
||||
manager.Clear();
|
||||
|
||||
Assert.IsTrue(manager.HasState(0));
|
||||
}, ss, [ reservedFrame ]);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestKeepsAtLeastAncientInterval()
|
||||
{
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
// Load branch with frame number almost at two old intervals
|
||||
int branchFrame = manager.Settings.FramesBetweenOldStates * 2 - 1;
|
||||
manager.Capture(branchFrame, ss, true);
|
||||
// Rewind to frame 0, play far enough that it will kick states before the branch.
|
||||
for (int i = 0; i < 2000; i++)
|
||||
manager.Capture(i, ss);
|
||||
// ASSERT: There are no gaps larger than the ancient interval
|
||||
int lastState = 0;
|
||||
while (lastState < branchFrame)
|
||||
{
|
||||
int nextState = manager.GetStateClosestToFrame(lastState + manager.Settings.FramesBetweenOldStates).Key;
|
||||
Assert.AreNotEqual(lastState, nextState, "FramesBetweenOldStates was not respected.");
|
||||
lastState = nextState;
|
||||
}
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RecapturesExpectedNumberOfSmallStates()
|
||||
{
|
||||
IStatable ss = CreateStateSource(8);
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 0; i < PAGE_COUNT; i++)
|
||||
manager.Capture(i, ss);
|
||||
manager.InvalidateAfter(0);
|
||||
for (int i = 0; i < PAGE_COUNT; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
Assert.IsTrue(manager.HasState(1));
|
||||
manager.Capture(PAGE_COUNT, ss);
|
||||
Assert.IsFalse(manager.HasState(1));
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RecapturesExpectedNumberOfLargeStates()
|
||||
{
|
||||
IStatable ss = CreateStateSource(11000);
|
||||
const int PAGES_PER_STATE = (11000 - 1) / STATE_BYTES_PER_PAGE + 1;
|
||||
const int MAX_STATES = PAGE_COUNT / PAGES_PER_STATE;
|
||||
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 0; i < MAX_STATES; i++)
|
||||
manager.Capture(i, ss);
|
||||
manager.InvalidateAfter(0);
|
||||
for (int i = 0; i < MAX_STATES; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
Assert.IsTrue(manager.HasState(1));
|
||||
manager.Capture(MAX_STATES, ss);
|
||||
Assert.IsFalse(manager.HasState(1));
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DoesNotCaptureEachFrameWhenFullOfOldStates()
|
||||
{
|
||||
IStatable ss = CreateStateSource();
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
int endFrame = PAGE_COUNT * manager.Settings.FramesBetweenOldStates;
|
||||
for (int i = 0; i <= endFrame; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
int frame = endFrame;
|
||||
// We're on a frame where we expect to have an "old" state.
|
||||
Assert.IsTrue(manager.HasState(frame));
|
||||
// But the next few won't.
|
||||
for (int i = 0; i < manager.Settings.FramesBetweenOldStates - 1; i++)
|
||||
{
|
||||
frame++;
|
||||
manager.Capture(frame, ss);
|
||||
Assert.IsFalse(manager.HasState(frame));
|
||||
}
|
||||
// And next one once again should capture.
|
||||
frame++;
|
||||
manager.Capture(frame, ss);
|
||||
Assert.IsTrue(manager.HasState(frame));
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BeginsUsingMidInterval()
|
||||
{
|
||||
IStatable ss = CreateStateSource(8);
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 0; i < PAGE_COUNT; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
for (int i = PAGE_COUNT; i < PAGE_COUNT + manager.Settings.FramesBetweenMidStates - 1; i++)
|
||||
{
|
||||
// Has state before capture, drops it after
|
||||
int theFrame = i - (PAGE_COUNT - 1);
|
||||
Assert.IsTrue(manager.HasState(theFrame));
|
||||
manager.Capture(i, ss);
|
||||
Assert.IsFalse(manager.HasState(theFrame));
|
||||
}
|
||||
// Has state before capture, keeps it after because we've reached FramesBetweenMidStates
|
||||
Assert.IsTrue(manager.HasState(manager.Settings.FramesBetweenMidStates));
|
||||
manager.Capture(manager.Settings.FramesBetweenMidStates + PAGE_COUNT, ss);
|
||||
Assert.IsTrue(manager.HasState(manager.Settings.FramesBetweenMidStates));
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void UsesNewIntervalInGapWhenPossible()
|
||||
{
|
||||
IStatable ss = CreateStateSource(8);
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
for (int i = 1000; i < 1020; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
// Every frame 0-20 should have a state now, since the buffer isn't full yet.
|
||||
for (int i = 0; i < 20; i += manager.Settings.FramesBetweenNewStates)
|
||||
Assert.IsTrue(manager.HasState(i));
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BeginsUsingMidIntervalInGap()
|
||||
{
|
||||
IStatable ss = CreateStateSource(8);
|
||||
WithRatioVariety((manager) =>
|
||||
{
|
||||
const int futureStateCount = 20;
|
||||
for (int i = 1000; i < 1000 + futureStateCount; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
const int bufferFullFrame = PAGE_COUNT - futureStateCount;
|
||||
for (int i = 0; i < bufferFullFrame; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
for (int i = bufferFullFrame; i < bufferFullFrame + manager.Settings.FramesBetweenMidStates - 1; i++)
|
||||
{
|
||||
// Has state before capture, drops it after
|
||||
int theFrame = i - (bufferFullFrame - 1);
|
||||
Assert.IsTrue(manager.HasState(theFrame));
|
||||
manager.Capture(i, ss);
|
||||
Assert.IsFalse(manager.HasState(theFrame));
|
||||
}
|
||||
// Has state before capture, keeps it after because we've reached FramesBetweenMidStates
|
||||
Assert.IsTrue(manager.HasState(manager.Settings.FramesBetweenMidStates));
|
||||
manager.Capture(manager.Settings.FramesBetweenMidStates + bufferFullFrame, ss);
|
||||
Assert.IsTrue(manager.HasState(manager.Settings.FramesBetweenMidStates));
|
||||
}, ss);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BufferTooSmallDoesntBreakEverything()
|
||||
{
|
||||
PagedStateManager.PagedSettings settings = MakeDefaultSettings();
|
||||
IStatable giantSource = CreateStateSource(settings.TotalMemoryLimitMB * 1024 * 1024 + 4);
|
||||
IStatable smallSource = CreateStateSource();
|
||||
PagedStateManager manager = new(settings, (f) => false);
|
||||
manager.Engage(smallSource.CloneSavestate());
|
||||
|
||||
manager.Capture(1, smallSource);
|
||||
|
||||
Assert.Throws<Exception>(() => manager.Capture(2, giantSource));
|
||||
|
||||
// Do we still have the use of all pages?
|
||||
for (int i = 0; i < PAGE_COUNT * settings.FramesBetweenOldStates; i += settings.FramesBetweenOldStates)
|
||||
manager.Capture(i, smallSource);
|
||||
Assert.AreEqual(PAGE_COUNT, manager.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CanHandleLargeBuffer()
|
||||
{
|
||||
// This should ensure there aren't any bugs like using a 32-bit value where a 64-bit one is needed.
|
||||
IStatable ss = CreateStateSource(STATE_BYTES_PER_PAGE * 999 + 1);
|
||||
PagedStateManager manager = new(new()
|
||||
{
|
||||
TotalMemoryLimitMB = 4400,
|
||||
FramesBetweenNewStates = 1,
|
||||
FramesBetweenMidStates = 1,
|
||||
FramesBetweenOldStates = 1,
|
||||
}, (f) => false);
|
||||
manager.Engage(ss.CloneSavestate());
|
||||
int expectedStates = (manager.Settings.TotalMemoryLimitMB * 1024 / 4) / 1000;
|
||||
|
||||
for (int i = 0; i < expectedStates * 1.2; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
Assert.AreEqual(expectedStates, manager.Count);
|
||||
|
||||
manager.InvalidateAfter(0);
|
||||
for (int i = 0; i < expectedStates * 1.2; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
Assert.AreEqual(expectedStates, manager.Count);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void KeepsReservedFrames()
|
||||
{
|
||||
const int reservedFrame1 = 7;
|
||||
int reservedFrame2 = 0;
|
||||
|
||||
IStatable ss = CreateStateSource();
|
||||
PagedStateManager manager = new(MakeDefaultSettings(), (f) => f == reservedFrame1 || f == reservedFrame2);
|
||||
manager.Engage(ss.CloneSavestate());
|
||||
|
||||
for (int i = 0; i < PAGE_COUNT; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
// Reserve a frame after capture
|
||||
reservedFrame2 = 13;
|
||||
for (int i = PAGE_COUNT; i < PAGE_COUNT * 2; i++)
|
||||
manager.Capture(i, ss);
|
||||
|
||||
Assert.IsTrue(manager.HasState(reservedFrame1));
|
||||
Assert.IsTrue(manager.HasState(reservedFrame2));
|
||||
}
|
||||
|
||||
private class StateSource : IStatable
|
||||
{
|
||||
public int Frame { get; set; }
|
||||
public byte[] PaddingData { get; set; } = Array.Empty<byte>();
|
||||
|
||||
public bool AvoidRewind => false;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -71,7 +71,8 @@ namespace BizHawk.Tests.Client.Common.Movie
|
|||
var buff = ms.ToArray();
|
||||
var rms = new MemoryStream(buff, false);
|
||||
|
||||
var zw2 = ZwinderStateManager.Create(new BinaryReader(rms), zw.Settings, f => false);
|
||||
var zw2 = new ZwinderStateManager(zw.Settings, f => false);
|
||||
zw2.LoadStateHistory(new BinaryReader(rms));
|
||||
|
||||
// TODO: we could assert more things here to be thorough
|
||||
Assert.IsNotNull(zw2);
|
||||
|
@ -131,7 +132,8 @@ namespace BizHawk.Tests.Client.Common.Movie
|
|||
var ms = new MemoryStream();
|
||||
buff.SaveStateBinary(new BinaryWriter(ms));
|
||||
ms.Position = 0;
|
||||
var buff2 = ZwinderBuffer.Create(new BinaryReader(ms), config);
|
||||
var buff2 = new ZwinderBuffer(config);
|
||||
buff2.Load(new BinaryReader(ms));
|
||||
|
||||
Assert.AreEqual(buff.Size, buff2.Size);
|
||||
Assert.AreEqual(buff.Used, buff2.Used);
|
||||
|
|
|
@ -16,7 +16,11 @@ namespace BizHawk.Tests.Client.Common.config
|
|||
|
||||
private const string RECENT_SER = @"{""recentlist"":[],""MAX_RECENT_FILES"":8,""AutoLoad"":false,""Frozen"":false}";
|
||||
|
||||
private const string ZWINDER_SER = @"{""CurrentUseCompression"":false,""CurrentBufferSize"":256,""CurrentTargetFrameLength"":500,""CurrentStoreType"":0,""RecentUseCompression"":false,""RecentBufferSize"":128,""RecentTargetFrameLength"":2000,""RecentStoreType"":0,""GapsUseCompression"":false,""GapsBufferSize"":64,""GapsTargetFrameLength"":125,""GapsStoreType"":0,""AncientStateInterval"":5000,""AncientStoreType"":0}";
|
||||
//private const string ZWINDER_SER = @"{""CurrentUseCompression"":false,""CurrentBufferSize"":256,""CurrentTargetFrameLength"":500,""CurrentStoreType"":0,""RecentUseCompression"":false,""RecentBufferSize"":128,""RecentTargetFrameLength"":2000,""RecentStoreType"":0,""GapsUseCompression"":false,""GapsBufferSize"":64,""GapsTargetFrameLength"":125,""GapsStoreType"":0,""AncientStateInterval"":5000,""AncientStoreType"":0}";
|
||||
|
||||
private const string PAGED_SER_BASE = @"""TotalMemoryLimitMB"":1024,""FramesBetweenNewStates"":4,""FramesBetweenMidStates"":20,""FramesBetweenOldStates"":400,""NewToMidRatio"":2.0,""FramesBetweenSavedStates"":100,""ForceSaveMarkerStates"":false}";
|
||||
private const string PAGED_SER_WITH_TYPE = @"{""$type"":""BizHawk.Client.Common.PagedStateManager+PagedSettings, BizHawk.Client.Common""," + PAGED_SER_BASE;
|
||||
private const string PAGED_SER = "{" + PAGED_SER_BASE;
|
||||
|
||||
#if NET5_0_OR_GREATER
|
||||
private static readonly IReadOnlySet<Type> KnownGoodFromStdlib = new HashSet<Type>
|
||||
|
@ -45,14 +49,15 @@ namespace BizHawk.Tests.Client.Common.config
|
|||
[typeof(CheatConfig)] = $@"{{""DisableOnLoad"":false,""LoadFileByGame"":true,""AutoSaveOnClose"":true,""Recent"":{RECENT_SER}}}",
|
||||
[typeof(FeedbackBind)] = @"{""Channels"":""Left+Right"",""GamepadPrefix"":""X1 "",""Prescale"":1.0}",
|
||||
[typeof(MessagePosition)] = @"{""X"":0,""Y"":0,""Anchor"":0}",
|
||||
[typeof(MovieConfig)] = $@"{{""MovieEndAction"":3,""EnableBackupMovies"":true,""MoviesOnDisk"":false,""MovieCompressionLevel"":2,""VBAStyleMovieLoadState"":false,""PlaySoundOnMovieEnd"":false,""DefaultTasStateManagerSettings"":{ZWINDER_SER}}}",
|
||||
[typeof(MovieConfig)] = $@"{{""MovieEndAction"":3,""EnableBackupMovies"":true,""MoviesOnDisk"":false,""MovieCompressionLevel"":2,""VBAStyleMovieLoadState"":false,""PlaySoundOnMovieEnd"":false,""DefaultTasStateManagerSettings"":{PAGED_SER_WITH_TYPE}}}",
|
||||
[typeof(PathEntry)] = PATHENTRY_SER,
|
||||
[typeof(PathEntryCollection)] = $@"{{""Paths"":[{PATHENTRY_SER}],""UseRecentForRoms"":false,""LastRomPath"":"".""}}",
|
||||
[typeof(RecentFiles)] = RECENT_SER,
|
||||
[typeof(RewindConfig)] = @"{""UseCompression"":false,""UseDelta"":false,""Enabled"":true,""AllowSlowStates"":false,""BufferSize"":512,""UseFixedRewindInterval"":false,""TargetFrameLength"":600,""TargetRewindInterval"":5,""AllowOutOfOrderStates"":true,""BackingStore"":0}",
|
||||
[typeof(SaveStateConfig)] = @"{""Type"":0,""CompressionLevelNormal"":1,""CompressionLevelRewind"":0,""MakeBackups"":true,""SaveScreenshot"":true,""BigScreenshotSize"":131072,""NoLowResLargeScreenshots"":false}",
|
||||
[typeof(ToolDialogSettings)] = @"{""_wndx"":52,""_wndy"":44,""Width"":796,""Height"":455,""SaveWindowPosition"":true,""TopMost"":false,""FloatingWindow"":true,""AutoLoad"":false}",
|
||||
[typeof(ZwinderStateManagerSettings)] = ZWINDER_SER,
|
||||
//[typeof(ZwinderStateManagerSettings)] = ZWINDER_SER,
|
||||
[typeof(PagedStateManager.PagedSettings)] = PAGED_SER,
|
||||
};
|
||||
|
||||
[TestMethod]
|
||||
|
|
Loading…
Reference in New Issue