Skip to content

Add an option to make .tasproj files smaller. #4331

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/BizHawk.Client.Common/movie/tasproj/IStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ public interface IStateManager : IDisposable
void Capture(int frame, IStatable source, bool force = false);

/// <summary>
/// Commands the state manager to remove a reserved state for the given frame, if it is exists
/// Tell the state manager we no longer wish to reserve the state for the given frame.
/// </summary>
void EvictReserved(int frame);
void Unreserve(int frame);

bool HasState(int frame);

Expand Down
7 changes: 0 additions & 7 deletions src/BizHawk.Client.Common/movie/tasproj/TasBranch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,6 @@ public void Replace(TasBranch old, TasBranch newBranch)
newBranch.Uuid = old.Uuid;
if (newBranch.UserText.Length is 0) newBranch.UserText = old.UserText;
this[index] = newBranch;
if (!_movie.IsReserved(old.Frame))
_movie.TasStateManager.EvictReserved(old.Frame);

_movie.FlagChanges();
}
Expand Down Expand Up @@ -118,12 +116,7 @@ public void Replace(TasBranch old, TasBranch newBranch)
{
var result = base.Remove(item);
if (result)
{
if (!_movie.IsReserved(item!.Frame))
_movie.TasStateManager.EvictReserved(item.Frame);

_movie.FlagChanges();
}

return result;
}
Expand Down
4 changes: 1 addition & 3 deletions src/BizHawk.Client.Common/movie/tasproj/TasMovie.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;

using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common;
Expand Down Expand Up @@ -345,8 +344,7 @@ public bool IsReserved(int frame)
// Why the frame before?
// because we always navigate to the frame before and emulate 1 frame so that we ensure a proper frame buffer on the screen
// users want instant navigation to markers, so to do this, we need to reserve the frame before the marker, not the marker itself
return Markers.Exists(m => m.Frame - 1 == frame)
|| Branches.Any(b => b.Frame == frame); // Branches should already be in the reserved list, but it doesn't hurt to check
return Markers.Exists(m => m.Frame - 1 == frame);
}

public void Dispose()
Expand Down
4 changes: 2 additions & 2 deletions src/BizHawk.Client.Common/movie/tasproj/TasMovieMarker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public void Add(int frame, string message)
return;
}

_movie.TasStateManager.EvictReserved(item.Frame - 1);
_movie.TasStateManager.Unreserve(item.Frame - 1);
_movie.ChangeLog.AddMarkerChange(null, item.Frame, item.Message);

base.Remove(item);
Expand All @@ -221,7 +221,7 @@ public void Add(int frame, string message)
if (match.Invoke(m))
{
_movie.ChangeLog.AddMarkerChange(null, m.Frame, m.Message);
_movie.TasStateManager.EvictReserved(m.Frame - 1);
_movie.TasStateManager.Unreserve(m.Frame - 1);
}
}

Expand Down
192 changes: 137 additions & 55 deletions src/BizHawk.Client.Common/movie/tasproj/ZwinderStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class ZwinderStateManager : IStateManager, IDisposable
private ZwinderBuffer _gapFiller;

// These never decay, but can be invalidated, they are for reserved states
// such as markers and branches, but also we naturally evict states from recent to reserved, based
// such as markers, but also we naturally evict states from recent to reserved, based
// on _ancientInterval
private IDictionary<int, byte[]> _reserved;

Expand Down Expand Up @@ -93,22 +93,22 @@ public void UpdateSettings(ZwinderStateManagerSettings settings, bool keepOldSta

if (keepOldStates)
{
// For ancients ... lets just make sure we aren't keeping states with a gap below the new interval
// For ancients, let's throw out states if doing so still satisfies the ancient state interval.
if (settings.AncientStateInterval > _ancientInterval)
{
int lastReserved = 0;
List<int> framesToRemove = new List<int>();
foreach (int f in _reserved.Keys)
{
if (!_reserveCallback(f) && f - lastReserved < settings.AncientStateInterval)
framesToRemove.Add(f);
else
lastReserved = f;
}
foreach (int f in framesToRemove)
List<int> reservedFrames = _reserved.Keys.ToList();
reservedFrames.Sort();
for (int i = 1; i < reservedFrames.Count - 1; i++)
{
if (f != 0)
EvictReserved(f);
if (_reserveCallback(reservedFrames[i]))
continue;

if (reservedFrames[i + 1] - reservedFrames[i - 1] <= settings.AncientStateInterval)
{
EvictReserved(reservedFrames[i]);
reservedFrames.RemoveAt(i);
i--;
}
}
}
}
Expand Down Expand Up @@ -205,16 +205,19 @@ private void RebuildStateCache()
internal class StateInfo
{
public int Frame { get; }
public int Size { get; }
public Func<Stream> Read { get; }
public StateInfo(ZwinderBuffer.StateInformation si)
{
Frame = si.Frame;
Size = si.Size;
Read = si.GetReadStream;
}

public StateInfo(int frame, byte[] data)
{
Frame = frame;
Size = data.Length;
Read = () => new MemoryStream(data, false);
}
}
Expand Down Expand Up @@ -297,7 +300,7 @@ private void AddStateCache(int frame)
}
}

public void EvictReserved(int frame)
private void EvictReserved(int frame)
{
if (frame == 0)
{
Expand All @@ -311,6 +314,16 @@ public void EvictReserved(int frame)
}
}

public void Unreserve(int frame)
{
// Before removing the state, check if it should be still be reserved.
// For now, this just means checking if we need to keep this state to satisfy the ancient interval.
if (ShouldKeepForAncient(frame))
return;

EvictReserved(frame);
}

public void Capture(int frame, IStatable source, bool force = false)
{
// We already have this state, no need to capture
Expand Down Expand Up @@ -366,41 +379,41 @@ public void Capture(int frame, IStatable source, bool force = false)
index2 =>
{
var state2 = _recent.GetState(index2);
StateCache.Remove(state2.Frame);

var isReserved = _reserveCallback(state2.Frame);

// Add to reserved if reserved, or if it matches an "ancient" state consideration
if (isReserved || !HasNearByReserved(state2.Frame))
if (isReserved || ShouldKeepForAncient(state2.Frame))
{
AddToReserved(state2);
}
else
StateCache.Remove(state2.Frame);
});
});
}

// Returns whether or not a frame has a reserved state within the frame interval on either side of it
private bool HasNearByReserved(int frame)
/// <summary>
/// Will removing the state on this leave us with a gap larger than the ancient interval?
/// </summary>
private bool ShouldKeepForAncient(int frame, SortedList<int> framesList = null)
{
// An easy optimization, we know frame 0 always exists
if (frame < _ancientInterval)
{
return true;
}
framesList ??= StateCache;

// Has nearby before
if (_reserved.Any(kvp => kvp.Key < frame && kvp.Key > frame - _ancientInterval))
int index = framesList.BinarySearch(frame + 1);
if (index < 0)
index = ~index;
if (index == framesList.Count)
{
return true;
}

// Has nearby after
if (_reserved.Any(kvp => kvp.Key > frame && kvp.Key < frame + _ancientInterval))
{
return true;
// There is no future state... so why are we wanting to evict this state?
// We aren't decaying from _recent, that's for sure. So,
return false;
}
if (index <= 1)
return true; // This should never happen.

return false;
int nextState = framesList[index];
int previousState = framesList[index - 2]; // assume framesList[index - 1] == frame
return nextState - previousState > _ancientInterval;
}

private bool NeedsGap(int frame)
Expand Down Expand Up @@ -528,22 +541,33 @@ public static ZwinderStateManager Create(BinaryReader br, ZwinderStateManagerSet
// 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();

var current = ZwinderBuffer.Create(br, settings.Current(), version == 0);
var recent = ZwinderBuffer.Create(br, settings.Recent());
var gaps = ZwinderBuffer.Create(br, settings.GapFiller());
ZwinderStateManager ret;
ZwinderStateManagerSettings.States statesSaved = version < 2 ?
ZwinderStateManagerSettings.States.All :
(ZwinderStateManagerSettings.States)br.ReadByte();
if (statesSaved == ZwinderStateManagerSettings.States.All)
{
var current = ZwinderBuffer.Create(br, settings.Current(), version == 0);
var recent = ZwinderBuffer.Create(br, settings.Recent());
var gaps = ZwinderBuffer.Create(br, settings.GapFiller());
ret = new ZwinderStateManager(current, recent, gaps, reserveCallback, settings);
}
else
ret = new ZwinderStateManager(settings, reserveCallback);

if (version == 0)
settings.AncientStateInterval = br.ReadInt32();

var ret = new ZwinderStateManager(current, recent, gaps, reserveCallback, settings);

var ancientCount = br.ReadInt32();
for (var i = 0; i < ancientCount; i++)
if (statesSaved != ZwinderStateManagerSettings.States.None)
{
var key = br.ReadInt32();
var length = br.ReadInt32();
var data = br.ReadBytes(length);
ret._reserved.Add(key, data);
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._reserved.Add(key, data);
}
}

ret.RebuildStateCache();
Expand All @@ -554,18 +578,76 @@ public static ZwinderStateManager Create(BinaryReader br, ZwinderStateManagerSet
public void SaveStateHistory(BinaryWriter bw)
{
// version
bw.Write((byte)1);
bw.Write((byte)2);

bw.Write((byte)Settings.StatesToSave);

_current.SaveStateBinary(bw);
_recent.SaveStateBinary(bw);
_gapFiller.SaveStateBinary(bw);
if (Settings.StatesToSave == ZwinderStateManagerSettings.States.All)
{
_current.SaveStateBinary(bw);
_recent.SaveStateBinary(bw);
_gapFiller.SaveStateBinary(bw);

bw.Write(_reserved.Count);
foreach (var (f, data) in _reserved)
bw.Write(_reserved.Count);
foreach (var (f, data) in _reserved)
{
bw.Write(f);
bw.Write(data.Length);
bw.Write(data);
}
}
else if (Settings.StatesToSave == ZwinderStateManagerSettings.States.ReservedOnly)
{
bw.Write(f);
bw.Write(data.Length);
bw.Write(data);
// When we save with "reserve only", we will want at least 1 state per "ancient interval" number of frames.
// This means we may have to pull some from the ring buffers.
List<StateInfo> allStates = AllStates().ToList();
SortedList<int> stateFrames = new(allStates.Select((si) => si.Frame));
for (int i = 0; i < stateFrames.Count - 1; i++)
{
if (!_reserveCallback(stateFrames[i]) && !ShouldKeepForAncient(stateFrames[i], stateFrames))
{
stateFrames.RemoveAt(i);
i--;
}
}
// If the last state is not already reserved, it's probably safe to not keep it.
// And we can't really know if it would be kept anyway.
if (stateFrames.Count != 0 && !_reserveCallback(stateFrames[stateFrames.Count - 1]))
stateFrames.RemoveAt(stateFrames.Count - 1);

List<StateInfo> toSave = new();
foreach (StateInfo si in allStates)
{
if (stateFrames.Contains(si.Frame))
toSave.Add(si);
}

bw.Write(toSave.Count);
for (int i = 0; i < toSave.Count; i++)
{
bw.Write(toSave[i].Frame);
bw.Write(toSave[i].Size);
toSave[i].Read().CopyTo(bw.BaseStream);
}
}
else if (Settings.StatesToSave == ZwinderStateManagerSettings.States.MarkersOnly)
{
// Some states that should be reserved via marker might be in a ring buffer but not yet in _reserved.
List<StateInfo> allStates = AllStates().ToList();
List<StateInfo> toSave = new();
for (int i = 0; i < allStates.Count; i++)
{
if (_reserveCallback(allStates[i].Frame))
toSave.Add(allStates[i]);
}

bw.Write(toSave.Count);
for (int i = 0; i < toSave.Count; i++)
{
bw.Write(toSave[i].Frame);
bw.Write(toSave[i].Size);
toSave[i].Read().CopyTo(bw.BaseStream);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public ZwinderStateManagerSettings(ZwinderStateManagerSettings settings)

AncientStateInterval = settings.AncientStateInterval;
AncientStoreType = settings.AncientStoreType;

StatesToSave = settings.StatesToSave;
}

/// <summary>
Expand Down Expand Up @@ -109,6 +111,21 @@ public ZwinderStateManagerSettings(ZwinderStateManagerSettings settings)
[Description("Where to keep the reserved states.")]
public IRewindSettings.BackingStoreType AncientStoreType { get; set; } = IRewindSettings.BackingStoreType.Memory;

public enum States : byte
{
All,
[Display(Name = "Reserved Only")]
ReservedOnly,
[Display(Name = "Markers Only")]
MarkersOnly,
None,
}

[DisplayName("States To Save")]
[Description("Which states should be included in the .tasproj file when saved. 'Reserved' states include ancient states and states for markers.")]
[TypeConverter(typeof(DescribableEnumConverter))]
public States StatesToSave { get; set; } = States.ReservedOnly;

// Just to simplify some other code.
public RewindConfig Current()
{
Expand Down
Loading