diff --git a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj
index 48e47e47a1..1716601466 100644
--- a/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj
+++ b/BizHawk.Client.EmuHawk/BizHawk.Client.EmuHawk.csproj
@@ -432,7 +432,9 @@
-
+
+
+
Form
diff --git a/BizHawk.Client.EmuHawk/MainForm.cs b/BizHawk.Client.EmuHawk/MainForm.cs
index 5fc7423621..653b16c2cd 100644
--- a/BizHawk.Client.EmuHawk/MainForm.cs
+++ b/BizHawk.Client.EmuHawk/MainForm.cs
@@ -2473,20 +2473,16 @@ namespace BizHawk.Client.EmuHawk
{
GlobalWin.Rewinder.Rewind(1);
suppressCaptureRewind = true;
- if (0 == GlobalWin.Rewinder.RewindBuf.Count)
- {
- runFrame = false;
- }
- else
- {
- runFrame = true;
- }
+
+ runFrame = !(GlobalWin.Rewinder.Count == 0);
+
//we don't want to capture input when rewinding, even in record mode
if (Global.MovieSession.Movie.IsRecording)
{
Global.MovieSession.Movie.SwitchToPlay();
}
}
+
if (UpdateFrame)
{
runFrame = true;
diff --git a/BizHawk.Client.EmuHawk/Rewind.cs b/BizHawk.Client.EmuHawk/Rewind.cs
deleted file mode 100644
index 1e658d131b..0000000000
--- a/BizHawk.Client.EmuHawk/Rewind.cs
+++ /dev/null
@@ -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; } }
- ///
- /// 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
- ///
- public class StreamBlobDatabase : IDisposable
- {
- public void Dispose()
- {
- mStream.Dispose();
- mStream = null;
- if (mAllocatedBuffer != null)
- mBufferManage(mAllocatedBuffer, 0, false);
- }
-
- Func mBufferManage;
- byte[] mAllocatedBuffer;
-
- public StreamBlobDatabase(bool onDisk, long capacity, Func 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 mBookmarks = new LinkedList();
- LinkedListNode mHead, mTail;
- long mCapacity, mSize;
-
- ///
- /// Returns the amount of the buffer that's used
- ///
- public long Size { get { return mSize; } }
-
- ///
- /// 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.
- ///
- public float FullnessRatio { get { return (float)((double)Size / (double)mCapacity); } }
-
- ///
- /// the number of frames stored here
- ///
- public int Count { get { return mBookmarks.Count; } }
-
- ///
- /// The underlying stream to
- ///
- public Stream Stream { get { return mStream; } }
-
- public void Clear()
- {
- mHead = mTail = null;
- mSize = 0;
- mBookmarks.Clear();
- }
-
- ///
- /// The push and pop semantics are for historical reasons and not resemblence to normal definitions
- ///
- public void Push(ArraySegment seg)
- {
- var buf = seg.Array;
- int len = seg.Count;
- long offset = Enqueue(0, len);
- mStream.Position = offset;
- mStream.Write(buf, seg.Offset, len);
- }
-
- ///
- /// The push and pop semantics are for historical reasons and not resemblence to normal definitions
- ///
- 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 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 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 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 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 Jobs = new ConcurrentQueue();
- }
-
- // 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(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;
- }
- }
-}
diff --git a/BizHawk.Client.EmuHawk/config/RewindConfig.cs b/BizHawk.Client.EmuHawk/config/RewindConfig.cs
index 0b3c64540e..f69750301d 100644
--- a/BizHawk.Client.EmuHawk/config/RewindConfig.cs
+++ b/BizHawk.Client.EmuHawk/config/RewindConfig.cs
@@ -19,10 +19,10 @@ namespace BizHawk.Client.EmuHawk
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) + "%";
- RewindFramesUsedLabel.Text = GlobalWin.Rewinder.Rewind_Count.ToString();
+ FullnessLabel.Text = String.Format("{0:0.00}", GlobalWin.Rewinder.FullnessRatio * 100) + "%";
+ RewindFramesUsedLabel.Text = GlobalWin.Rewinder.Count.ToString();
}
else
{
@@ -260,9 +260,9 @@ namespace BizHawk.Client.EmuHawk
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
{
diff --git a/BizHawk.Client.EmuHawk/rewind/RewindThreader.cs b/BizHawk.Client.EmuHawk/rewind/RewindThreader.cs
new file mode 100644
index 0000000000..edfeb2c0ac
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/rewind/RewindThreader.cs
@@ -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 Jobs = new ConcurrentQueue();
+ 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;
+ }
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/rewind/Rewinder.cs b/BizHawk.Client.EmuHawk/rewind/Rewinder.cs
new file mode 100644
index 0000000000..084512735b
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/rewind/Rewinder.cs
@@ -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(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));
+ }
+ }
+ }
+}
diff --git a/BizHawk.Client.EmuHawk/rewind/StreamBlobDatabase.cs b/BizHawk.Client.EmuHawk/rewind/StreamBlobDatabase.cs
new file mode 100644
index 0000000000..c083a9e29c
--- /dev/null
+++ b/BizHawk.Client.EmuHawk/rewind/StreamBlobDatabase.cs
@@ -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
+{
+ ///
+ /// Manages a ring buffer of storage which can continually chow its own tail to keep growing forward.
+ /// Probably only useful for the rewind buffer
+ ///
+ public class StreamBlobDatabase : IDisposable
+ {
+ private Func _mBufferManage;
+ private byte[] _mAllocatedBuffer;
+ private Stream _mStream;
+ private LinkedList _mBookmarks = new LinkedList();
+ private LinkedListNode _mHead, _mTail;
+ private long _mCapacity, _mSize;
+
+ public StreamBlobDatabase(bool onDisk, long capacity, Func 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);
+ }
+ }
+
+ ///
+ /// Returns the amount of the buffer that's used
+ ///
+ public long Size { get { return _mSize; } }
+
+ ///
+ /// 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.
+ ///
+ public float FullnessRatio { get { return (float)((double)Size / (double)_mCapacity); } }
+
+ ///
+ /// the number of frames stored here
+ ///
+ public int Count { get { return _mBookmarks.Count; } }
+
+ ///
+ /// The underlying stream to
+ ///
+ 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();
+ }
+
+ ///
+ /// The push and pop semantics are for historical reasons and not resemblence to normal definitions
+ ///
+ public void Push(ArraySegment seg)
+ {
+ var buf = seg.Array;
+ int len = seg.Count;
+ long offset = Enqueue(0, len);
+ _mStream.Position = offset;
+ _mStream.Write(buf, seg.Offset, len);
+ }
+
+ ///
+ /// The push and pop semantics are for historical reasons and not resemblence to normal definitions
+ ///
+ 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 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 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 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 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; }
+ }
+ }
+ }
+}