TimingEvents: Switch to 64-bit counters

This commit is contained in:
Stenzek
2024-08-13 23:52:25 +10:00
parent 1e8ca51f44
commit 86d4d92753
9 changed files with 275 additions and 143 deletions

View File

@ -16,11 +16,14 @@ Log_SetChannel(TimingEvents);
namespace TimingEvents {
static GlobalTicks GetTimestampForNewEvent();
static void SortEvent(TimingEvent* event);
static void AddActiveEvent(TimingEvent* event);
static void RemoveActiveEvent(TimingEvent* event);
static void SortEvents();
static TimingEvent* FindActiveEvent(const std::string_view name);
static void CommitGlobalTicks(const GlobalTicks new_global_ticks);
namespace {
struct TimingEventsState
@ -28,11 +31,11 @@ struct TimingEventsState
TimingEvent* active_events_head = nullptr;
TimingEvent* active_events_tail = nullptr;
TimingEvent* current_event = nullptr;
TickCount current_event_new_downcount = 0;
u32 active_event_count = 0;
u32 global_tick_counter = 0;
u32 event_run_tick_counter = 0;
bool frame_done = false;
GlobalTicks current_event_next_run_time = 0;
GlobalTicks global_tick_counter = 0;
GlobalTicks event_run_tick_counter = 0;
};
} // namespace
@ -40,12 +43,19 @@ ALIGN_TO_CACHE_LINE static TimingEventsState s_state;
} // namespace TimingEvents
u32 TimingEvents::GetGlobalTickCounter()
GlobalTicks TimingEvents::GetGlobalTickCounter()
{
return s_state.global_tick_counter;
}
u32 TimingEvents::GetEventRunTickCounter()
GlobalTicks TimingEvents::GetTimestampForNewEvent()
{
// we want to schedule relative to the currently-being processed event, but if we haven't run events in a while, it
// needs to include the pending time. so explicitly add the two.
return s_state.global_tick_counter + CPU::GetPendingTicks();
}
GlobalTicks TimingEvents::GetEventRunTickCounter()
{
return s_state.event_run_tick_counter;
}
@ -58,6 +68,7 @@ void TimingEvents::Initialize()
void TimingEvents::Reset()
{
s_state.global_tick_counter = 0;
s_state.event_run_tick_counter = 0;
}
void TimingEvents::Shutdown()
@ -67,7 +78,9 @@ void TimingEvents::Shutdown()
void TimingEvents::UpdateCPUDowncount()
{
const u32 event_downcount = s_state.active_events_head->GetDowncount();
DebugAssert(s_state.active_events_head->m_next_run_time >= s_state.global_tick_counter);
const u32 event_downcount =
static_cast<u32>(s_state.active_events_head->m_next_run_time - s_state.global_tick_counter);
CPU::g_state.downcount = CPU::HasPendingInterrupt() ? 0 : event_downcount;
}
@ -78,13 +91,13 @@ TimingEvent** TimingEvents::GetHeadEventPtr()
void TimingEvents::SortEvent(TimingEvent* event)
{
const TickCount event_downcount = event->m_downcount;
const GlobalTicks event_runtime = event->m_next_run_time;
if (event->prev && event->prev->m_downcount > event_downcount)
if (event->prev && event->prev->m_next_run_time > event_runtime)
{
// move backwards
TimingEvent* current = event->prev;
while (current && current->m_downcount > event_downcount)
while (current && current->m_next_run_time > event_runtime)
current = current->prev;
// unlink
@ -120,11 +133,11 @@ void TimingEvents::SortEvent(TimingEvent* event)
UpdateCPUDowncount();
}
}
else if (event->next && event_downcount > event->next->m_downcount)
else if (event->next && event_runtime > event->next->m_next_run_time)
{
// move forwards
TimingEvent* current = event->next;
while (current && event_downcount > current->m_downcount)
while (current && event_runtime > current->m_next_run_time)
current = current->next;
// unlink
@ -135,7 +148,8 @@ void TimingEvents::SortEvent(TimingEvent* event)
else
{
s_state.active_events_head = event->next;
UpdateCPUDowncount();
if (!s_state.current_event)
UpdateCPUDowncount();
}
if (event->next)
event->next->prev = event->prev;
@ -155,7 +169,8 @@ void TimingEvents::SortEvent(TimingEvent* event)
else
{
s_state.active_events_head = event;
UpdateCPUDowncount();
if (!s_state.current_event)
UpdateCPUDowncount();
}
current->prev = event;
@ -177,9 +192,10 @@ void TimingEvents::AddActiveEvent(TimingEvent* event)
DebugAssert(!event->prev && !event->next);
s_state.active_event_count++;
const GlobalTicks event_runtime = event->m_next_run_time;
TimingEvent* current = nullptr;
TimingEvent* next = s_state.active_events_head;
while (next && event->m_downcount > next->m_downcount)
while (next && event_runtime > next->m_next_run_time)
{
current = next;
next = next->next;
@ -240,7 +256,7 @@ void TimingEvents::RemoveActiveEvent(TimingEvent* event)
else
{
s_state.active_events_head = event->next;
if (s_state.active_events_head)
if (s_state.active_events_head && !s_state.current_event)
UpdateCPUDowncount();
}
@ -295,60 +311,68 @@ void TimingEvents::SetFrameDone()
CPU::g_state.downcount = 0;
}
void TimingEvents::RunEvents()
void TimingEvents::CancelRunningEvent()
{
DebugAssert(!s_state.current_event);
if (!s_state.current_event)
return;
// Might need to sort it, since we're bailing out.
if (s_state.current_event->IsActive())
SortEvent(s_state.current_event);
s_state.current_event = nullptr;
}
ALWAYS_INLINE_RELEASE void TimingEvents::CommitGlobalTicks(const GlobalTicks new_global_ticks)
{
s_state.event_run_tick_counter = new_global_ticks;
do
{
TickCount pending_ticks = CPU::GetPendingTicks();
if (pending_ticks >= s_state.active_events_head->GetDowncount())
TimingEvent* event = s_state.active_events_head;
s_state.global_tick_counter = std::min(new_global_ticks, event->m_next_run_time);
// Now we can actually run the callbacks.
while (s_state.global_tick_counter >= event->m_next_run_time)
{
s_state.current_event = event;
// Factor late time into the time for the next invocation.
const TickCount ticks_late = static_cast<TickCount>(s_state.global_tick_counter - event->m_next_run_time);
const TickCount ticks_to_execute = static_cast<TickCount>(s_state.global_tick_counter - event->m_last_run_time);
// Why don't we modify event->m_downcount directly? Because otherwise the event list won't be sorted.
// Adding the interval may cause this event to have a greater downcount than the next, and a new event
// may be inserted at the front, despite having a higher downcount than the next.
s_state.current_event_next_run_time = event->m_next_run_time + static_cast<u32>(event->m_interval);
event->m_last_run_time = s_state.global_tick_counter;
// The cycles_late is only an indicator, it doesn't modify the cycles to execute.
event->m_callback(event->m_callback_param, ticks_to_execute, ticks_late);
if (event->m_active)
{
event->m_next_run_time = s_state.current_event_next_run_time;
SortEvent(event);
}
event = s_state.active_events_head;
}
} while (new_global_ticks > s_state.global_tick_counter);
s_state.current_event = nullptr;
}
void TimingEvents::RunEvents()
{
DebugAssert(!s_state.current_event);
DebugAssert(CPU::GetPendingTicks() >= CPU::g_state.downcount);
do
{
const GlobalTicks new_global_ticks =
s_state.event_run_tick_counter + static_cast<GlobalTicks>(CPU::GetPendingTicks());
if (new_global_ticks >= s_state.active_events_head->m_next_run_time)
{
CPU::ResetPendingTicks();
s_state.event_run_tick_counter = s_state.global_tick_counter + static_cast<u32>(pending_ticks);
do
{
const TickCount time = std::min(pending_ticks, s_state.active_events_head->GetDowncount());
s_state.global_tick_counter += static_cast<u32>(time);
pending_ticks -= time;
// Apply downcount to all events.
// This will result in a negative downcount for those events which are late.
for (TimingEvent* event = s_state.active_events_head; event; event = event->next)
{
event->m_downcount -= time;
event->m_time_since_last_run += time;
}
// Now we can actually run the callbacks.
while (s_state.active_events_head->m_downcount <= 0)
{
// move it to the end, since that'll likely be its new position
TimingEvent* event = s_state.active_events_head;
s_state.current_event = event;
// Factor late time into the time for the next invocation.
const TickCount ticks_late = -event->m_downcount;
const TickCount ticks_to_execute = event->m_time_since_last_run;
// Why don't we modify event->m_downcount directly? Because otherwise the event list won't be sorted.
// Adding the interval may cause this event to have a greater downcount than the next, and a new event
// may be inserted at the front, despite having a higher downcount than the next.
s_state.current_event_new_downcount = event->m_downcount + event->m_interval;
event->m_time_since_last_run = 0;
// The cycles_late is only an indicator, it doesn't modify the cycles to execute.
event->m_callback(event->m_callback_param, ticks_to_execute, ticks_late);
if (event->m_active)
{
event->m_downcount = s_state.current_event_new_downcount;
SortEvent(event);
}
}
} while (pending_ticks > 0);
s_state.current_event = nullptr;
CommitGlobalTicks(new_global_ticks);
}
if (s_state.frame_done)
@ -364,12 +388,30 @@ void TimingEvents::RunEvents()
} while (CPU::GetPendingTicks() >= CPU::g_state.downcount);
}
void TimingEvents::CommitLeftoverTicks()
{
#ifdef _DEBUG
if (s_state.event_run_tick_counter > s_state.global_tick_counter)
WARNING_LOG("Late-running {} ticks before execution", s_state.event_run_tick_counter - s_state.global_tick_counter);
#endif
CommitGlobalTicks(s_state.event_run_tick_counter);
if (CPU::HasPendingInterrupt())
CPU::DispatchInterrupt();
UpdateCPUDowncount();
}
bool TimingEvents::DoState(StateWrapper& sw)
{
sw.Do(&s_state.global_tick_counter);
if (sw.IsReading())
if (sw.GetVersion() < 71) [[unlikely]]
{
u32 old_global_tick_counter = 0;
sw.Do(&old_global_tick_counter);
s_state.global_tick_counter = static_cast<GlobalTicks>(old_global_tick_counter);
s_state.event_run_tick_counter = s_state.global_tick_counter;
// Load timestamps for the clock events.
// Any oneshot events should be recreated by the load state method, so we can fix up their times here.
u32 event_count = 0;
@ -394,9 +436,8 @@ bool TimingEvents::DoState(StateWrapper& sw)
continue;
}
// Using reschedule is safe here since we call sort afterwards.
event->m_downcount = downcount;
event->m_time_since_last_run = time_since_last_run;
event->m_next_run_time = s_state.global_tick_counter + static_cast<u32>(downcount);
event->m_last_run_time = s_state.global_tick_counter - static_cast<u32>(time_since_last_run);
event->m_period = period;
event->m_interval = interval;
}
@ -408,23 +449,71 @@ bool TimingEvents::DoState(StateWrapper& sw)
}
DEBUG_LOG("Loaded {} events from save state.", event_count);
// Add pending ticks to the CPU, this'll happen if we saved state when we weren't paused.
const TickCount pending_ticks =
static_cast<TickCount>(s_state.event_run_tick_counter - s_state.global_tick_counter);
DebugAssert(pending_ticks >= 0);
CPU::AddPendingTicks(pending_ticks);
SortEvents();
UpdateCPUDowncount();
}
else
{
sw.Do(&s_state.global_tick_counter);
sw.Do(&s_state.event_run_tick_counter);
sw.Do(&s_state.active_event_count);
for (TimingEvent* event = s_state.active_events_head; event; event = event->next)
if (sw.IsReading())
{
sw.Do(&event->m_name);
sw.Do(&event->m_downcount);
sw.Do(&event->m_time_since_last_run);
sw.Do(&event->m_period);
sw.Do(&event->m_interval);
}
// Load timestamps for the clock events.
// Any oneshot events should be recreated by the load state method, so we can fix up their times here.
u32 event_count = 0;
sw.Do(&event_count);
DEBUG_LOG("Wrote {} events to save state.", s_state.active_event_count);
for (u32 i = 0; i < event_count; i++)
{
TinyString event_name;
GlobalTicks next_run_time, last_run_time;
TickCount period, interval;
sw.Do(&event_name);
sw.Do(&next_run_time);
sw.Do(&last_run_time);
sw.Do(&period);
sw.Do(&interval);
if (sw.HasError())
return false;
TimingEvent* event = FindActiveEvent(event_name);
if (!event)
{
WARNING_LOG("Save state has event '{}', but couldn't find this event when loading.", event_name);
continue;
}
event->m_next_run_time = next_run_time;
event->m_last_run_time = last_run_time;
event->m_period = period;
event->m_interval = interval;
}
DEBUG_LOG("Loaded {} events from save state.", event_count);
SortEvents();
}
else
{
sw.Do(&s_state.active_event_count);
for (TimingEvent* event = s_state.active_events_head; event; event = event->next)
{
sw.Do(&event->m_name);
sw.Do(&event->m_next_run_time);
sw.Do(&event->m_last_run_time);
sw.Do(&event->m_period);
sw.Do(&event->m_interval);
}
DEBUG_LOG("Wrote {} events to save state.", s_state.active_event_count);
}
}
return !sw.HasError();
@ -432,9 +521,11 @@ bool TimingEvents::DoState(StateWrapper& sw)
TimingEvent::TimingEvent(const std::string_view name, TickCount period, TickCount interval,
TimingEventCallback callback, void* callback_param)
: m_callback(callback), m_callback_param(callback_param), m_downcount(interval), m_time_since_last_run(0),
m_period(period), m_interval(interval), m_name(name)
: m_callback(callback), m_callback_param(callback_param), m_period(period), m_interval(interval), m_name(name)
{
const GlobalTicks ts = TimingEvents::GetTimestampForNewEvent();
m_last_run_time = ts;
m_next_run_time = ts + static_cast<u32>(interval);
}
TimingEvent::~TimingEvent()
@ -444,46 +535,51 @@ TimingEvent::~TimingEvent()
TickCount TimingEvent::GetTicksSinceLastExecution() const
{
return CPU::GetPendingTicks() + m_time_since_last_run;
// Can be negative if event A->B invoked B early while in the event loop.
const GlobalTicks ts = TimingEvents::GetTimestampForNewEvent();
return (ts >= m_last_run_time) ? static_cast<TickCount>(ts - m_last_run_time) : 0;
}
TickCount TimingEvent::GetTicksUntilNextExecution() const
{
return std::max(m_downcount - CPU::GetPendingTicks(), static_cast<TickCount>(0));
const GlobalTicks ts = TimingEvents::GetTimestampForNewEvent();
return (ts >= m_next_run_time) ? 0 : static_cast<TickCount>(m_next_run_time - ts);
}
void TimingEvent::Delay(TickCount ticks)
{
using namespace TimingEvents;
if (!m_active)
{
Panic("Trying to delay an inactive event");
return;
}
m_downcount += ticks;
DebugAssert(TimingEvents::s_state.current_event != this);
TimingEvents::SortEvent(this);
if (TimingEvents::s_state.active_events_head == this)
TimingEvents::UpdateCPUDowncount();
m_next_run_time += static_cast<u32>(ticks);
SortEvent(this);
if (s_state.active_events_head == this)
UpdateCPUDowncount();
}
void TimingEvent::Schedule(TickCount ticks)
{
using namespace TimingEvents;
const TickCount pending_ticks = CPU::GetPendingTicks();
const TickCount new_downcount = pending_ticks + ticks;
const GlobalTicks ts = GetTimestampForNewEvent();
const GlobalTicks next_run_time = ts + static_cast<u32>(ticks);
// See note in RunEvents().
s_state.current_event_new_downcount =
(s_state.current_event == this) ? new_downcount : s_state.current_event_new_downcount;
s_state.current_event_next_run_time =
(s_state.current_event == this) ? next_run_time : s_state.current_event_next_run_time;
if (!m_active)
{
// Event is going active, so we want it to only execute ticks from the current timestamp.
m_downcount = new_downcount;
m_time_since_last_run = -pending_ticks;
m_next_run_time = next_run_time;
m_last_run_time = ts;
m_active = true;
AddActiveEvent(this);
}
@ -493,7 +589,7 @@ void TimingEvent::Schedule(TickCount ticks)
// If this is a call from an IO handler for example, re-sort the event queue.
if (s_state.current_event != this)
{
m_downcount = new_downcount;
m_next_run_time = next_run_time;
SortEvent(this);
if (s_state.active_events_head == this)
UpdateCPUDowncount();
@ -503,6 +599,7 @@ void TimingEvent::Schedule(TickCount ticks)
void TimingEvent::SetIntervalAndSchedule(TickCount ticks)
{
DebugAssert(ticks > 0);
SetInterval(ticks);
Schedule(ticks);
}
@ -516,51 +613,61 @@ void TimingEvent::SetPeriodAndSchedule(TickCount ticks)
void TimingEvent::InvokeEarly(bool force /* = false */)
{
using namespace TimingEvents;
if (!m_active)
return;
const TickCount pending_ticks = CPU::GetPendingTicks();
const TickCount ticks_to_execute = m_time_since_last_run + pending_ticks;
if ((!force && ticks_to_execute < m_period) || ticks_to_execute <= 0)
// Might happen due to other InvokeEarly()'s mid event loop.
const GlobalTicks ts = GetTimestampForNewEvent();
if (ts <= m_last_run_time)
return;
// Shouldn't be invoking early when we're the current event running.
DebugAssert(TimingEvents::s_state.current_event != this);
// TODO: Make DebugAssert instead.
Assert(s_state.current_event != this);
m_downcount = pending_ticks + m_interval;
m_time_since_last_run -= ticks_to_execute;
m_callback(m_callback_param, ticks_to_execute, 0);
const TickCount ticks_to_execute = static_cast<TickCount>(ts - m_last_run_time);
if (!force && ticks_to_execute < m_period)
return;
m_next_run_time = ts + static_cast<u32>(m_interval);
m_last_run_time = ts;
// Since we've changed the downcount, we need to re-sort the events.
TimingEvents::SortEvent(this);
if (TimingEvents::s_state.active_events_head == this)
TimingEvents::UpdateCPUDowncount();
SortEvent(this);
if (s_state.active_events_head == this)
UpdateCPUDowncount();
m_callback(m_callback_param, ticks_to_execute, 0);
}
void TimingEvent::Activate()
{
using namespace TimingEvents;
if (m_active)
return;
// leave the downcount intact
// if we're running events, this is going to be zero, so no effect
const TickCount pending_ticks = CPU::GetPendingTicks();
m_downcount += pending_ticks;
m_time_since_last_run -= pending_ticks;
const GlobalTicks ts = GetTimestampForNewEvent();
const GlobalTicks next_run_time = ts + static_cast<u32>(m_interval);
m_next_run_time = next_run_time;
m_last_run_time = ts;
s_state.current_event_next_run_time =
(s_state.current_event == this) ? next_run_time : s_state.current_event_next_run_time;
m_active = true;
TimingEvents::AddActiveEvent(this);
AddActiveEvent(this);
}
void TimingEvent::Deactivate()
{
using namespace TimingEvents;
if (!m_active)
return;
const TickCount pending_ticks = CPU::GetPendingTicks();
m_downcount -= pending_ticks;
m_time_since_last_run += pending_ticks;
m_active = false;
TimingEvents::RemoveActiveEvent(this);
RemoveActiveEvent(this);
}