CoreTiming: Data structure cleanup
Replace adhoc linked list with a priority heap. Performance characteristics are mostly the same, but is more cache friendly. [Priority Queues have O(log n) push/pop compared to the linked list's O(n) push/O(1) pop but the queue is not big enough for that to matter, so linear is faster over linked. Very slight gains when framelimit is unlimited (Wind Waker), 1900% -> 1950%]
This commit is contained in:
parent
b88b188819
commit
17c34ae0b1
|
@ -51,12 +51,6 @@
|
||||||
#error No version of is_trivially_copyable
|
#error No version of is_trivially_copyable
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
template <class T>
|
|
||||||
struct LinkedListItem : public T
|
|
||||||
{
|
|
||||||
LinkedListItem<T>* next;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Wrapper class
|
// Wrapper class
|
||||||
class PointerWrap
|
class PointerWrap
|
||||||
{
|
{
|
||||||
|
@ -244,67 +238,6 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's pretend std::list doesn't exist!
|
|
||||||
template <class T, LinkedListItem<T>* (*TNew)(), void (*TFree)(LinkedListItem<T>*),
|
|
||||||
void (*TDo)(PointerWrap&, T*)>
|
|
||||||
void DoLinkedList(LinkedListItem<T>*& list_start, LinkedListItem<T>** list_end = 0)
|
|
||||||
{
|
|
||||||
LinkedListItem<T>* list_cur = list_start;
|
|
||||||
LinkedListItem<T>* prev = nullptr;
|
|
||||||
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
u8 shouldExist = !!list_cur;
|
|
||||||
Do(shouldExist);
|
|
||||||
if (shouldExist == 1)
|
|
||||||
{
|
|
||||||
LinkedListItem<T>* cur = list_cur ? list_cur : TNew();
|
|
||||||
TDo(*this, (T*)cur);
|
|
||||||
if (!list_cur)
|
|
||||||
{
|
|
||||||
if (mode == MODE_READ)
|
|
||||||
{
|
|
||||||
cur->next = nullptr;
|
|
||||||
list_cur = cur;
|
|
||||||
if (prev)
|
|
||||||
prev->next = cur;
|
|
||||||
else
|
|
||||||
list_start = cur;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TFree(cur);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (mode == MODE_READ)
|
|
||||||
{
|
|
||||||
if (prev)
|
|
||||||
prev->next = nullptr;
|
|
||||||
if (list_end)
|
|
||||||
*list_end = prev;
|
|
||||||
if (list_cur)
|
|
||||||
{
|
|
||||||
if (list_start == list_cur)
|
|
||||||
list_start = nullptr;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
LinkedListItem<T>* next = list_cur->next;
|
|
||||||
TFree(list_cur);
|
|
||||||
list_cur = next;
|
|
||||||
} while (list_cur);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
prev = list_cur;
|
|
||||||
list_cur = list_cur->next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DoMarker(const std::string& prevName, u32 arbitraryNumber = 0x42)
|
void DoMarker(const std::string& prevName, u32 arbitraryNumber = 0x42)
|
||||||
{
|
{
|
||||||
u32 cookie = arbitraryNumber;
|
u32 cookie = arbitraryNumber;
|
||||||
|
@ -319,16 +252,22 @@ public:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename T, typename Functor>
|
||||||
|
void DoEachElement(T& container, Functor member)
|
||||||
|
{
|
||||||
|
u32 size = static_cast<u32>(container.size());
|
||||||
|
Do(size);
|
||||||
|
container.resize(size);
|
||||||
|
|
||||||
|
for (auto& elem : container)
|
||||||
|
member(*this, elem);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void DoContainer(T& x)
|
void DoContainer(T& x)
|
||||||
{
|
{
|
||||||
u32 size = (u32)x.size();
|
DoEachElement(x, [](PointerWrap& p, typename T::value_type& elem) { p.Do(elem); });
|
||||||
Do(size);
|
|
||||||
x.resize(size);
|
|
||||||
|
|
||||||
for (auto& elem : x)
|
|
||||||
Do(elem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__forceinline void DoVoid(void* data, u32 size)
|
__forceinline void DoVoid(void* data, u32 size)
|
||||||
|
|
|
@ -8,8 +8,10 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Common/Assert.h"
|
||||||
#include "Common/ChunkFile.h"
|
#include "Common/ChunkFile.h"
|
||||||
#include "Common/FifoQueue.h"
|
#include "Common/FifoQueue.h"
|
||||||
|
#include "Common/Logging/Log.h"
|
||||||
#include "Common/StringUtil.h"
|
#include "Common/StringUtil.h"
|
||||||
#include "Common/Thread.h"
|
#include "Common/Thread.h"
|
||||||
|
|
||||||
|
@ -31,22 +33,30 @@ struct EventType
|
||||||
|
|
||||||
static std::vector<EventType> s_event_types;
|
static std::vector<EventType> s_event_types;
|
||||||
|
|
||||||
struct BaseEvent
|
struct Event
|
||||||
{
|
{
|
||||||
s64 time;
|
s64 time;
|
||||||
u64 userdata;
|
u64 userdata;
|
||||||
int type;
|
int type;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef LinkedListItem<BaseEvent> Event;
|
constexpr bool operator>(const Event& left, const Event& right)
|
||||||
|
{
|
||||||
|
return left.time > right.time;
|
||||||
|
}
|
||||||
|
constexpr bool operator<(const Event& left, const Event& right)
|
||||||
|
{
|
||||||
|
return left.time < right.time;
|
||||||
|
}
|
||||||
|
|
||||||
// STATE_TO_SAVE
|
// STATE_TO_SAVE
|
||||||
static Event* s_first_event;
|
// The queue is a min-heap using std::make_heap/push_heap/pop_heap.
|
||||||
|
// We don't use std::priority_queue because we need to be able to serialize, unserialize and
|
||||||
|
// erase arbitrary events (RemoveEvent()) regardless of the queue order. These aren't accomodated
|
||||||
|
// by the standard adaptor class.
|
||||||
|
static std::vector<Event> s_event_queue;
|
||||||
static std::mutex s_ts_write_lock;
|
static std::mutex s_ts_write_lock;
|
||||||
static Common::FifoQueue<BaseEvent, false> s_ts_queue;
|
static Common::FifoQueue<Event, false> s_ts_queue;
|
||||||
|
|
||||||
// event pools
|
|
||||||
static Event* s_event_pool = nullptr;
|
|
||||||
|
|
||||||
static float s_last_OC_factor;
|
static float s_last_OC_factor;
|
||||||
float g_last_OC_factor_inverted;
|
float g_last_OC_factor_inverted;
|
||||||
|
@ -66,22 +76,6 @@ u64 g_fake_TB_start_ticks;
|
||||||
|
|
||||||
static int s_ev_lost;
|
static int s_ev_lost;
|
||||||
|
|
||||||
static Event* GetNewEvent()
|
|
||||||
{
|
|
||||||
if (!s_event_pool)
|
|
||||||
return new Event;
|
|
||||||
|
|
||||||
Event* ev = s_event_pool;
|
|
||||||
s_event_pool = ev->next;
|
|
||||||
return ev;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void FreeEvent(Event* ev)
|
|
||||||
{
|
|
||||||
ev->next = s_event_pool;
|
|
||||||
s_event_pool = ev;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void EmptyTimedCallback(u64 userdata, s64 cyclesLate)
|
static void EmptyTimedCallback(u64 userdata, s64 cyclesLate)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -132,9 +126,9 @@ int RegisterEvent(const std::string& name, TimedCallback callback)
|
||||||
|
|
||||||
void UnregisterAllEvents()
|
void UnregisterAllEvents()
|
||||||
{
|
{
|
||||||
if (s_first_event)
|
_assert_msg_(POWERPC, s_event_queue.empty(), "Cannot unregister events with events pending");
|
||||||
PanicAlert("Cannot unregister events with events pending");
|
|
||||||
s_event_types.clear();
|
s_event_types.clear();
|
||||||
|
s_event_types.shrink_to_fit();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Init()
|
void Init()
|
||||||
|
@ -156,50 +150,6 @@ void Shutdown()
|
||||||
MoveEvents();
|
MoveEvents();
|
||||||
ClearPendingEvents();
|
ClearPendingEvents();
|
||||||
UnregisterAllEvents();
|
UnregisterAllEvents();
|
||||||
|
|
||||||
while (s_event_pool)
|
|
||||||
{
|
|
||||||
Event* ev = s_event_pool;
|
|
||||||
s_event_pool = ev->next;
|
|
||||||
delete ev;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void EventDoState(PointerWrap& p, BaseEvent* ev)
|
|
||||||
{
|
|
||||||
p.Do(ev->time);
|
|
||||||
|
|
||||||
// this is why we can't have (nice things) pointers as userdata
|
|
||||||
p.Do(ev->userdata);
|
|
||||||
|
|
||||||
// we can't savestate ev->type directly because events might not get registered in the same order
|
|
||||||
// (or at all) every time.
|
|
||||||
// so, we savestate the event's type's name, and derive ev->type from that when loading.
|
|
||||||
std::string name;
|
|
||||||
if (p.GetMode() != PointerWrap::MODE_READ)
|
|
||||||
name = s_event_types[ev->type].name;
|
|
||||||
|
|
||||||
p.Do(name);
|
|
||||||
if (p.GetMode() == PointerWrap::MODE_READ)
|
|
||||||
{
|
|
||||||
bool foundMatch = false;
|
|
||||||
for (unsigned int i = 0; i < s_event_types.size(); ++i)
|
|
||||||
{
|
|
||||||
if (name == s_event_types[i].name)
|
|
||||||
{
|
|
||||||
ev->type = i;
|
|
||||||
foundMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundMatch)
|
|
||||||
{
|
|
||||||
WARN_LOG(POWERPC,
|
|
||||||
"Lost event from savestate because its type, \"%s\", has not been registered.",
|
|
||||||
name.c_str());
|
|
||||||
ev->type = s_ev_lost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DoState(PointerWrap& p)
|
void DoState(PointerWrap& p)
|
||||||
|
@ -218,9 +168,44 @@ void DoState(PointerWrap& p)
|
||||||
p.DoMarker("CoreTimingData");
|
p.DoMarker("CoreTimingData");
|
||||||
|
|
||||||
MoveEvents();
|
MoveEvents();
|
||||||
|
p.DoEachElement(s_event_queue, [](PointerWrap& pw, Event& ev) {
|
||||||
|
pw.Do(ev.time);
|
||||||
|
|
||||||
p.DoLinkedList<BaseEvent, GetNewEvent, FreeEvent, EventDoState>(s_first_event);
|
// this is why we can't have (nice things) pointers as userdata
|
||||||
|
pw.Do(ev.userdata);
|
||||||
|
|
||||||
|
// we can't savestate ev.type directly because events might not get registered in the same
|
||||||
|
// order (or at all) every time.
|
||||||
|
// so, we savestate the event's type's name, and derive ev.type from that when loading.
|
||||||
|
std::string name;
|
||||||
|
if (pw.GetMode() != PointerWrap::MODE_READ)
|
||||||
|
name = s_event_types[ev.type].name;
|
||||||
|
|
||||||
|
pw.Do(name);
|
||||||
|
if (pw.GetMode() == PointerWrap::MODE_READ)
|
||||||
|
{
|
||||||
|
auto itr = std::find_if(s_event_types.begin(), s_event_types.end(),
|
||||||
|
[&](const EventType& evt) { return evt.name == name; });
|
||||||
|
if (itr != s_event_types.end())
|
||||||
|
{
|
||||||
|
ev.type = static_cast<int>(itr - s_event_types.begin());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WARN_LOG(POWERPC,
|
||||||
|
"Lost event from savestate because its type, \"%s\", has not been registered.",
|
||||||
|
name.c_str());
|
||||||
|
ev.type = s_ev_lost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
p.DoMarker("CoreTimingEvents");
|
p.DoMarker("CoreTimingEvents");
|
||||||
|
|
||||||
|
// When loading from a save state, we must assume the Event order is random and meaningless.
|
||||||
|
// The exact layout of the heap in memory is implementation defined, therefore it is platform
|
||||||
|
// and library version specific.
|
||||||
|
if (p.GetMode() == PointerWrap::MODE_READ)
|
||||||
|
std::make_heap(s_event_queue.begin(), s_event_queue.end(), std::greater<Event>());
|
||||||
}
|
}
|
||||||
|
|
||||||
// This should only be called from the CPU thread. If you are calling
|
// This should only be called from the CPU thread. If you are calling
|
||||||
|
@ -243,30 +228,7 @@ u64 GetIdleTicks()
|
||||||
|
|
||||||
void ClearPendingEvents()
|
void ClearPendingEvents()
|
||||||
{
|
{
|
||||||
while (s_first_event)
|
s_event_queue.clear();
|
||||||
{
|
|
||||||
Event* e = s_first_event->next;
|
|
||||||
FreeEvent(s_first_event);
|
|
||||||
s_first_event = e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void AddEventToQueue(Event* ne)
|
|
||||||
{
|
|
||||||
Event* prev = nullptr;
|
|
||||||
Event** pNext = &s_first_event;
|
|
||||||
for (;;)
|
|
||||||
{
|
|
||||||
Event*& next = *pNext;
|
|
||||||
if (!next || ne->time < next->time)
|
|
||||||
{
|
|
||||||
ne->next = next;
|
|
||||||
next = ne;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
prev = next;
|
|
||||||
pNext = &prev->next;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScheduleEvent(s64 cycles_into_future, int event_type, u64 userdata, FromThread from)
|
void ScheduleEvent(s64 cycles_into_future, int event_type, u64 userdata, FromThread from)
|
||||||
|
@ -285,16 +247,14 @@ void ScheduleEvent(s64 cycles_into_future, int event_type, u64 userdata, FromThr
|
||||||
|
|
||||||
if (from_cpu_thread)
|
if (from_cpu_thread)
|
||||||
{
|
{
|
||||||
Event* ne = GetNewEvent();
|
s64 timeout = GetTicks() + cycles_into_future;
|
||||||
ne->time = GetTicks() + cycles_into_future;
|
|
||||||
ne->userdata = userdata;
|
|
||||||
ne->type = event_type;
|
|
||||||
|
|
||||||
// If this event needs to be scheduled before the next advance(), force one early
|
// If this event needs to be scheduled before the next advance(), force one early
|
||||||
if (!s_is_global_timer_sane)
|
if (!s_is_global_timer_sane)
|
||||||
ForceExceptionCheck(cycles_into_future);
|
ForceExceptionCheck(cycles_into_future);
|
||||||
|
|
||||||
AddEventToQueue(ne);
|
s_event_queue.emplace_back(Event{timeout, userdata, event_type});
|
||||||
|
std::push_heap(s_event_queue.begin(), s_event_queue.end(), std::greater<Event>());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -306,41 +266,20 @@ void ScheduleEvent(s64 cycles_into_future, int event_type, u64 userdata, FromThr
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lk(s_ts_write_lock);
|
std::lock_guard<std::mutex> lk(s_ts_write_lock);
|
||||||
Event ne;
|
s_ts_queue.Push(Event{g_global_timer + cycles_into_future, userdata, event_type});
|
||||||
ne.time = g_global_timer + cycles_into_future;
|
|
||||||
ne.type = event_type;
|
|
||||||
ne.userdata = userdata;
|
|
||||||
s_ts_queue.Push(ne);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RemoveEvent(int event_type)
|
void RemoveEvent(int event_type)
|
||||||
{
|
{
|
||||||
while (s_first_event && s_first_event->type == event_type)
|
auto itr = std::remove_if(s_event_queue.begin(), s_event_queue.end(),
|
||||||
{
|
[&](const Event& e) { return e.type == event_type; });
|
||||||
Event* next = s_first_event->next;
|
|
||||||
FreeEvent(s_first_event);
|
|
||||||
s_first_event = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!s_first_event)
|
// Removing random items breaks the invariant so we have to re-establish it.
|
||||||
return;
|
if (itr != s_event_queue.end())
|
||||||
|
|
||||||
Event* prev = s_first_event;
|
|
||||||
Event* ptr = prev->next;
|
|
||||||
while (ptr)
|
|
||||||
{
|
{
|
||||||
if (ptr->type == event_type)
|
s_event_queue.erase(itr, s_event_queue.end());
|
||||||
{
|
std::make_heap(s_event_queue.begin(), s_event_queue.end(), std::greater<Event>());
|
||||||
prev->next = ptr->next;
|
|
||||||
FreeEvent(ptr);
|
|
||||||
ptr = prev->next;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
prev = ptr;
|
|
||||||
ptr = ptr->next;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,14 +302,10 @@ void ForceExceptionCheck(s64 cycles)
|
||||||
|
|
||||||
void MoveEvents()
|
void MoveEvents()
|
||||||
{
|
{
|
||||||
BaseEvent sevt;
|
for (Event ev; s_ts_queue.Pop(ev);)
|
||||||
while (s_ts_queue.Pop(sevt))
|
|
||||||
{
|
{
|
||||||
Event* evt = GetNewEvent();
|
s_event_queue.emplace_back(std::move(ev));
|
||||||
evt->time = sevt.time;
|
std::push_heap(s_event_queue.begin(), s_event_queue.end(), std::greater<Event>());
|
||||||
evt->userdata = sevt.userdata;
|
|
||||||
evt->type = sevt.type;
|
|
||||||
AddEventToQueue(evt);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,24 +321,23 @@ void Advance()
|
||||||
|
|
||||||
s_is_global_timer_sane = true;
|
s_is_global_timer_sane = true;
|
||||||
|
|
||||||
while (s_first_event && s_first_event->time <= g_global_timer)
|
while (!s_event_queue.empty() && s_event_queue.front().time <= g_global_timer)
|
||||||
{
|
{
|
||||||
// LOG(POWERPC, "[Scheduler] %s (%lld, %lld) ",
|
Event evt = std::move(s_event_queue.front());
|
||||||
// s_event_types[s_first_event->type].name ? s_event_types[s_first_event->type].name
|
std::pop_heap(s_event_queue.begin(), s_event_queue.end(), std::greater<Event>());
|
||||||
// : "?",
|
s_event_queue.pop_back();
|
||||||
// (u64)g_global_timer, (u64)s_first_event->time);
|
// NOTICE_LOG(POWERPC, "[Scheduler] %-20s (%lld, %lld)", evt.type->name->c_str(),
|
||||||
Event* evt = s_first_event;
|
// g_global_timer, evt.time);
|
||||||
s_first_event = s_first_event->next;
|
s_event_types[evt.type].callback(evt.userdata, g_global_timer - evt.time);
|
||||||
s_event_types[evt->type].callback(evt->userdata, g_global_timer - evt->time);
|
|
||||||
FreeEvent(evt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s_is_global_timer_sane = false;
|
s_is_global_timer_sane = false;
|
||||||
|
|
||||||
if (s_first_event)
|
// Still events left (scheduled in the future)
|
||||||
|
if (!s_event_queue.empty())
|
||||||
{
|
{
|
||||||
g_slice_length =
|
g_slice_length = static_cast<int>(
|
||||||
static_cast<int>(std::min<s64>(s_first_event->time - g_global_timer, MAX_SLICE_LENGTH));
|
std::min<s64>(s_event_queue.front().time - g_global_timer, MAX_SLICE_LENGTH));
|
||||||
}
|
}
|
||||||
|
|
||||||
PowerPC::ppcState.downcount = CyclesToDowncount(g_slice_length);
|
PowerPC::ppcState.downcount = CyclesToDowncount(g_slice_length);
|
||||||
|
@ -417,12 +351,14 @@ void Advance()
|
||||||
|
|
||||||
void LogPendingEvents()
|
void LogPendingEvents()
|
||||||
{
|
{
|
||||||
Event* ptr = s_first_event;
|
auto clone = s_event_queue;
|
||||||
while (ptr)
|
std::sort(clone.begin(), clone.end());
|
||||||
|
for (const Event& ev : clone)
|
||||||
{
|
{
|
||||||
INFO_LOG(POWERPC, "PENDING: Now: %" PRId64 " Pending: %" PRId64 " Type: %d", g_global_timer,
|
INFO_LOG(POWERPC, "PENDING: Now: %" PRId64 " Pending: %" PRId64 " Type: %s (%d)",
|
||||||
ptr->time, ptr->type);
|
g_global_timer, ev.time,
|
||||||
ptr = ptr->next;
|
ev.type < s_event_types.size() ? s_event_types[ev.type].name.c_str() : "<INVALID>",
|
||||||
|
ev.type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -442,20 +378,24 @@ void Idle()
|
||||||
|
|
||||||
std::string GetScheduledEventsSummary()
|
std::string GetScheduledEventsSummary()
|
||||||
{
|
{
|
||||||
Event* ptr = s_first_event;
|
|
||||||
std::string text = "Scheduled events\n";
|
std::string text = "Scheduled events\n";
|
||||||
text.reserve(1000);
|
text.reserve(1000);
|
||||||
while (ptr)
|
|
||||||
|
auto clone = s_event_queue;
|
||||||
|
std::sort(clone.begin(), clone.end());
|
||||||
|
for (const Event& ev : clone)
|
||||||
{
|
{
|
||||||
unsigned int t = ptr->type;
|
unsigned int t = ev.type;
|
||||||
if (t >= s_event_types.size())
|
if (t >= s_event_types.size())
|
||||||
|
{
|
||||||
PanicAlertT("Invalid event type %i", t);
|
PanicAlertT("Invalid event type %i", t);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const std::string& name = s_event_types[ptr->type].name;
|
const std::string& name = s_event_types[ev.type].name;
|
||||||
|
|
||||||
text += StringFromFormat("%s : %" PRIi64 " %016" PRIx64 "\n", name.c_str(), ptr->time,
|
text +=
|
||||||
ptr->userdata);
|
StringFromFormat("%s : %" PRIi64 " %016" PRIx64 "\n", name.c_str(), ev.time, ev.userdata);
|
||||||
ptr = ptr->next;
|
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ static Common::Event g_compressAndDumpStateSyncEvent;
|
||||||
static std::thread g_save_thread;
|
static std::thread g_save_thread;
|
||||||
|
|
||||||
// Don't forget to increase this after doing changes on the savestate system
|
// Don't forget to increase this after doing changes on the savestate system
|
||||||
static const u32 STATE_VERSION = 55;
|
static const u32 STATE_VERSION = 56;
|
||||||
|
|
||||||
// Maps savestate versions to Dolphin versions.
|
// Maps savestate versions to Dolphin versions.
|
||||||
// Versions after 42 don't need to be added to this list,
|
// Versions after 42 don't need to be added to this list,
|
||||||
|
|
Loading…
Reference in New Issue