First pass at implementing state save/replay using an iterator-based approach.

Saving a state and later rewinding it now works, but still TODO is unwinding
(ie, replay in the future vs. in the past).

For now, debugging info remains, and some methods are commented out.  This
code is mostly for Thomas to inspect and make sure we're on the right track.
This commit is contained in:
Stephen Anthony 2017-11-11 22:15:28 -03:30
parent c18469c2a3
commit bfc7574cd7
3 changed files with 156 additions and 79 deletions

View File

@ -22,27 +22,30 @@
#include "bspf.hxx"
/**
* A fixed-size object-pool based doubly-linked list that makes use of
* multiple STL lists, to reduce frequent (de)allocations.
*
* This structure can be used as either a stack or queue, but also allows
* for removal at any location in the list.
*
* There are two internal lists; one stores active nodes, and the other
* stores pool nodes that have been 'deleted' from the active list (note
* that no actual deletion takes place; nodes are simply moved from one list
* to another). Similarly, when a new node is added to the active list, it
* is simply moved from the pool list to the active list.
*
* NOTE: For efficiency reasons, none of the methods check for overflow or
* underflow; that is the responsibility of the caller.
*
* In the case of methods which wrap the C++ 'splice()' method, the
* semantics of splice are followed wrt invalid/out-of-range/etc
* iterators. See the applicable documentation for such behaviour.
*
* @author Stephen Anthony
*/
A fixed-size object-pool based doubly-linked list that makes use of
multiple STL lists, to reduce frequent (de)allocations.
This structure can be used as either a stack or queue, but also allows
for removal at any location in the list.
There are two internal lists; one stores active nodes, and the other
stores pool nodes that have been 'deleted' from the active list (note
that no actual deletion takes place; nodes are simply moved from one list
to another). Similarly, when a new node is added to the active list, it
is simply moved from the pool list to the active list.
In all cases, the variable 'myCurrent' is updated to point to the
current node.
NOTE: You must always call 'currentIsValid()' before calling 'current()',
to make sure that the return value is a valid reference.
In the case of methods which wrap the C++ 'splice()' method, the
semantics of splice are followed wrt invalid/out-of-range/etc
iterators. See the applicable documentation for such behaviour.
@author Stephen Anthony
*/
namespace Common {
template <class T, uInt32 CAPACITY = 100>
@ -53,88 +56,140 @@ class LinkedObjectPool
using const_iter = typename std::list<T>::const_iterator;
/*
* Create a pool of size CAPACITY; the active list starts out empty.
*/
LinkedObjectPool<T, CAPACITY>() {
Create a pool of size CAPACITY; the active list starts out empty.
*/
LinkedObjectPool<T, CAPACITY>() : myCurrent(myList.end()) {
for(uInt32 i = 0; i < CAPACITY; ++i)
myPool.emplace_back(T());
}
/**
* Return a reference to the element at the first node in the active list.
*/
Return node data that the 'current' iterator points to.
Note that this returns a valid value only in the case where the list
is non-empty (at least one node has been added to the active list).
Make sure to call 'currentIsValid()' before accessing this method.
*/
T& current() { return *myCurrent; }
/**
Does the 'current' iterator point to a valid node in the active list?
This must be called before 'current()' is called.
*/
bool currentIsValid() { return myCurrent != myList.end(); }
/**
Advance 'current' iterator to previous position in the active list.
If we go past the beginning, it is reset to one past the end (indicates nullptr).
*/
void moveToPrevious() {
if(currentIsValid())
myCurrent = myCurrent == myList.begin() ? myList.end() : std::prev(myCurrent, 1);
}
/**
Advance 'current' iterator to next position in the active list.
If we go past the last node, it will point to one past the end (indicates nullptr).
*/
void moveToNext() {
if(currentIsValid())
myCurrent = std::next(myCurrent, 1);
}
#if 0
/**
Return a reference to the element at the first node in the active list.
*/
T& first() { return myList.front(); }
/**
* Return a reference to the element at the last node in the active list.
*/
Return a reference to the element at the last node in the active list.
*/
T& last() { return myList.back(); }
#endif
/**
* Add a new node at beginning of the active list, and return a reference
* to that nodes' data. The reference may then be modified; ie, you're
* able to change the data located at that node.
*/
T& addFirst() {
Add a new node at the beginning of the active list, and update 'current'
to point to that node.
*/
void addFirst() {
myList.splice(myList.begin(), myPool, myPool.begin());
return myList.front();
myCurrent = myList.begin();
}
/**
* Add a new node at the end of the active list, and return a reference
* to that nodes' data. The reference may then be modified; ie, you're
* able to change the data located at that node.
*/
T& addLast() {
Add a new node at the end of the active list, and update 'current'
to point to that node.
*/
void addLast() {
myList.splice(myList.end(), myPool, myPool.begin());
return myList.back();
myCurrent = std::prev(myList.end(), 1);
}
/**
* Remove the first node of the active list.
*/
Remove the first node of the active list, updating 'current' if it
happens to be the one removed.
*/
void removeFirst() {
myPool.splice(myPool.end(), myList, myList.begin());
const_iter i = myList.begin();
myPool.splice(myPool.end(), myList, i);
if(myCurrent == i) // did we just invalidate 'current'
moveToNext(); // if so, move to the next node
}
/**
* Remove the last node of the active list.
*/
Remove the last node of the active list, updating 'current' if it
happens to be the one removed.
*/
void removeLast() {
myPool.splice(myPool.end(), myList, std::prev(myList.end(), 1));
const_iter i = std::prev(myList.end(), 1);
myPool.splice(myPool.end(), myList, i);
if(myCurrent == i) // did we just invalidate 'current'
moveToPrevious(); // if so, move to the previous node
}
#if 0
/**
* Convenience method to remove a single element from the active list at
* position of the iterator +- the offset.
*/
Convenience method to remove a single element from the active list at
position of the iterator +- the offset.
*/
void remove(const_iter i, Int32 offset = 0) {
myPool.splice(myPool.end(), myList,
offset >= 0 ? std::next(i, offset) : std::prev(i, -offset));
}
/**
* Convenience method to remove a single element from the active list by
* index, offset from the beginning of the list. (ie, '0' means first
* element, '1' is second, and so on).
*/
Convenience method to remove a single element from the active list by
index, offset from the beginning of the list. (ie, '0' means first
element, '1' is second, and so on).
*/
void remove(uInt32 index) {
myPool.splice(myPool.end(), myList, std::next(myList.begin(), index));
}
#endif
/**
* Convenience method to remove a range of elements from 'index' to the
* end of the active list.
*/
void removeLast(uInt32 index) {
myPool.splice(myPool.end(), myList, std::next(myList.begin(), index), myList.end());
Remove range of elements from the beginning of the active list to
the 'current' node.
*/
void removeToFirst() {
myPool.splice(myPool.end(), myList, myList.begin(), myCurrent);
}
/**
* Erase entire contents of active list.
*/
Remove range of elements from the node after 'current' to the end of the
active list.
*/
void removeToLast() {
myPool.splice(myPool.end(), myList, std::next(myCurrent, 1), myList.end());
}
/**
Erase entire contents of active list.
*/
void clear() {
myPool.splice(myPool.end(), myList, myList.begin(), myList.end());
myCurrent = myList.end();
}
/** Access the list with iterators, just as you would a normal C++ STL list */
@ -150,6 +205,9 @@ class LinkedObjectPool
private:
std::list<T> myList, myPool;
// Current position in the active list (end() indicates an invalid position)
iter myCurrent;
private:
// Following constructors and assignment operators not supported
LinkedObjectPool(const LinkedObjectPool&) = delete;

View File

@ -24,26 +24,29 @@
#include "RewindManager.hxx"
static int count = 1;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
RewindManager::RewindManager(OSystem& system, StateManager& statemgr)
: myOSystem(system),
myStateManager(statemgr),
myIsNTSC(true) // TODO
// TODO: current is not valid
{
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool RewindManager::addState(const string& message)
{
// TODO: remove following (preceding???) (all invalid) states
// myStateList.removeToFront(); from current() + 1 (or - 1???)
// Remove all future states
myStateList.removeToLast();
// Make sure we never run out of space
if(myStateList.full())
compressStates();
RewindState& state = myStateList.addFirst();
// TODO: addFirst() must set current() to the just added element
// Add new state at the end of the list (queue adds at end)
// This updates the 'current' iterator inside the list
myStateList.addLast();
RewindState& state = myStateList.current();
Serializer& s = state.data;
s.reset(); // rewind Serializer internal buffers
@ -51,6 +54,8 @@ bool RewindManager::addState(const string& message)
{
state.message = message;
state.cycle = myOSystem.console().tia().cycles();
state.count = count++;
cerr << "add " << state.count << endl;
return true;
}
return false;
@ -59,14 +64,12 @@ bool RewindManager::addState(const string& message)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool RewindManager::rewindState()
{
if(!myStateList.empty())
if(myStateList.currentIsValid())
{
// TODO: get state previous to the current state instead of first()
// RewindState& state = myStateList.current();
// myStateList.prev(); // moves current to the previous (older) element
RewindState& state = myStateList.first(); // TOOD: remove
RewindState& state = myStateList.current();
Serializer& s = state.data;
string message = getMessage(state);
cerr << "rewind " << state.count << endl;
s.reset(); // rewind Serializer internal buffers
myStateManager.loadState(s);
@ -75,8 +78,9 @@ bool RewindManager::rewindState()
// Show message indicating the rewind state
myOSystem.frameBuffer().showMessage(message);
// TODO: Do NOT remove state (TODO later somewhere else: stop emulation)
myStateList.removeFirst(); // TODO: delete this
// Set internal current iterator to previous state (back in time),
// since we've now processed this state
myStateList.moveToPrevious();
return true;
}
@ -87,6 +91,7 @@ bool RewindManager::rewindState()
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool RewindManager::unwindState()
{
#if 0
if(!atFirst()) // or last???
{
// TODO: get state next to the current state
@ -101,14 +106,15 @@ bool RewindManager::unwindState()
// Show message indicating the rewind state
myOSystem.frameBuffer().showMessage(message);*/
return true;
}
}
#endif
return false;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void RewindManager::compressStates()
{
myStateList.removeLast(); // remove the oldest state file
myStateList.removeFirst(); // remove the oldest state file
// TODO: add smart state removal
}
@ -122,7 +128,7 @@ string RewindManager::getMessage(RewindState& state)
diffUnit;
stringstream message;
string unit;
freq = NTSC_FREQ; // TODO: remove
message << (diff >= 0 ? "Rewind" : "Unwind");
diff = abs(diff);

View File

@ -25,7 +25,17 @@ class StateManager;
#include "bspf.hxx"
/**
This class is used to save (and later 'rewind') system save states.
This class is used to save (and later 'replay') system save states.
In this implementation, we assume states are added at the end of the list.
Rewinding involves moving the internal iterator backwards in time (towards
the beginning of the list).
Unwinding involves moving the internal iterator forwards in time (towards
the end of the list).
Any time a new state is added, the internal iterator moves back to the
insertion point of the data (the end of the list).
@author Stephen Anthony
*/
@ -37,9 +47,9 @@ class RewindManager
public:
/**
Add a new state file with the given message; this message will be
displayed when the state is rewound.
displayed when the state is replayed.
@param message Message to display when rewinding to this state
@param message Message to display when replaying this state
*/
bool addState(const string& message);
@ -70,12 +80,16 @@ class RewindManager
Serializer data;
string message;
uInt64 cycle;
int count; // TODO - remove this
// We do nothing on object instantiation or copy
// The goal of LinkedObjectPool is to not do any allocations at all
RewindState() { }
RewindState(const RewindState&) { }
};
// The linked-list to store states (internally it takes care of reducing
// frequent (de)-allocations)
Common::LinkedObjectPool<RewindState, MAX_SIZE> myStateList;
bool myIsNTSC;
@ -84,7 +98,6 @@ class RewindManager
string getMessage(RewindState& state);
private:
// Following constructors and assignment operators not supported
RewindManager() = delete;