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