2013-07-27 21:53:47 +00:00
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Collections.Generic;
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
|
|
|
|
namespace BizHawk.MultiClient
|
|
|
|
|
{
|
2011-06-12 00:14:19 +00:00
|
|
|
|
public partial class MainForm
|
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
//adelikat: change the way this is constructed to control whether its on disk or in memory
|
2013-07-27 21:54:41 +00:00
|
|
|
|
private readonly StreamBlobDatabase RewindBuf = new StreamBlobDatabase(true,128*1024*1024);
|
2013-04-16 00:19:31 +00:00
|
|
|
|
private byte[] LastState;
|
|
|
|
|
private bool RewindImpossible;
|
2013-07-20 14:38:09 +00:00
|
|
|
|
private int RewindFrequency = 1;
|
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
/// <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>
|
|
|
|
|
class StreamBlobDatabase : IDisposable
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2011-09-19 00:39:28 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
public class ListItem
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
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; } }
|
2011-06-12 00:14:19 +00:00
|
|
|
|
}
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
Stream mStream;
|
|
|
|
|
LinkedList<ListItem> mBookmarks = new LinkedList<ListItem>();
|
|
|
|
|
LinkedListNode<ListItem> mHead, mTail;
|
|
|
|
|
long mCapacity;
|
|
|
|
|
|
|
|
|
|
public int Count { get { return mBookmarks.Count; } }
|
|
|
|
|
public Stream Stream { get { return mStream; } }
|
|
|
|
|
|
|
|
|
|
public void Clear()
|
2013-07-20 14:38:09 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
mHead = mTail = null;
|
|
|
|
|
mBookmarks.Clear();
|
2013-07-20 14:38:09 +00:00
|
|
|
|
}
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The push and pop semantics are for historical reasons and not resemblence
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void PushMemoryStream(MemoryStream ms)
|
|
|
|
|
{
|
|
|
|
|
var buf = ms.ToArray();
|
|
|
|
|
long offset = Enqueue(0, buf.Length);
|
|
|
|
|
mStream.Position = offset;
|
|
|
|
|
mStream.Write(buf, 0, buf.Length);
|
|
|
|
|
}
|
2013-07-21 20:39:11 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
public MemoryStream PopMemoryStream()
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
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)
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
mTail = mHead = mBookmarks.AddFirst(new ListItem(timestamp, 0, amount));
|
|
|
|
|
return 0;
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
long target = mHead.Value.endExclusive + amount;
|
|
|
|
|
if (mTail != null && target <= mTail.Value.index)
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
//theres room to add a new head before the tail
|
|
|
|
|
mHead = mBookmarks.AddAfter(mHead, new ListItem(timestamp, mHead.Value.endExclusive, amount));
|
|
|
|
|
goto CLEANUP;
|
|
|
|
|
}
|
2013-07-21 20:39:11 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
//maybe the tail is earlier than the head
|
|
|
|
|
if (mTail.Value.index < mHead.Value.index)
|
|
|
|
|
{
|
|
|
|
|
if (target <= mCapacity)
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
//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;
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
//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;
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
//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 (; ; )
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
if (mTail == null) break;
|
|
|
|
|
if (mHead.Value.endExclusive > mTail.Value.index && mHead.Value.index <= mTail.Value.index)
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
LinkedListNode<ListItem> nextTail = mTail.Next;
|
|
|
|
|
mBookmarks.Remove(mTail);
|
|
|
|
|
mTail = nextTail;
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
2013-07-27 21:53:47 +00:00
|
|
|
|
else break;
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
2013-07-27 21:53:47 +00:00
|
|
|
|
|
|
|
|
|
return mHead.Value.index;
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
2013-07-27 21:53:47 +00:00
|
|
|
|
|
|
|
|
|
public ListItem Pop()
|
|
|
|
|
{
|
|
|
|
|
if (mHead == null) throw new InvalidOperationException("Attempted to pop from an empty data structure");
|
|
|
|
|
var ret = mHead.Value;
|
|
|
|
|
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()
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
if (mTail == null) throw new InvalidOperationException("Attempted to dequeue from an empty data structure");
|
|
|
|
|
var ret = mTail.Value;
|
|
|
|
|
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 (; ; )
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
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;
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
2013-07-27 21:53:47 +00:00
|
|
|
|
}
|
2013-07-21 20:39:11 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
void Test()
|
|
|
|
|
{
|
|
|
|
|
var sbb = new StreamBlobDatabase(false);
|
|
|
|
|
var rand = new Random(0);
|
|
|
|
|
int timestamp = 0;
|
|
|
|
|
for (; ; )
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
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)
|
2013-07-21 20:39:11 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
while (sbb.Count != 0)
|
|
|
|
|
{
|
|
|
|
|
Console.WriteLine("ZAM!!!");
|
|
|
|
|
sbb.Dequeue();
|
|
|
|
|
}
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
2013-07-27 21:53:47 +00:00
|
|
|
|
sbb.AssertMonotonic();
|
|
|
|
|
timestamp++;
|
|
|
|
|
Console.WriteLine("{0}, {1}", test, sbb.Count);
|
2013-07-21 20:39:11 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
private void CaptureRewindState()
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
if (RewindImpossible)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (LastState == null)
|
2012-10-10 15:04:13 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
DoRewindSettings();
|
2012-10-10 15:04:13 +00:00
|
|
|
|
}
|
2013-07-27 21:53:47 +00:00
|
|
|
|
|
|
|
|
|
// Otherwise, it's not the first frame, so build a delta.
|
|
|
|
|
if (LastState != null && Global.Emulator.Frame % RewindFrequency == 0)
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
if (LastState.Length <= 0x10000)
|
|
|
|
|
CaptureRewindState64K();
|
|
|
|
|
else
|
|
|
|
|
CaptureRewindStateLarge();
|
|
|
|
|
}
|
|
|
|
|
}
|
2012-10-10 15:04:13 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
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);
|
2012-10-10 15:04:13 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
RewindActive = enabled;
|
|
|
|
|
RewindFrequency = frequency;
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
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)
|
|
|
|
|
SetRewindParams(Global.Config.RewindEnabledLarge,Global.Config.RewindFrequencyLarge);
|
|
|
|
|
else if (LastState.Length > 32768)
|
|
|
|
|
SetRewindParams(Global.Config.RewindEnabledMedium,Global.Config.RewindFrequencyMedium);
|
|
|
|
|
else
|
|
|
|
|
SetRewindParams(Global.Config.RewindEnabledSmall, Global.Config.RewindFrequencySmall);
|
2011-06-12 00:14:19 +00:00
|
|
|
|
}
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
2011-09-19 00:39:28 +00:00
|
|
|
|
// Builds a delta for states that are > 64K in size.
|
2013-07-27 21:53:47 +00:00
|
|
|
|
void CaptureRewindStateLarge() { CaptureRewindStateDelta(false); }
|
|
|
|
|
// Builds a delta for states that are <= 64K in size.
|
|
|
|
|
void CaptureRewindState64K() { CaptureRewindStateDelta(true); }
|
|
|
|
|
|
|
|
|
|
void CaptureRewindStateDelta(bool isSmall)
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
|
|
|
|
byte[] CurrentState = Global.Emulator.SaveStateBinary();
|
|
|
|
|
int beginChangeSequence = -1;
|
|
|
|
|
bool inChangeSequence = false;
|
|
|
|
|
var ms = new MemoryStream();
|
|
|
|
|
var writer = new BinaryWriter(ms);
|
2012-10-10 15:04:13 +00:00
|
|
|
|
if (CurrentState.Length != LastState.Length)
|
|
|
|
|
{
|
|
|
|
|
writer.Write(true); // full state
|
2012-10-11 20:05:02 +00:00
|
|
|
|
writer.Write(LastState);
|
2012-10-10 15:04:13 +00:00
|
|
|
|
}
|
|
|
|
|
else
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
2012-10-10 15:04:13 +00:00
|
|
|
|
writer.Write(false); // delta state
|
|
|
|
|
for (int i = 0; i < CurrentState.Length; i++)
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
2012-10-10 15:04:13 +00:00
|
|
|
|
if (inChangeSequence == false)
|
|
|
|
|
{
|
|
|
|
|
if (i >= LastState.Length)
|
|
|
|
|
continue;
|
|
|
|
|
if (CurrentState[i] == LastState[i])
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
inChangeSequence = true;
|
|
|
|
|
beginChangeSequence = i;
|
2011-03-01 09:32:12 +00:00
|
|
|
|
continue;
|
2012-10-10 15:04:13 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (i - beginChangeSequence == 254 || i == CurrentState.Length - 1)
|
|
|
|
|
{
|
|
|
|
|
writer.Write((byte)(i - beginChangeSequence + 1));
|
2013-07-27 21:53:47 +00:00
|
|
|
|
if(isSmall) writer.Write((ushort)beginChangeSequence);
|
|
|
|
|
else writer.Write(beginChangeSequence);
|
2012-10-10 15:04:13 +00:00
|
|
|
|
writer.Write(LastState, beginChangeSequence, i - beginChangeSequence + 1);
|
|
|
|
|
inChangeSequence = false;
|
2011-06-12 00:14:19 +00:00
|
|
|
|
continue;
|
2012-10-10 15:04:13 +00:00
|
|
|
|
}
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
2012-10-10 15:04:13 +00:00
|
|
|
|
if (CurrentState[i] == LastState[i])
|
|
|
|
|
{
|
|
|
|
|
writer.Write((byte)(i - beginChangeSequence));
|
2013-07-27 21:53:47 +00:00
|
|
|
|
writer.Write((ushort)beginChangeSequence);
|
2012-10-10 15:04:13 +00:00
|
|
|
|
writer.Write(LastState, beginChangeSequence, i - beginChangeSequence);
|
|
|
|
|
inChangeSequence = false;
|
|
|
|
|
}
|
2011-06-12 00:14:19 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
LastState = CurrentState;
|
|
|
|
|
ms.Position = 0;
|
2013-07-27 21:53:47 +00:00
|
|
|
|
RewindBuf.PushMemoryStream(ms);
|
2011-06-12 00:14:19 +00:00
|
|
|
|
}
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
2013-07-27 21:53:47 +00:00
|
|
|
|
void RewindLarge() { RewindDelta(false); }
|
|
|
|
|
void Rewind64K() { RewindDelta(true); }
|
|
|
|
|
void RewindDelta(bool isSmall)
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
2013-07-27 21:53:47 +00:00
|
|
|
|
var ms = RewindBuf.PopMemoryStream();
|
2011-06-12 00:14:19 +00:00
|
|
|
|
var reader = new BinaryReader(ms);
|
2012-10-10 15:04:13 +00:00
|
|
|
|
bool fullstate = reader.ReadBoolean();
|
|
|
|
|
if (fullstate)
|
|
|
|
|
{
|
|
|
|
|
Global.Emulator.LoadStateBinary(reader);
|
|
|
|
|
}
|
|
|
|
|
else
|
2011-06-12 00:14:19 +00:00
|
|
|
|
{
|
2012-10-10 15:04:13 +00:00
|
|
|
|
var output = new MemoryStream(LastState);
|
|
|
|
|
while (ms.Position < ms.Length - 1)
|
|
|
|
|
{
|
|
|
|
|
byte len = reader.ReadByte();
|
2013-07-27 21:53:47 +00:00
|
|
|
|
int offset;
|
|
|
|
|
if(isSmall)
|
|
|
|
|
offset = reader.ReadUInt16();
|
|
|
|
|
else offset = reader.ReadInt32();
|
2012-10-10 15:04:13 +00:00
|
|
|
|
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));
|
2011-06-12 00:14:19 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
2011-06-12 00:14:19 +00:00
|
|
|
|
public void Rewind(int frames)
|
|
|
|
|
{
|
|
|
|
|
for (int i = 0; i < frames; i++)
|
|
|
|
|
{
|
2013-04-16 00:19:31 +00:00
|
|
|
|
if (RewindBuf.Count == 0 || (Global.MovieSession.Movie.Loaded && 0 == Global.MovieSession.Movie.Frames))
|
2011-06-12 00:14:19 +00:00
|
|
|
|
return;
|
2013-07-27 21:53:47 +00:00
|
|
|
|
|
2011-06-12 00:14:19 +00:00
|
|
|
|
if (LastState.Length < 0x10000)
|
|
|
|
|
Rewind64K();
|
|
|
|
|
else
|
|
|
|
|
RewindLarge();
|
|
|
|
|
}
|
|
|
|
|
}
|
2011-01-11 02:55:51 +00:00
|
|
|
|
|
2011-06-12 00:14:19 +00:00
|
|
|
|
public void ResetRewindBuffer()
|
|
|
|
|
{
|
|
|
|
|
RewindBuf.Clear();
|
2013-04-16 00:19:31 +00:00
|
|
|
|
RewindImpossible = false;
|
2011-06-12 00:14:19 +00:00
|
|
|
|
LastState = null;
|
|
|
|
|
}
|
2012-05-28 00:44:27 +00:00
|
|
|
|
|
2013-04-16 00:19:31 +00:00
|
|
|
|
public int RewindBufferCount()
|
|
|
|
|
{
|
|
|
|
|
return RewindBuf.Count;
|
|
|
|
|
}
|
2011-06-12 00:14:19 +00:00
|
|
|
|
}
|
2011-01-11 02:55:51 +00:00
|
|
|
|
}
|