diff --git a/BizHawk.MultiClient/Rewind.cs b/BizHawk.MultiClient/Rewind.cs index 0eabccdfd6..9436b569c9 100644 --- a/BizHawk.MultiClient/Rewind.cs +++ b/BizHawk.MultiClient/Rewind.cs @@ -1,14 +1,216 @@ -using System.IO; +using System; +using System.IO; +using System.Collections.Generic; namespace BizHawk.MultiClient { public partial class MainForm { - private readonly MruStack RewindBuf = new MruStack(15000); + //adelikat: change the way this is constructed to control whether its on disk or in memory + private readonly StreamBlobDatabase RewindBuf = new StreamBlobDatabase(true,64*1024); private byte[] LastState; private bool RewindImpossible; private int RewindFrequency = 1; + /// + /// 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 + /// + class StreamBlobDatabase : IDisposable + { + public void Dispose() + { + mStream.Dispose(); + mStream = null; + } + + public StreamBlobDatabase(bool onDisk, long capacity = 256*1024*1024) + { + mCapacity = capacity; + if (onDisk) + { + var path = Path.GetTempFileName() + ".biz.rewindbuf"; + + //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. + mStream = new FileStream(path, FileMode.Create, System.Security.AccessControl.FileSystemRights.FullControl, FileShare.None, 512, FileOptions.DeleteOnClose); + } + else + { + var buffer = new byte[capacity]; + mStream = new MemoryStream(buffer); + } + } + + 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; + + public int Count { get { return mBookmarks.Count; } } + public Stream Stream { get { return mStream; } } + + public void Clear() + { + mHead = mTail = null; + mBookmarks.Clear(); + } + + /// + /// The push and pop semantics are for historical reasons and not resemblence + /// + public void PushMemoryStream(MemoryStream ms) + { + var buf = ms.ToArray(); + long offset = Enqueue(0, buf.Length); + mStream.Position = offset; + mStream.Write(buf, 0, buf.Length); + } + + 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) + { + 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; + 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; + 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; + 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); + 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); + } + } + } private void CaptureRewindState() { @@ -30,135 +232,39 @@ namespace BizHawk.MultiClient } } + void SetRewindParams(bool enabled, int frequency) + { + if(RewindActive != enabled) + Global.OSD.AddMessage("Rewind " + (enabled ? "Enabled" : "Disabled")); + if (RewindFrequency != frequency) + Global.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 = Global.Emulator.SaveStateBinary(); if (LastState.Length > 0x100000) - { - if (RewindActive != Global.Config.RewindEnabledLarge) - { - Global.OSD.AddMessage("Rewind " + (Global.Config.RewindEnabledLarge ? "Enabled" : "Disabled")); - } - - if (Global.Config.RewindEnabledLarge) - { - RewindActive = true; - - if (RewindFrequency != Global.Config.RewindFrequencyLarge) - { - Global.OSD.AddMessage("Rewind frequency set to " + Global.Config.RewindFrequencyLarge.ToString()); - } - RewindFrequency = Global.Config.RewindFrequencyLarge; - } - else - { - RewindActive = false; - LastState = null; - } - } + SetRewindParams(Global.Config.RewindEnabledLarge,Global.Config.RewindFrequencyLarge); else if (LastState.Length > 32768) - { - if (RewindActive != Global.Config.RewindEnabledMedium) - { - Global.OSD.AddMessage("Rewind " + (Global.Config.RewindEnabledMedium ? "Enabled" : "Disabled")); - } - - if (Global.Config.RewindEnabledMedium) - { - RewindActive = true; - if (RewindFrequency != Global.Config.RewindFrequencyMedium) - { - Global.OSD.AddMessage("Rewind frequency set to " + Global.Config.RewindFrequencyMedium.ToString()); - } - RewindFrequency = Global.Config.RewindFrequencyMedium; - } - else - { - RewindActive = false; - LastState = null; - } - } + SetRewindParams(Global.Config.RewindEnabledMedium,Global.Config.RewindFrequencyMedium); else - { - if (RewindActive != Global.Config.RewindEnabledSmall) - { - Global.OSD.AddMessage("Rewind " + (Global.Config.RewindEnabledSmall ? "Enabled" : "Disabled")); - } - - if (Global.Config.RewindEnabledSmall) - { - RewindActive = true; - if (RewindFrequency != Global.Config.RewindFrequencySmall) - { - Global.OSD.AddMessage("Rewind frequency set to " + Global.Config.RewindFrequencySmall.ToString()); - } - RewindFrequency = Global.Config.RewindFrequencySmall; - } - else - { - RewindActive = false; - LastState = null; - } - } - } - - // Builds a delta for states that are <= 64K in size. - void CaptureRewindState64K() - { - byte[] CurrentState = Global.Emulator.SaveStateBinary(); - int beginChangeSequence = -1; - bool inChangeSequence = false; - var ms = new MemoryStream(); - var writer = new BinaryWriter(ms); - if (CurrentState.Length != LastState.Length) - { - writer.Write(true); // full state - writer.Write(LastState); - } - else - { - 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)); - writer.Write((ushort)beginChangeSequence); - writer.Write(LastState, beginChangeSequence, i - beginChangeSequence + 1); - inChangeSequence = false; - continue; - } - - if (CurrentState[i] == LastState[i]) - { - writer.Write((byte)(i - beginChangeSequence)); - writer.Write((ushort)beginChangeSequence); - writer.Write(LastState, beginChangeSequence, i - beginChangeSequence); - inChangeSequence = false; - } - } - } - LastState = CurrentState; - ms.Position = 0; - RewindBuf.Push(ms); + SetRewindParams(Global.Config.RewindEnabledSmall, Global.Config.RewindFrequencySmall); } // Builds a delta for states that are > 64K in size. - void CaptureRewindStateLarge() + void CaptureRewindStateLarge() { CaptureRewindStateDelta(false); } + // Builds a delta for states that are <= 64K in size. + void CaptureRewindState64K() { CaptureRewindStateDelta(true); } + + void CaptureRewindStateDelta(bool isSmall) { byte[] CurrentState = Global.Emulator.SaveStateBinary(); int beginChangeSequence = -1; @@ -190,7 +296,8 @@ namespace BizHawk.MultiClient if (i - beginChangeSequence == 254 || i == CurrentState.Length - 1) { writer.Write((byte)(i - beginChangeSequence + 1)); - writer.Write(beginChangeSequence); + if(isSmall) writer.Write((ushort)beginChangeSequence); + else writer.Write(beginChangeSequence); writer.Write(LastState, beginChangeSequence, i - beginChangeSequence + 1); inChangeSequence = false; continue; @@ -199,7 +306,7 @@ namespace BizHawk.MultiClient if (CurrentState[i] == LastState[i]) { writer.Write((byte)(i - beginChangeSequence)); - writer.Write(beginChangeSequence); + writer.Write((ushort)beginChangeSequence); writer.Write(LastState, beginChangeSequence, i - beginChangeSequence); inChangeSequence = false; } @@ -207,12 +314,14 @@ namespace BizHawk.MultiClient } LastState = CurrentState; ms.Position = 0; - RewindBuf.Push(ms); + RewindBuf.PushMemoryStream(ms); } - void Rewind64K() + void RewindLarge() { RewindDelta(false); } + void Rewind64K() { RewindDelta(true); } + void RewindDelta(bool isSmall) { - var ms = RewindBuf.Pop(); + var ms = RewindBuf.PopMemoryStream(); var reader = new BinaryReader(ms); bool fullstate = reader.ReadBoolean(); if (fullstate) @@ -225,33 +334,10 @@ namespace BizHawk.MultiClient while (ms.Position < ms.Length - 1) { byte len = reader.ReadByte(); - ushort offset = reader.ReadUInt16(); - 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)); - } - } - - void RewindLarge() - { - var ms = RewindBuf.Pop(); - 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 = reader.ReadInt32(); + 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; @@ -267,17 +353,12 @@ namespace BizHawk.MultiClient for (int i = 0; i < frames; i++) { if (RewindBuf.Count == 0 || (Global.MovieSession.Movie.Loaded && 0 == Global.MovieSession.Movie.Frames)) - { return; - } + if (LastState.Length < 0x10000) - { Rewind64K(); - } else - { RewindLarge(); - } } }