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-28 20:33:26 +00:00
private StreamBlobDatabase RewindBuf = new StreamBlobDatabase ( Global . Config . Rewind_OnDisk , Global . Config . Rewind_BufferSize * ( long ) 1024 * ( long ) 1024 ) ;
2013-07-27 23:13:27 +00:00
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 23:13:27 +00:00
private bool RewindDeltaEnable = false ;
2013-07-20 14:38:09 +00:00
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 ;
}
2013-07-28 20:33:26 +00:00
public StreamBlobDatabase ( bool onDisk , long capacity )
2013-07-27 21:53:47 +00:00
{
mCapacity = capacity ;
if ( onDisk )
{
2013-07-27 22:02:43 +00:00
var path = Path . Combine ( System . IO . Path . GetTempPath ( ) , "bizhawk.rewindbuf-pid" + System . Diagnostics . Process . GetCurrentProcess ( ) . Id + "-" + Guid . NewGuid ( ) . ToString ( ) ) ;
2013-07-27 21:53:47 +00:00
//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.
2013-07-27 22:39:12 +00:00
//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 ) ;
2013-07-27 21:53:47 +00:00
}
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 ;
2013-07-27 22:39:12 +00:00
long mCapacity , mSize ;
2013-07-27 21:53:47 +00:00
2013-07-27 22:39:12 +00:00
/// <summary>
/// Returns the amount of the buffer that's used
/// </summary>
public long Size { get { return mSize ; } }
/// <summary>
/// Gets the current fullness ratio (Size/Capacity). Note that this wont reach 100% due to the buffer size not being a multiple of a fixed savestate size.
/// </summary>
public float FullnessRatio { get { return ( float ) ( ( double ) Size / ( double ) mCapacity ) ; } }
/// <summary>
/// the number of frames stored here
/// </summary>
2013-07-27 21:53:47 +00:00
public int Count { get { return mBookmarks . Count ; } }
2013-07-27 22:39:12 +00:00
/// <summary>
/// The underlying stream to
/// </summary>
2013-07-27 21:53:47 +00:00
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 ;
2013-07-27 22:39:12 +00:00
mSize = 0 ;
2013-07-27 21:53:47 +00:00
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>
2013-07-27 23:02:26 +00:00
/// The push and pop semantics are for historical reasons and not resemblence to normal definitions
2013-07-27 21:53:47 +00:00
/// </summary>
2013-07-27 23:02:26 +00:00
public void Push ( ArraySegment < byte > seg )
2013-07-27 21:53:47 +00:00
{
2013-07-27 23:02:26 +00:00
var buf = seg . Array ;
int len = seg . Count ;
2013-07-27 22:43:08 +00:00
long offset = Enqueue ( 0 , len ) ;
2013-07-27 21:53:47 +00:00
mStream . Position = offset ;
2013-07-27 23:02:26 +00:00
mStream . Write ( buf , seg . Offset , len ) ;
2013-07-27 21:53:47 +00:00
}
2013-07-21 20:39:11 +00:00
2013-07-27 23:02:26 +00:00
/// <summary>
/// The push and pop semantics are for historical reasons and not resemblence to normal definitions
/// </summary>
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 )
{
2013-07-27 22:39:12 +00:00
mSize + = amount ;
2013-07-27 21:53:47 +00:00
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 ;
2013-07-27 22:39:12 +00:00
mSize - = mTail . Value . length ;
2013-07-27 21:53:47 +00:00
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 ;
2013-07-27 22:39:12 +00:00
mSize - = ret . length ;
2013-07-27 21:53:47 +00:00
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 ;
2013-07-27 22:39:12 +00:00
mSize - = ret . length ;
2013-07-27 21:53:47 +00:00
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 ( )
{
2013-07-28 20:33:26 +00:00
var sbb = new StreamBlobDatabase ( false , Global . Config . Rewind_BufferSize * 1024 * 1024 ) ;
2013-07-27 21:53:47 +00:00
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
2013-07-27 23:13:27 +00:00
//log a frame
2013-07-27 21:53:47 +00:00
if ( LastState ! = null & & Global . Emulator . Frame % RewindFrequency = = 0 )
2011-06-12 00:14:19 +00:00
{
2013-07-27 23:13:27 +00:00
if ( RewindDeltaEnable )
{
if ( LastState . Length < = 0x10000 )
CaptureRewindStateDelta ( true ) ;
else
CaptureRewindStateDelta ( false ) ;
}
else CaptureRewindStateNonDelta ( ) ;
2013-07-27 21:53:47 +00:00
}
}
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 ( )
{
2013-07-28 20:33:26 +00:00
long cap = Global . Config . Rewind_BufferSize * ( long ) 1024 * ( long ) 1024 ;
RewindBuf = new StreamBlobDatabase ( Global . Config . Rewind_OnDisk , cap ) ;
2013-07-27 21:53:47 +00:00
// This is the first frame. Capture the state, and put it in LastState for future deltas to be compared against.
LastState = Global . Emulator . SaveStateBinary ( ) ;
2013-07-28 19:09:52 +00:00
if ( LastState . Length > = Global . Config . Rewind_LargeStateSize )
{
SetRewindParams ( Global . Config . RewindEnabledLarge , Global . Config . RewindFrequencyLarge ) ;
}
else if ( LastState . Length > = Global . Config . RewindFrequencyMedium )
{
SetRewindParams ( Global . Config . RewindEnabledMedium , Global . Config . RewindFrequencyMedium ) ;
}
2013-07-27 21:53:47 +00:00
else
2013-07-28 19:09:52 +00:00
{
2013-07-27 21:53:47 +00:00
SetRewindParams ( Global . Config . RewindEnabledSmall , Global . Config . RewindFrequencySmall ) ;
2013-07-28 19:09:52 +00:00
}
2013-07-27 23:13:27 +00:00
2013-07-28 19:09:52 +00:00
RewindDeltaEnable = Global . Config . Rewind_UseDelta ;
2011-06-12 00:14:19 +00:00
}
2011-01-11 02:55:51 +00:00
2013-07-27 23:13:27 +00:00
void CaptureRewindStateNonDelta ( )
{
byte [ ] CurrentState = Global . Emulator . SaveStateBinary ( ) ;
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 ) ;
}
2013-07-27 21:53:47 +00:00
2013-07-27 23:02:26 +00:00
byte [ ] TempBuf = new byte [ 0 ] ;
2013-07-27 21:53:47 +00:00
void CaptureRewindStateDelta ( bool isSmall )
2011-06-12 00:14:19 +00:00
{
byte [ ] CurrentState = Global . Emulator . SaveStateBinary ( ) ;
2013-07-27 23:13:27 +00:00
//in case the state sizes mismatch, capture a full state rather than trying to do anything clever
if ( CurrentState . Length ! = LastState . Length )
{
CaptureRewindStateNonDelta ( ) ;
return ;
}
2011-06-12 00:14:19 +00:00
int beginChangeSequence = - 1 ;
bool inChangeSequence = false ;
2013-07-27 23:02:26 +00:00
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
2011-06-12 00:14:19 +00:00
{
2013-07-27 23:02:26 +00:00
var writer = new BinaryWriter ( ms ) ;
2013-07-27 23:13:27 +00:00
writer . Write ( false ) ; // delta state
for ( int i = 0 ; i < CurrentState . Length ; i + + )
2013-07-27 23:02:26 +00:00
{
2013-07-27 23:13:27 +00:00
if ( inChangeSequence = = false )
2012-10-10 15:04:13 +00:00
{
2013-07-27 23:13:27 +00:00
if ( i > = LastState . Length )
2013-07-27 23:02:26 +00:00
continue ;
2013-07-27 23:13:27 +00:00
if ( CurrentState [ i ] = = LastState [ i ] )
2013-07-27 23:02:26 +00:00
continue ;
2011-01-11 02:55:51 +00:00
2013-07-27 23:13:27 +00:00
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 ;
2012-10-10 15:04:13 +00:00
}
2011-06-12 00:14:19 +00:00
}
}
2013-07-27 23:02:26 +00:00
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 ;
}
2011-06-12 00:14:19 +00:00
LastState = CurrentState ;
2013-07-27 23:02:26 +00:00
var seg = new ArraySegment < byte > ( TempBuf , 0 , ( int ) ms . Position ) ;
RewindBuf . Push ( seg ) ;
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
}