A round of cleanup on rewind code - break subclass into their own files, into a rewind subfolder, cleanup some formatting and naming
This commit is contained in:
parent
f090597fbe
commit
edb5560da7
|
@ -432,7 +432,9 @@
|
||||||
</Compile>
|
</Compile>
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Rewind.cs" />
|
<Compile Include="rewind\Rewinder.cs" />
|
||||||
|
<Compile Include="rewind\RewindThreader.cs" />
|
||||||
|
<Compile Include="rewind\StreamBlobDatabase.cs" />
|
||||||
<Compile Include="ScanlineSlider.cs">
|
<Compile Include="ScanlineSlider.cs">
|
||||||
<SubType>Form</SubType>
|
<SubType>Form</SubType>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
|
|
@ -2473,20 +2473,16 @@ namespace BizHawk.Client.EmuHawk
|
||||||
{
|
{
|
||||||
GlobalWin.Rewinder.Rewind(1);
|
GlobalWin.Rewinder.Rewind(1);
|
||||||
suppressCaptureRewind = true;
|
suppressCaptureRewind = true;
|
||||||
if (0 == GlobalWin.Rewinder.RewindBuf.Count)
|
|
||||||
{
|
runFrame = !(GlobalWin.Rewinder.Count == 0);
|
||||||
runFrame = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
runFrame = true;
|
|
||||||
}
|
|
||||||
//we don't want to capture input when rewinding, even in record mode
|
//we don't want to capture input when rewinding, even in record mode
|
||||||
if (Global.MovieSession.Movie.IsRecording)
|
if (Global.MovieSession.Movie.IsRecording)
|
||||||
{
|
{
|
||||||
Global.MovieSession.Movie.SwitchToPlay();
|
Global.MovieSession.Movie.SwitchToPlay();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UpdateFrame)
|
if (UpdateFrame)
|
||||||
{
|
{
|
||||||
runFrame = true;
|
runFrame = true;
|
||||||
|
|
|
@ -1,656 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
|
|
||||||
using BizHawk.Client.Common;
|
|
||||||
|
|
||||||
namespace BizHawk.Client.EmuHawk
|
|
||||||
{
|
|
||||||
public class Rewinder
|
|
||||||
{
|
|
||||||
public bool RewindActive = true;
|
|
||||||
|
|
||||||
public StreamBlobDatabase RewindBuf;// = new StreamBlobDatabase(Global.Config.Rewind_OnDisk, Global.Config.Rewind_BufferSize * (long)1024 * (long)1024);
|
|
||||||
private RewindThreader RewindThread;
|
|
||||||
|
|
||||||
private byte[] LastState;
|
|
||||||
private bool RewindImpossible;
|
|
||||||
private int RewindFrequency = 1;
|
|
||||||
private bool RewindDeltaEnable = false;
|
|
||||||
|
|
||||||
public float Rewind_FullnessRatio { get { return RewindBuf.FullnessRatio; } }
|
|
||||||
public int Rewind_Count { get { return RewindBuf != null ? RewindBuf.Count : 0; } }
|
|
||||||
public long Rewind_Size { get { return RewindBuf != null ? RewindBuf.Size : 0; } }
|
|
||||||
/// <summary>
|
|
||||||
/// Manages a ring buffer of storage which can continually chow its own tail to keep growing forward.
|
|
||||||
/// Probably only useful for the rewind buffer, so I didnt put it in another file
|
|
||||||
/// </summary>
|
|
||||||
public class StreamBlobDatabase : IDisposable
|
|
||||||
{
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
mStream.Dispose();
|
|
||||||
mStream = null;
|
|
||||||
if (mAllocatedBuffer != null)
|
|
||||||
mBufferManage(mAllocatedBuffer, 0, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
Func<byte[], long, bool, byte[]> mBufferManage;
|
|
||||||
byte[] mAllocatedBuffer;
|
|
||||||
|
|
||||||
public StreamBlobDatabase(bool onDisk, long capacity, Func<byte[],long,bool,byte[]> BufferManage)
|
|
||||||
{
|
|
||||||
this.mBufferManage = BufferManage;
|
|
||||||
mCapacity = capacity;
|
|
||||||
if (onDisk)
|
|
||||||
{
|
|
||||||
var path = Path.Combine(System.IO.Path.GetTempPath(), "bizhawk.rewindbuf-pid" + System.Diagnostics.Process.GetCurrentProcess().Id + "-" + Guid.NewGuid().ToString());
|
|
||||||
|
|
||||||
//I checked the DeleteOnClose operation to make sure it cleans up when the process is aborted, and it seems to.
|
|
||||||
//Otherwise we would have a more complex tempfile management problem here.
|
|
||||||
//4KB buffer chosen due to similarity to .net defaults, and fear of anything larger making hiccups for small systems (we could try asyncing this stuff though...)
|
|
||||||
mStream = new FileStream(path, FileMode.Create, System.Security.AccessControl.FileSystemRights.FullControl, FileShare.None, 4*1024, FileOptions.DeleteOnClose);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mAllocatedBuffer = mBufferManage(null, capacity, true);
|
|
||||||
mStream = new MemoryStream(mAllocatedBuffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ListItem
|
|
||||||
{
|
|
||||||
public ListItem(int _timestamp, long _index, int _length) { this.timestamp = _timestamp; this.index = _index; this.length = _length; }
|
|
||||||
public int timestamp;
|
|
||||||
public long index;
|
|
||||||
public int length;
|
|
||||||
public long endExclusive { get { return index + length; } }
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream mStream;
|
|
||||||
LinkedList<ListItem> mBookmarks = new LinkedList<ListItem>();
|
|
||||||
LinkedListNode<ListItem> mHead, mTail;
|
|
||||||
long mCapacity, mSize;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns the amount of the buffer that's used
|
|
||||||
/// </summary>
|
|
||||||
public long Size { get { return mSize; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current fullness ratio (Size/Capacity). Note that this wont reach 100% due to the buffer size not being a multiple of a fixed savestate size.
|
|
||||||
/// </summary>
|
|
||||||
public float FullnessRatio { get { return (float)((double)Size / (double)mCapacity); } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// the number of frames stored here
|
|
||||||
/// </summary>
|
|
||||||
public int Count { get { return mBookmarks.Count; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The underlying stream to
|
|
||||||
/// </summary>
|
|
||||||
public Stream Stream { get { return mStream; } }
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
mHead = mTail = null;
|
|
||||||
mSize = 0;
|
|
||||||
mBookmarks.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The push and pop semantics are for historical reasons and not resemblence to normal definitions
|
|
||||||
/// </summary>
|
|
||||||
public void Push(ArraySegment<byte> seg)
|
|
||||||
{
|
|
||||||
var buf = seg.Array;
|
|
||||||
int len = seg.Count;
|
|
||||||
long offset = Enqueue(0, len);
|
|
||||||
mStream.Position = offset;
|
|
||||||
mStream.Write(buf, seg.Offset, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The push and pop semantics are for historical reasons and not resemblence to normal definitions
|
|
||||||
/// </summary>
|
|
||||||
public MemoryStream PopMemoryStream()
|
|
||||||
{
|
|
||||||
var item = Pop();
|
|
||||||
var buf = new byte[item.length];
|
|
||||||
mStream.Position = item.index;
|
|
||||||
mStream.Read(buf, 0, item.length);
|
|
||||||
var ret = new MemoryStream(buf, 0, item.length, false, true);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long Enqueue(int timestamp, int amount)
|
|
||||||
{
|
|
||||||
mSize += amount;
|
|
||||||
|
|
||||||
if (mHead == null)
|
|
||||||
{
|
|
||||||
mTail = mHead = mBookmarks.AddFirst(new ListItem(timestamp, 0, amount));
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
long target = mHead.Value.endExclusive + amount;
|
|
||||||
if (mTail != null && target <= mTail.Value.index)
|
|
||||||
{
|
|
||||||
//theres room to add a new head before the tail
|
|
||||||
mHead = mBookmarks.AddAfter(mHead, new ListItem(timestamp, mHead.Value.endExclusive, amount));
|
|
||||||
goto CLEANUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
//maybe the tail is earlier than the head
|
|
||||||
if (mTail.Value.index < mHead.Value.index)
|
|
||||||
{
|
|
||||||
if (target <= mCapacity)
|
|
||||||
{
|
|
||||||
//theres room to add a new head before the end of capacity
|
|
||||||
mHead = mBookmarks.AddAfter(mHead, new ListItem(timestamp, mHead.Value.endExclusive, amount));
|
|
||||||
goto CLEANUP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
//nope, tail is after head. we'll have to clobber from the tail
|
|
||||||
mHead = mBookmarks.AddAfter(mHead, new ListItem(timestamp, mHead.Value.endExclusive, amount));
|
|
||||||
goto CLEANUP;
|
|
||||||
}
|
|
||||||
|
|
||||||
//no room before the tail, or before capacity. head needs to wrap around.
|
|
||||||
mHead = mBookmarks.AddAfter(mHead, new ListItem(timestamp, 0, amount));
|
|
||||||
|
|
||||||
CLEANUP:
|
|
||||||
//while the head impinges on tail items, discard them
|
|
||||||
for (; ; )
|
|
||||||
{
|
|
||||||
if (mTail == null) break;
|
|
||||||
if (mHead.Value.endExclusive > mTail.Value.index && mHead.Value.index <= mTail.Value.index)
|
|
||||||
{
|
|
||||||
LinkedListNode<ListItem> nextTail = mTail.Next;
|
|
||||||
mSize -= mTail.Value.length;
|
|
||||||
mBookmarks.Remove(mTail);
|
|
||||||
mTail = nextTail;
|
|
||||||
}
|
|
||||||
else break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mHead.Value.index;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListItem Pop()
|
|
||||||
{
|
|
||||||
if (mHead == null) throw new InvalidOperationException("Attempted to pop from an empty data structure");
|
|
||||||
var ret = mHead.Value;
|
|
||||||
mSize -= ret.length;
|
|
||||||
LinkedListNode<ListItem> nextHead = mHead.Previous;
|
|
||||||
mBookmarks.Remove(mHead);
|
|
||||||
if (mHead == mTail)
|
|
||||||
mTail = null;
|
|
||||||
mHead = nextHead;
|
|
||||||
if (mHead == null)
|
|
||||||
mHead = mBookmarks.Last;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ListItem Dequeue()
|
|
||||||
{
|
|
||||||
if (mTail == null) throw new InvalidOperationException("Attempted to dequeue from an empty data structure");
|
|
||||||
var ret = mTail.Value;
|
|
||||||
mSize -= ret.length;
|
|
||||||
LinkedListNode<ListItem> nextTail = mTail.Next;
|
|
||||||
mBookmarks.Remove(mTail);
|
|
||||||
if (mTail == mHead)
|
|
||||||
mHead = null;
|
|
||||||
mTail = nextTail;
|
|
||||||
if (mTail == null)
|
|
||||||
mTail = mBookmarks.First;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
//-------- tests ---------
|
|
||||||
public void AssertMonotonic()
|
|
||||||
{
|
|
||||||
if (mTail == null) return;
|
|
||||||
int ts = mTail.Value.timestamp;
|
|
||||||
LinkedListNode<ListItem> curr = mTail;
|
|
||||||
for (; ; )
|
|
||||||
{
|
|
||||||
if (curr == null)
|
|
||||||
curr = mBookmarks.First;
|
|
||||||
if (curr == null) break;
|
|
||||||
System.Diagnostics.Debug.Assert(curr.Value.timestamp >= ts);
|
|
||||||
if (curr == mHead) return;
|
|
||||||
ts = curr.Value.timestamp;
|
|
||||||
curr = curr.Next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//void Test()
|
|
||||||
//{
|
|
||||||
// var sbb = new StreamBlobDatabase(false, Global.Config.Rewind_BufferSize * 1024 * 1024);
|
|
||||||
// var rand = new Random(0);
|
|
||||||
// int timestamp = 0;
|
|
||||||
// for (; ; )
|
|
||||||
// {
|
|
||||||
// long test = sbb.Enqueue(timestamp, rand.Next(100 * 1024));
|
|
||||||
// if (rand.Next(10) == 0)
|
|
||||||
// if (sbb.Count != 0) sbb.Dequeue();
|
|
||||||
// if (rand.Next(10) == 0)
|
|
||||||
// if (sbb.Count != 0) sbb.Pop();
|
|
||||||
// if (rand.Next(50) == 1)
|
|
||||||
// {
|
|
||||||
// while (sbb.Count != 0)
|
|
||||||
// {
|
|
||||||
// Console.WriteLine("ZAM!!!");
|
|
||||||
// sbb.Dequeue();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// sbb.AssertMonotonic();
|
|
||||||
// timestamp++;
|
|
||||||
// Console.WriteLine("{0}, {1}", test, sbb.Count);
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
} //class StreamBlobDatabase
|
|
||||||
|
|
||||||
class RewindThreader : IDisposable
|
|
||||||
{
|
|
||||||
//adelikat: tweak this to test performance with threading or not with threading
|
|
||||||
// natt: IEmulator.SaveStateBinary() returns a byte[] but you're not allowed to modify it,
|
|
||||||
// and if the method is called again, the data from a previous call could be invalidated.
|
|
||||||
// GPGX and N64 make use of this property. if you set IsThreaded = true, you'll need to Clone() in many cases,
|
|
||||||
// which will kill N64 for sure...
|
|
||||||
public static bool IsThreaded = false;
|
|
||||||
|
|
||||||
Rewinder mf;
|
|
||||||
|
|
||||||
public RewindThreader(Rewinder mf, bool isThreaded)
|
|
||||||
{
|
|
||||||
IsThreaded = isThreaded;
|
|
||||||
this.mf = mf;
|
|
||||||
|
|
||||||
if (IsThreaded)
|
|
||||||
{
|
|
||||||
ewh = new EventWaitHandle(false, EventResetMode.AutoReset);
|
|
||||||
ewh2 = new EventWaitHandle(false, EventResetMode.AutoReset);
|
|
||||||
thread = new Thread(ThreadProc);
|
|
||||||
thread.IsBackground = true;
|
|
||||||
thread.Start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (!IsThreaded)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var job = new Job();
|
|
||||||
job.Type = JobType.Abort;
|
|
||||||
Jobs.Enqueue(job);
|
|
||||||
ewh.Set();
|
|
||||||
|
|
||||||
thread.Join();
|
|
||||||
ewh.Dispose();
|
|
||||||
ewh2.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ThreadProc()
|
|
||||||
{
|
|
||||||
for (; ; )
|
|
||||||
{
|
|
||||||
ewh.WaitOne();
|
|
||||||
while (Jobs.Count != 0)
|
|
||||||
{
|
|
||||||
Job job = null;
|
|
||||||
if (Jobs.TryDequeue(out job))
|
|
||||||
{
|
|
||||||
if (job.Type == JobType.Abort)
|
|
||||||
return;
|
|
||||||
if (job.Type == JobType.Capture)
|
|
||||||
{
|
|
||||||
mf._RunCapture(job.CoreState);
|
|
||||||
}
|
|
||||||
if (job.Type == JobType.Rewind)
|
|
||||||
{
|
|
||||||
mf._RunRewind(job.Frames);
|
|
||||||
ewh2.Set();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EventWaitHandle ewh, ewh2;
|
|
||||||
Thread thread;
|
|
||||||
|
|
||||||
public void Rewind(int frames)
|
|
||||||
{
|
|
||||||
if (!IsThreaded)
|
|
||||||
{
|
|
||||||
mf._RunRewind(frames);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var job = new Job();
|
|
||||||
job.Type = JobType.Rewind;
|
|
||||||
job.Frames = frames;
|
|
||||||
Jobs.Enqueue(job);
|
|
||||||
ewh.Set();
|
|
||||||
ewh2.WaitOne();
|
|
||||||
}
|
|
||||||
|
|
||||||
void DoSafeEnqueue(Job job)
|
|
||||||
{
|
|
||||||
Jobs.Enqueue(job);
|
|
||||||
ewh.Set();
|
|
||||||
|
|
||||||
//just in case... we're getting really behind.. slow it down here
|
|
||||||
//if this gets backed up too much, then the rewind will seem to malfunction since it requires all the captures in the queue to complete first
|
|
||||||
while (Jobs.Count > 15)
|
|
||||||
{
|
|
||||||
Thread.Sleep(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Capture(byte[] coreSavestate)
|
|
||||||
{
|
|
||||||
if (!IsThreaded)
|
|
||||||
{
|
|
||||||
mf._RunCapture(coreSavestate);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var job = new Job();
|
|
||||||
job.Type = JobType.Capture;
|
|
||||||
job.CoreState = coreSavestate;
|
|
||||||
DoSafeEnqueue(job);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum JobType
|
|
||||||
{
|
|
||||||
Capture, Rewind, Abort
|
|
||||||
}
|
|
||||||
|
|
||||||
class Job
|
|
||||||
{
|
|
||||||
public JobType Type;
|
|
||||||
public byte[] CoreState;
|
|
||||||
public int Frames;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConcurrentQueue<Job> Jobs = new ConcurrentQueue<Job>();
|
|
||||||
}
|
|
||||||
|
|
||||||
// TOOD: this should not be parameterless?! It is only possible due to passing a static context in
|
|
||||||
public void CaptureRewindState()
|
|
||||||
{
|
|
||||||
if (RewindImpossible)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (LastState == null)
|
|
||||||
{
|
|
||||||
DoRewindSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
//log a frame
|
|
||||||
if (LastState != null && Global.Emulator.Frame % RewindFrequency == 0)
|
|
||||||
{
|
|
||||||
byte[] CurrentState = Global.Emulator.SaveStateBinary();
|
|
||||||
RewindThread.Capture(CurrentState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetRewindParams(bool enabled, int frequency)
|
|
||||||
{
|
|
||||||
if (RewindActive != enabled)
|
|
||||||
{
|
|
||||||
GlobalWin.OSD.AddMessage("Rewind " + (enabled ? "Enabled" : "Disabled"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RewindFrequency != frequency && enabled)
|
|
||||||
{
|
|
||||||
GlobalWin.OSD.AddMessage("Rewind frequency set to " + frequency);
|
|
||||||
}
|
|
||||||
|
|
||||||
RewindActive = enabled;
|
|
||||||
RewindFrequency = frequency;
|
|
||||||
|
|
||||||
if(!RewindActive)
|
|
||||||
LastState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DoRewindSettings()
|
|
||||||
{
|
|
||||||
// This is the first frame. Capture the state, and put it in LastState for future deltas to be compared against.
|
|
||||||
LastState = (byte[])Global.Emulator.SaveStateBinary().Clone();
|
|
||||||
|
|
||||||
int state_size = 0;
|
|
||||||
if (LastState.Length >= Global.Config.Rewind_LargeStateSize)
|
|
||||||
{
|
|
||||||
SetRewindParams(Global.Config.RewindEnabledLarge, Global.Config.RewindFrequencyLarge);
|
|
||||||
state_size = 3;
|
|
||||||
}
|
|
||||||
else if (LastState.Length >= Global.Config.Rewind_MediumStateSize)
|
|
||||||
{
|
|
||||||
SetRewindParams(Global.Config.RewindEnabledMedium, Global.Config.RewindFrequencyMedium);
|
|
||||||
state_size = 2;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SetRewindParams(Global.Config.RewindEnabledSmall, Global.Config.RewindFrequencySmall);
|
|
||||||
state_size = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool rewind_enabled = false;
|
|
||||||
if (state_size == 1) rewind_enabled = Global.Config.RewindEnabledSmall;
|
|
||||||
if (state_size == 2) rewind_enabled = Global.Config.RewindEnabledMedium;
|
|
||||||
if (state_size == 3) rewind_enabled = Global.Config.RewindEnabledLarge;
|
|
||||||
|
|
||||||
RewindDeltaEnable = Global.Config.Rewind_UseDelta;
|
|
||||||
|
|
||||||
if (rewind_enabled)
|
|
||||||
{
|
|
||||||
long cap = Global.Config.Rewind_BufferSize * (long)1024 * (long)1024;
|
|
||||||
|
|
||||||
if(RewindBuf != null)
|
|
||||||
RewindBuf.Dispose();
|
|
||||||
RewindBuf = new StreamBlobDatabase(Global.Config.Rewind_OnDisk, cap, BufferManage);
|
|
||||||
|
|
||||||
if (RewindThread != null)
|
|
||||||
RewindThread.Dispose();
|
|
||||||
RewindThread = new RewindThreader(this, Global.Config.Rewind_IsThreaded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] RewindFellationBuf;
|
|
||||||
byte[] BufferManage(byte[] inbuf, long size, bool allocate)
|
|
||||||
{
|
|
||||||
if (allocate)
|
|
||||||
{
|
|
||||||
//if we have an appropriate buffer free, return it
|
|
||||||
if (RewindFellationBuf != null && RewindFellationBuf.LongLength == size)
|
|
||||||
{
|
|
||||||
byte[] ret = RewindFellationBuf;
|
|
||||||
RewindFellationBuf = null;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
//otherwise, allocate it
|
|
||||||
return new byte[size];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RewindFellationBuf = inbuf;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CaptureRewindStateNonDelta(byte[] CurrentState)
|
|
||||||
{
|
|
||||||
long offset = RewindBuf.Enqueue(0, CurrentState.Length + 1);
|
|
||||||
Stream stream = RewindBuf.Stream;
|
|
||||||
stream.Position = offset;
|
|
||||||
|
|
||||||
//write the header for a non-delta frame
|
|
||||||
stream.WriteByte(1); //i.e. true
|
|
||||||
stream.Write(CurrentState, 0, CurrentState.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] TempBuf = new byte[0];
|
|
||||||
void CaptureRewindStateDelta(byte[] CurrentState, bool isSmall)
|
|
||||||
{
|
|
||||||
//in case the state sizes mismatch, capture a full state rather than trying to do anything clever
|
|
||||||
if (CurrentState.Length != LastState.Length)
|
|
||||||
{
|
|
||||||
CaptureRewindStateNonDelta(CurrentState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int beginChangeSequence = -1;
|
|
||||||
bool inChangeSequence = false;
|
|
||||||
MemoryStream ms;
|
|
||||||
|
|
||||||
//try to set up the buffer in advance so we dont ever have exceptions in here
|
|
||||||
if(TempBuf.Length < CurrentState.Length)
|
|
||||||
TempBuf = new byte[CurrentState.Length*2];
|
|
||||||
|
|
||||||
ms = new MemoryStream(TempBuf, 0, TempBuf.Length, true, true);
|
|
||||||
RETRY:
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var writer = new BinaryWriter(ms);
|
|
||||||
writer.Write(false); // delta state
|
|
||||||
for (int i = 0; i < CurrentState.Length; i++)
|
|
||||||
{
|
|
||||||
if (inChangeSequence == false)
|
|
||||||
{
|
|
||||||
if (i >= LastState.Length)
|
|
||||||
continue;
|
|
||||||
if (CurrentState[i] == LastState[i])
|
|
||||||
continue;
|
|
||||||
|
|
||||||
inChangeSequence = true;
|
|
||||||
beginChangeSequence = i;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i - beginChangeSequence == 254 || i == CurrentState.Length - 1)
|
|
||||||
{
|
|
||||||
writer.Write((byte)(i - beginChangeSequence + 1));
|
|
||||||
if (isSmall) writer.Write((ushort)beginChangeSequence);
|
|
||||||
else writer.Write(beginChangeSequence);
|
|
||||||
writer.Write(LastState, beginChangeSequence, i - beginChangeSequence + 1);
|
|
||||||
inChangeSequence = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CurrentState[i] == LastState[i])
|
|
||||||
{
|
|
||||||
writer.Write((byte)(i - beginChangeSequence));
|
|
||||||
if (isSmall) writer.Write((ushort)beginChangeSequence);
|
|
||||||
else writer.Write(beginChangeSequence);
|
|
||||||
writer.Write(LastState, beginChangeSequence, i - beginChangeSequence);
|
|
||||||
inChangeSequence = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (NotSupportedException)
|
|
||||||
{
|
|
||||||
//ok... we had an exception after all
|
|
||||||
//if we did actually run out of room in the memorystream, then try it again with a bigger buffer
|
|
||||||
TempBuf = new byte[TempBuf.Length * 2];
|
|
||||||
goto RETRY;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LastState != null && LastState.Length == CurrentState.Length)
|
|
||||||
Buffer.BlockCopy(CurrentState, 0, LastState, 0, LastState.Length);
|
|
||||||
else
|
|
||||||
LastState = (byte[])CurrentState.Clone();
|
|
||||||
var seg = new ArraySegment<byte>(TempBuf, 0, (int)ms.Position);
|
|
||||||
RewindBuf.Push(seg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void RewindLarge() { RewindDelta(false); }
|
|
||||||
void Rewind64K() { RewindDelta(true); }
|
|
||||||
void RewindDelta(bool isSmall)
|
|
||||||
{
|
|
||||||
var ms = RewindBuf.PopMemoryStream();
|
|
||||||
var reader = new BinaryReader(ms);
|
|
||||||
bool fullstate = reader.ReadBoolean();
|
|
||||||
if (fullstate)
|
|
||||||
{
|
|
||||||
Global.Emulator.LoadStateBinary(reader);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var output = new MemoryStream(LastState);
|
|
||||||
while (ms.Position < ms.Length - 1)
|
|
||||||
{
|
|
||||||
byte len = reader.ReadByte();
|
|
||||||
int offset;
|
|
||||||
if(isSmall)
|
|
||||||
offset = reader.ReadUInt16();
|
|
||||||
else offset = reader.ReadInt32();
|
|
||||||
output.Position = offset;
|
|
||||||
output.Write(ms.GetBuffer(), (int)ms.Position, len);
|
|
||||||
ms.Position += len;
|
|
||||||
}
|
|
||||||
reader.Close();
|
|
||||||
output.Position = 0;
|
|
||||||
Global.Emulator.LoadStateBinary(new BinaryReader(output));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Rewind(int frames)
|
|
||||||
{
|
|
||||||
RewindThread.Rewind(frames);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _RunRewind(int frames)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < frames; i++)
|
|
||||||
{
|
|
||||||
if (RewindBuf.Count == 0 || (Global.MovieSession.Movie.IsActive && Global.MovieSession.Movie.InputLogLength == 0))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (LastState.Length < 0x10000)
|
|
||||||
{
|
|
||||||
Rewind64K();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RewindLarge();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _RunCapture(byte[] coreSavestate)
|
|
||||||
{
|
|
||||||
if (RewindDeltaEnable)
|
|
||||||
{
|
|
||||||
if (LastState.Length <= 0x10000)
|
|
||||||
CaptureRewindStateDelta(coreSavestate, true);
|
|
||||||
else
|
|
||||||
CaptureRewindStateDelta(coreSavestate, false);
|
|
||||||
}
|
|
||||||
else CaptureRewindStateNonDelta(coreSavestate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetRewindBuffer()
|
|
||||||
{
|
|
||||||
if (RewindBuf != null) { RewindBuf.Clear(); }
|
|
||||||
RewindImpossible = false;
|
|
||||||
LastState = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int RewindBufferCount()
|
|
||||||
{
|
|
||||||
return RewindBuf.Count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,10 +19,10 @@ namespace BizHawk.Client.EmuHawk
|
||||||
|
|
||||||
private void RewindConfig_Load(object sender, EventArgs e)
|
private void RewindConfig_Load(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (GlobalWin.Rewinder.RewindBuf != null)
|
if (GlobalWin.Rewinder.HasBuffer)
|
||||||
{
|
{
|
||||||
FullnessLabel.Text = String.Format("{0:0.00}", GlobalWin.Rewinder.Rewind_FullnessRatio * 100) + "%";
|
FullnessLabel.Text = String.Format("{0:0.00}", GlobalWin.Rewinder.FullnessRatio * 100) + "%";
|
||||||
RewindFramesUsedLabel.Text = GlobalWin.Rewinder.Rewind_Count.ToString();
|
RewindFramesUsedLabel.Text = GlobalWin.Rewinder.Count.ToString();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -260,9 +260,9 @@ namespace BizHawk.Client.EmuHawk
|
||||||
if (UseDeltaCompression.Checked || _stateSize == 0)
|
if (UseDeltaCompression.Checked || _stateSize == 0)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (GlobalWin.Rewinder.Rewind_Count > 0)
|
if (GlobalWin.Rewinder.Count > 0)
|
||||||
{
|
{
|
||||||
avg_state_size = GlobalWin.Rewinder.Rewind_Size / GlobalWin.Rewinder.Rewind_Count;
|
avg_state_size = GlobalWin.Rewinder.Size / GlobalWin.Rewinder.Count;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace BizHawk.Client.EmuHawk
|
||||||
|
{
|
||||||
|
public class RewindThreader : IDisposable
|
||||||
|
{
|
||||||
|
// adelikat: tweak this to test performance with threading or not with threading
|
||||||
|
// natt: IEmulator.SaveStateBinary() returns a byte[] but you're not allowed to modify it,
|
||||||
|
// and if the method is called again, the data from a previous call could be invalidated.
|
||||||
|
// GPGX and N64 make use of this property. if you set IsThreaded = true, you'll need to Clone() in many cases,
|
||||||
|
// which will kill N64 for sure...
|
||||||
|
public static bool IsThreaded = false;
|
||||||
|
|
||||||
|
private readonly ConcurrentQueue<Job> Jobs = new ConcurrentQueue<Job>();
|
||||||
|
private EventWaitHandle _ewh, _ewh2;
|
||||||
|
private Thread _thread;
|
||||||
|
|
||||||
|
// TODO: this is bad design!
|
||||||
|
private Rewinder _rewinder;
|
||||||
|
|
||||||
|
public RewindThreader(Rewinder rewinder, bool isThreaded)
|
||||||
|
{
|
||||||
|
IsThreaded = isThreaded;
|
||||||
|
_rewinder = rewinder;
|
||||||
|
|
||||||
|
if (IsThreaded)
|
||||||
|
{
|
||||||
|
_ewh = new EventWaitHandle(false, EventResetMode.AutoReset);
|
||||||
|
_ewh2 = new EventWaitHandle(false, EventResetMode.AutoReset);
|
||||||
|
_thread = new Thread(ThreadProc);
|
||||||
|
_thread.IsBackground = true;
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!IsThreaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var job = new Job();
|
||||||
|
job.Type = JobType.Abort;
|
||||||
|
Jobs.Enqueue(job);
|
||||||
|
_ewh.Set();
|
||||||
|
|
||||||
|
_thread.Join();
|
||||||
|
_ewh.Dispose();
|
||||||
|
_ewh2.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThreadProc()
|
||||||
|
{
|
||||||
|
for (; ; )
|
||||||
|
{
|
||||||
|
_ewh.WaitOne();
|
||||||
|
while (Jobs.Count != 0)
|
||||||
|
{
|
||||||
|
Job job = null;
|
||||||
|
if (Jobs.TryDequeue(out job))
|
||||||
|
{
|
||||||
|
if (job.Type == JobType.Abort)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.Type == JobType.Capture)
|
||||||
|
{
|
||||||
|
_rewinder.RunCapture(job.CoreState);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (job.Type == JobType.Rewind)
|
||||||
|
{
|
||||||
|
_rewinder._RunRewind(job.Frames);
|
||||||
|
_ewh2.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rewind(int frames)
|
||||||
|
{
|
||||||
|
if (!IsThreaded)
|
||||||
|
{
|
||||||
|
_rewinder._RunRewind(frames);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var job = new Job();
|
||||||
|
job.Type = JobType.Rewind;
|
||||||
|
job.Frames = frames;
|
||||||
|
Jobs.Enqueue(job);
|
||||||
|
_ewh.Set();
|
||||||
|
_ewh2.WaitOne();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoSafeEnqueue(Job job)
|
||||||
|
{
|
||||||
|
Jobs.Enqueue(job);
|
||||||
|
_ewh.Set();
|
||||||
|
|
||||||
|
//just in case... we're getting really behind.. slow it down here
|
||||||
|
//if this gets backed up too much, then the rewind will seem to malfunction since it requires all the captures in the queue to complete first
|
||||||
|
while (Jobs.Count > 15)
|
||||||
|
{
|
||||||
|
Thread.Sleep(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Capture(byte[] coreSavestate)
|
||||||
|
{
|
||||||
|
if (!IsThreaded)
|
||||||
|
{
|
||||||
|
_rewinder.RunCapture(coreSavestate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var job = new Job();
|
||||||
|
job.Type = JobType.Capture;
|
||||||
|
job.CoreState = coreSavestate;
|
||||||
|
DoSafeEnqueue(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum JobType
|
||||||
|
{
|
||||||
|
Capture, Rewind, Abort
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Job
|
||||||
|
{
|
||||||
|
public JobType Type;
|
||||||
|
public byte[] CoreState;
|
||||||
|
public int Frames;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,370 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
using BizHawk.Client.Common;
|
||||||
|
|
||||||
|
namespace BizHawk.Client.EmuHawk
|
||||||
|
{
|
||||||
|
public class Rewinder
|
||||||
|
{
|
||||||
|
public bool RewindActive = true;
|
||||||
|
|
||||||
|
private StreamBlobDatabase RewindBuffer;
|
||||||
|
private RewindThreader RewindThread;
|
||||||
|
private byte[] LastState;
|
||||||
|
private bool RewindImpossible;
|
||||||
|
private int RewindFrequency = 1;
|
||||||
|
private bool RewindDeltaEnable = false;
|
||||||
|
private byte[] RewindFellationBuf;
|
||||||
|
private byte[] TempBuf = new byte[0];
|
||||||
|
|
||||||
|
// TODO: make RewindBuf never be null
|
||||||
|
public float FullnessRatio
|
||||||
|
{
|
||||||
|
get { return RewindBuffer.FullnessRatio; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get { return RewindBuffer != null ? RewindBuffer.Count : 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Size
|
||||||
|
{
|
||||||
|
get { return RewindBuffer != null ? RewindBuffer.Size : 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public int BufferCount
|
||||||
|
{
|
||||||
|
get { return RewindBuffer != null ? RewindBuffer.Count : 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasBuffer
|
||||||
|
{
|
||||||
|
get { return RewindBuffer != null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TOOD: this should not be parameterless?! It is only possible due to passing a static context in
|
||||||
|
public void CaptureRewindState()
|
||||||
|
{
|
||||||
|
if (RewindImpossible)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LastState == null)
|
||||||
|
{
|
||||||
|
DoRewindSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//log a frame
|
||||||
|
if (LastState != null && Global.Emulator.Frame % RewindFrequency == 0)
|
||||||
|
{
|
||||||
|
byte[] CurrentState = Global.Emulator.SaveStateBinary();
|
||||||
|
RewindThread.Capture(CurrentState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DoRewindSettings()
|
||||||
|
{
|
||||||
|
// This is the first frame. Capture the state, and put it in LastState for future deltas to be compared against.
|
||||||
|
LastState = (byte[])Global.Emulator.SaveStateBinary().Clone();
|
||||||
|
|
||||||
|
int state_size = 0;
|
||||||
|
if (LastState.Length >= Global.Config.Rewind_LargeStateSize)
|
||||||
|
{
|
||||||
|
SetRewindParams(Global.Config.RewindEnabledLarge, Global.Config.RewindFrequencyLarge);
|
||||||
|
state_size = 3;
|
||||||
|
}
|
||||||
|
else if (LastState.Length >= Global.Config.Rewind_MediumStateSize)
|
||||||
|
{
|
||||||
|
SetRewindParams(Global.Config.RewindEnabledMedium, Global.Config.RewindFrequencyMedium);
|
||||||
|
state_size = 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetRewindParams(Global.Config.RewindEnabledSmall, Global.Config.RewindFrequencySmall);
|
||||||
|
state_size = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool rewind_enabled = false;
|
||||||
|
if (state_size == 1)
|
||||||
|
{
|
||||||
|
rewind_enabled = Global.Config.RewindEnabledSmall;
|
||||||
|
}
|
||||||
|
else if (state_size == 2)
|
||||||
|
{
|
||||||
|
rewind_enabled = Global.Config.RewindEnabledMedium;
|
||||||
|
}
|
||||||
|
else if (state_size == 3)
|
||||||
|
{
|
||||||
|
rewind_enabled = Global.Config.RewindEnabledLarge;
|
||||||
|
}
|
||||||
|
|
||||||
|
RewindDeltaEnable = Global.Config.Rewind_UseDelta;
|
||||||
|
|
||||||
|
if (rewind_enabled)
|
||||||
|
{
|
||||||
|
long cap = Global.Config.Rewind_BufferSize * (long)1024 * (long)1024;
|
||||||
|
|
||||||
|
if (RewindBuffer != null)
|
||||||
|
RewindBuffer.Dispose();
|
||||||
|
RewindBuffer = new StreamBlobDatabase(Global.Config.Rewind_OnDisk, cap, BufferManage);
|
||||||
|
|
||||||
|
if (RewindThread != null)
|
||||||
|
RewindThread.Dispose();
|
||||||
|
RewindThread = new RewindThreader(this, Global.Config.Rewind_IsThreaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Rewind(int frames)
|
||||||
|
{
|
||||||
|
RewindThread.Rewind(frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove me
|
||||||
|
public void _RunRewind(int frames)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < frames; i++)
|
||||||
|
{
|
||||||
|
if (RewindBuffer.Count == 0 || (Global.MovieSession.Movie.IsActive && Global.MovieSession.Movie.InputLogLength == 0))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LastState.Length < 0x10000)
|
||||||
|
{
|
||||||
|
Rewind64K();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RewindLarge();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: only run by RewindThreader, refactor
|
||||||
|
public void RunCapture(byte[] coreSavestate)
|
||||||
|
{
|
||||||
|
if (RewindDeltaEnable)
|
||||||
|
{
|
||||||
|
if (LastState.Length <= 0x10000)
|
||||||
|
{
|
||||||
|
CaptureRewindStateDelta(coreSavestate, true);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CaptureRewindStateDelta(coreSavestate, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CaptureRewindStateNonDelta(coreSavestate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetRewindBuffer()
|
||||||
|
{
|
||||||
|
if (RewindBuffer != null)
|
||||||
|
{
|
||||||
|
RewindBuffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
RewindImpossible = false;
|
||||||
|
LastState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetRewindParams(bool enabled, int frequency)
|
||||||
|
{
|
||||||
|
if (RewindActive != enabled)
|
||||||
|
{
|
||||||
|
GlobalWin.OSD.AddMessage("Rewind " + (enabled ? "Enabled" : "Disabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RewindFrequency != frequency && enabled)
|
||||||
|
{
|
||||||
|
GlobalWin.OSD.AddMessage("Rewind frequency set to " + frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
RewindActive = enabled;
|
||||||
|
RewindFrequency = frequency;
|
||||||
|
|
||||||
|
if (!RewindActive)
|
||||||
|
{
|
||||||
|
LastState = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] BufferManage(byte[] inbuf, long size, bool allocate)
|
||||||
|
{
|
||||||
|
if (allocate)
|
||||||
|
{
|
||||||
|
//if we have an appropriate buffer free, return it
|
||||||
|
if (RewindFellationBuf != null && RewindFellationBuf.LongLength == size)
|
||||||
|
{
|
||||||
|
byte[] ret = RewindFellationBuf;
|
||||||
|
RewindFellationBuf = null;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
//otherwise, allocate it
|
||||||
|
return new byte[size];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RewindFellationBuf = inbuf;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaptureRewindStateNonDelta(byte[] CurrentState)
|
||||||
|
{
|
||||||
|
long offset = RewindBuffer.Enqueue(0, CurrentState.Length + 1);
|
||||||
|
Stream stream = RewindBuffer.Stream;
|
||||||
|
stream.Position = offset;
|
||||||
|
|
||||||
|
//write the header for a non-delta frame
|
||||||
|
stream.WriteByte(1); //i.e. true
|
||||||
|
stream.Write(CurrentState, 0, CurrentState.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaptureRewindStateDelta(byte[] CurrentState, bool isSmall)
|
||||||
|
{
|
||||||
|
//in case the state sizes mismatch, capture a full state rather than trying to do anything clever
|
||||||
|
if (CurrentState.Length != LastState.Length)
|
||||||
|
{
|
||||||
|
CaptureRewindStateNonDelta(CurrentState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int beginChangeSequence = -1;
|
||||||
|
bool inChangeSequence = false;
|
||||||
|
MemoryStream ms;
|
||||||
|
|
||||||
|
// try to set up the buffer in advance so we dont ever have exceptions in here
|
||||||
|
if (TempBuf.Length < CurrentState.Length)
|
||||||
|
{
|
||||||
|
TempBuf = new byte[CurrentState.Length * 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
ms = new MemoryStream(TempBuf, 0, TempBuf.Length, true, true);
|
||||||
|
RETRY:
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var writer = new BinaryWriter(ms);
|
||||||
|
writer.Write(false); // delta state
|
||||||
|
for (int i = 0; i < CurrentState.Length; i++)
|
||||||
|
{
|
||||||
|
if (inChangeSequence == false)
|
||||||
|
{
|
||||||
|
if (i >= LastState.Length)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CurrentState[i] == LastState[i])
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
inChangeSequence = true;
|
||||||
|
beginChangeSequence = i;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i - beginChangeSequence == 254 || i == CurrentState.Length - 1)
|
||||||
|
{
|
||||||
|
writer.Write((byte)(i - beginChangeSequence + 1));
|
||||||
|
if (isSmall)
|
||||||
|
{
|
||||||
|
writer.Write((ushort)beginChangeSequence);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.Write(beginChangeSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(LastState, beginChangeSequence, i - beginChangeSequence + 1);
|
||||||
|
inChangeSequence = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CurrentState[i] == LastState[i])
|
||||||
|
{
|
||||||
|
writer.Write((byte)(i - beginChangeSequence));
|
||||||
|
if (isSmall)
|
||||||
|
{
|
||||||
|
writer.Write((ushort)beginChangeSequence);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.Write(beginChangeSequence);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(LastState, beginChangeSequence, i - beginChangeSequence);
|
||||||
|
inChangeSequence = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NotSupportedException)
|
||||||
|
{
|
||||||
|
//ok... we had an exception after all
|
||||||
|
//if we did actually run out of room in the memorystream, then try it again with a bigger buffer
|
||||||
|
TempBuf = new byte[TempBuf.Length * 2];
|
||||||
|
goto RETRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LastState != null && LastState.Length == CurrentState.Length)
|
||||||
|
{
|
||||||
|
Buffer.BlockCopy(CurrentState, 0, LastState, 0, LastState.Length);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LastState = (byte[])CurrentState.Clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
var seg = new ArraySegment<byte>(TempBuf, 0, (int)ms.Position);
|
||||||
|
RewindBuffer.Push(seg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RewindLarge()
|
||||||
|
{
|
||||||
|
RewindDelta(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Rewind64K()
|
||||||
|
{
|
||||||
|
RewindDelta(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RewindDelta(bool isSmall)
|
||||||
|
{
|
||||||
|
var ms = RewindBuffer.PopMemoryStream();
|
||||||
|
var reader = new BinaryReader(ms);
|
||||||
|
bool fullstate = reader.ReadBoolean();
|
||||||
|
if (fullstate)
|
||||||
|
{
|
||||||
|
Global.Emulator.LoadStateBinary(reader);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var output = new MemoryStream(LastState);
|
||||||
|
while (ms.Position < ms.Length - 1)
|
||||||
|
{
|
||||||
|
byte len = reader.ReadByte();
|
||||||
|
int offset;
|
||||||
|
if(isSmall)
|
||||||
|
offset = reader.ReadUInt16();
|
||||||
|
else offset = reader.ReadInt32();
|
||||||
|
output.Position = offset;
|
||||||
|
output.Write(ms.GetBuffer(), (int)ms.Position, len);
|
||||||
|
ms.Position += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.Close();
|
||||||
|
output.Position = 0;
|
||||||
|
Global.Emulator.LoadStateBinary(new BinaryReader(output));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,265 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
using BizHawk.Client.Common;
|
||||||
|
|
||||||
|
namespace BizHawk.Client.EmuHawk
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Manages a ring buffer of storage which can continually chow its own tail to keep growing forward.
|
||||||
|
/// Probably only useful for the rewind buffer
|
||||||
|
/// </summary>
|
||||||
|
public class StreamBlobDatabase : IDisposable
|
||||||
|
{
|
||||||
|
private Func<byte[], long, bool, byte[]> _mBufferManage;
|
||||||
|
private byte[] _mAllocatedBuffer;
|
||||||
|
private Stream _mStream;
|
||||||
|
private LinkedList<ListItem> _mBookmarks = new LinkedList<ListItem>();
|
||||||
|
private LinkedListNode<ListItem> _mHead, _mTail;
|
||||||
|
private long _mCapacity, _mSize;
|
||||||
|
|
||||||
|
public StreamBlobDatabase(bool onDisk, long capacity, Func<byte[], long, bool, byte[]> mBufferManage)
|
||||||
|
{
|
||||||
|
_mBufferManage = mBufferManage;
|
||||||
|
_mCapacity = capacity;
|
||||||
|
if (onDisk)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(System.IO.Path.GetTempPath(), "bizhawk.rewindbuf-pid" + System.Diagnostics.Process.GetCurrentProcess().Id + "-" + Guid.NewGuid().ToString());
|
||||||
|
|
||||||
|
// I checked the DeleteOnClose operation to make sure it cleans up when the process is aborted, and it seems to.
|
||||||
|
// Otherwise we would have a more complex tempfile management problem here.
|
||||||
|
// 4KB buffer chosen due to similarity to .net defaults, and fear of anything larger making hiccups for small systems (we could try asyncing this stuff though...)
|
||||||
|
_mStream = new FileStream(path, FileMode.Create, System.Security.AccessControl.FileSystemRights.FullControl, FileShare.None, 4 * 1024, FileOptions.DeleteOnClose);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_mAllocatedBuffer = _mBufferManage(null, capacity, true);
|
||||||
|
_mStream = new MemoryStream(_mAllocatedBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the amount of the buffer that's used
|
||||||
|
/// </summary>
|
||||||
|
public long Size { get { return _mSize; } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the current fullness ratio (Size/Capacity). Note that this wont reach 100% due to the buffer size not being a multiple of a fixed savestate size.
|
||||||
|
/// </summary>
|
||||||
|
public float FullnessRatio { get { return (float)((double)Size / (double)_mCapacity); } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// the number of frames stored here
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get { return _mBookmarks.Count; } }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The underlying stream to
|
||||||
|
/// </summary>
|
||||||
|
public Stream Stream { get { return _mStream; } }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_mStream.Dispose();
|
||||||
|
_mStream = null;
|
||||||
|
if (_mAllocatedBuffer != null)
|
||||||
|
{
|
||||||
|
_mBufferManage(_mAllocatedBuffer, 0, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_mHead = _mTail = null;
|
||||||
|
_mSize = 0;
|
||||||
|
_mBookmarks.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The push and pop semantics are for historical reasons and not resemblence to normal definitions
|
||||||
|
/// </summary>
|
||||||
|
public void Push(ArraySegment<byte> seg)
|
||||||
|
{
|
||||||
|
var buf = seg.Array;
|
||||||
|
int len = seg.Count;
|
||||||
|
long offset = Enqueue(0, len);
|
||||||
|
_mStream.Position = offset;
|
||||||
|
_mStream.Write(buf, seg.Offset, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The push and pop semantics are for historical reasons and not resemblence to normal definitions
|
||||||
|
/// </summary>
|
||||||
|
public MemoryStream PopMemoryStream()
|
||||||
|
{
|
||||||
|
var item = Pop();
|
||||||
|
var buf = new byte[item.Length];
|
||||||
|
_mStream.Position = item.Index;
|
||||||
|
_mStream.Read(buf, 0, item.Length);
|
||||||
|
var ret = new MemoryStream(buf, 0, item.Length, false, true);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long Enqueue(int timestamp, int amount)
|
||||||
|
{
|
||||||
|
_mSize += amount;
|
||||||
|
|
||||||
|
if (_mHead == null)
|
||||||
|
{
|
||||||
|
_mTail = _mHead = _mBookmarks.AddFirst(new ListItem(timestamp, 0, amount));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long target = _mHead.Value.EndExclusive + amount;
|
||||||
|
if (_mTail != null && target <= _mTail.Value.Index)
|
||||||
|
{
|
||||||
|
// theres room to add a new head before the tail
|
||||||
|
_mHead = _mBookmarks.AddAfter(_mHead, new ListItem(timestamp, _mHead.Value.EndExclusive, amount));
|
||||||
|
goto CLEANUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybe the tail is earlier than the head
|
||||||
|
if (_mTail.Value.Index < _mHead.Value.Index)
|
||||||
|
{
|
||||||
|
if (target <= _mCapacity)
|
||||||
|
{
|
||||||
|
// theres room to add a new head before the end of capacity
|
||||||
|
_mHead = _mBookmarks.AddAfter(_mHead, new ListItem(timestamp, _mHead.Value.EndExclusive, amount));
|
||||||
|
goto CLEANUP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// nope, tail is after head. we'll have to clobber from the tail
|
||||||
|
_mHead = _mBookmarks.AddAfter(_mHead, new ListItem(timestamp, _mHead.Value.EndExclusive, amount));
|
||||||
|
goto CLEANUP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no room before the tail, or before capacity. head needs to wrap around.
|
||||||
|
_mHead = _mBookmarks.AddAfter(_mHead, new ListItem(timestamp, 0, amount));
|
||||||
|
|
||||||
|
CLEANUP:
|
||||||
|
// while the head impinges on tail items, discard them
|
||||||
|
for (; ; )
|
||||||
|
{
|
||||||
|
if (_mTail == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_mHead.Value.EndExclusive > _mTail.Value.Index && _mHead.Value.Index <= _mTail.Value.Index)
|
||||||
|
{
|
||||||
|
LinkedListNode<ListItem> nextTail = _mTail.Next;
|
||||||
|
_mSize -= _mTail.Value.Length;
|
||||||
|
_mBookmarks.Remove(_mTail);
|
||||||
|
_mTail = nextTail;
|
||||||
|
}
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _mHead.Value.Index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListItem Pop()
|
||||||
|
{
|
||||||
|
if (_mHead == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Attempted to pop from an empty data structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = _mHead.Value;
|
||||||
|
_mSize -= ret.Length;
|
||||||
|
LinkedListNode<ListItem> nextHead = _mHead.Previous;
|
||||||
|
_mBookmarks.Remove(_mHead);
|
||||||
|
if (_mHead == _mTail)
|
||||||
|
{
|
||||||
|
_mTail = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mHead = nextHead;
|
||||||
|
|
||||||
|
if (_mHead == null)
|
||||||
|
{
|
||||||
|
_mHead = _mBookmarks.Last;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ListItem Dequeue()
|
||||||
|
{
|
||||||
|
if (_mTail == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Attempted to dequeue from an empty data structure");
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret = _mTail.Value;
|
||||||
|
_mSize -= ret.Length;
|
||||||
|
LinkedListNode<ListItem> nextTail = _mTail.Next;
|
||||||
|
_mBookmarks.Remove(_mTail);
|
||||||
|
if (_mTail == _mHead)
|
||||||
|
{
|
||||||
|
_mHead = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mTail = nextTail;
|
||||||
|
if (_mTail == null)
|
||||||
|
{
|
||||||
|
_mTail = _mBookmarks.First;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
//-------- tests ---------
|
||||||
|
public void AssertMonotonic()
|
||||||
|
{
|
||||||
|
if (_mTail == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ts = _mTail.Value.Timestamp;
|
||||||
|
LinkedListNode<ListItem> curr = _mTail;
|
||||||
|
for (; ; )
|
||||||
|
{
|
||||||
|
if (curr == null)
|
||||||
|
{
|
||||||
|
curr = _mBookmarks.First;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Diagnostics.Debug.Assert(curr.Value.Timestamp >= ts);
|
||||||
|
if (curr == _mHead)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ts = curr.Value.Timestamp;
|
||||||
|
curr = curr.Next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ListItem
|
||||||
|
{
|
||||||
|
public ListItem(int timestamp, long index, int length)
|
||||||
|
{
|
||||||
|
Timestamp = timestamp;
|
||||||
|
Index = index;
|
||||||
|
Length = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Timestamp { get; private set; }
|
||||||
|
public long Index { get; private set; }
|
||||||
|
public int Length { get; private set; }
|
||||||
|
|
||||||
|
public long EndExclusive
|
||||||
|
{
|
||||||
|
get { return Index + Length; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue