Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CoreTiming: Allow forcing seek of accurate overall emulation time. #13392

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 96 additions & 49 deletions Source/Core/Core/CoreTiming.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

#include "Core/AchievementManager.h"
#include "Core/CPUThreadConfigCallback.h"
#include "Core/Config/AchievementSettings.h"
#include "Core/Config/MainSettings.h"
#include "Core/Core.h"
#include "Core/PowerPC/PowerPC.h"
Expand Down Expand Up @@ -82,12 +81,6 @@ void CoreTimingManager::UnregisterAllEvents()

void CoreTimingManager::Init()
{
m_registered_config_callback_id =
CPUThreadConfigCallback::AddConfigChangedCallback([this]() { RefreshConfig(); });
RefreshConfig();

m_last_oc_factor = m_config_oc_factor;
m_globals.last_OC_factor_inverted = m_config_oc_inv_factor;
m_system.GetPPCState().downcount = CyclesToDowncount(MAX_SLICE_LENGTH);
m_globals.slice_length = MAX_SLICE_LENGTH;
m_globals.global_timer = 0;
Expand All @@ -104,6 +97,13 @@ void CoreTimingManager::Init()

m_event_fifo_id = 0;
m_ev_lost = RegisterEvent("_lost_event", &EmptyTimedCallback);

m_registered_config_callback_id =
CPUThreadConfigCallback::AddConfigChangedCallback([this]() { RefreshConfig(); });
RefreshConfig();

m_last_oc_factor = m_config_oc_factor;
m_globals.last_OC_factor_inverted = m_config_oc_inv_factor;
}

void CoreTimingManager::Shutdown()
Expand Down Expand Up @@ -133,11 +133,10 @@ void CoreTimingManager::RefreshConfig()
Config::Get(Config::MAIN_EMULATION_SPEED) > 0.0f)
{
Config::SetCurrent(Config::MAIN_EMULATION_SPEED, 1.0f);
m_emulation_speed = 1.0f;
OSD::AddMessage("Minimum speed is 100% in Hardcore Mode");
}

m_emulation_speed = Config::Get(Config::MAIN_EMULATION_SPEED);
UpdateSpeedLimit(GetTicks(), Config::Get(Config::MAIN_EMULATION_SPEED));
}

void CoreTimingManager::DoState(PointerWrap& p)
Expand Down Expand Up @@ -196,13 +195,14 @@ void CoreTimingManager::DoState(PointerWrap& p)
// The exact layout of the heap in memory is implementation defined, therefore it is platform
// and library version specific.
std::ranges::make_heap(m_event_queue, std::ranges::greater{});

// The stave state has changed the time, so our previous Throttle targets are invalid.
// Especially when global_time goes down; So we create a fake throttle update.
ResetThrottle(m_globals.global_timer);
}
}

void CoreTimingManager::Resume()
{
ResetThrottle(m_globals.global_timer);
}

// This should only be called from the CPU thread. If you are calling
// it from any other thread, you are doing something evil
u64 CoreTimingManager::GetTicks() const
Expand Down Expand Up @@ -358,63 +358,109 @@ void CoreTimingManager::Advance()

void CoreTimingManager::Throttle(const s64 target_cycle)
{
// Based on number of cycles and emulation speed, increase the target deadline
const s64 cycles = target_cycle - m_throttle_last_cycle;

// Prevent any throttling code if the amount of time passed is < ~0.122ms
if (cycles < m_throttle_min_clock_per_sleep)
const bool is_unlimited = m_throttle_adj_clock_per_sec == 0;
if (is_unlimited || Core::GetIsThrottlerTempDisabled())
{
ResetThrottle(target_cycle);
m_throttle_disable_vi_int = false;
return;
}

m_throttle_last_cycle = target_cycle;
// Push throttle reference values forward by exact seconds.
// This maintains proper requested overall emulation time
// without any drifting from cumulative rounding errors.
{
const s64 sec_adj = (target_cycle - m_throttle_reference_cycle) / m_throttle_adj_clock_per_sec;
const s64 cycle_adj = sec_adj * m_throttle_adj_clock_per_sec;

const double speed = Core::GetIsThrottlerTempDisabled() ? 0.0 : m_emulation_speed;
m_throttle_reference_cycle += cycle_adj;
m_throttle_reference_time += std::chrono::seconds{sec_adj};
}

if (0.0 < speed)
m_throttle_deadline +=
std::chrono::duration_cast<DT>(DT_s(cycles) / (speed * m_throttle_clock_per_sec));
const s64 elapsed_cycles = target_cycle - m_throttle_reference_cycle;
TimePoint target_time =
m_throttle_reference_time +
Clock::duration{std::chrono::seconds{elapsed_cycles}} / m_throttle_adj_clock_per_sec;

const TimePoint time = Clock::now();
const TimePoint min_deadline = time - m_max_fallback;
const TimePoint max_deadline = time + m_max_fallback;

if (m_throttle_deadline > max_deadline)
{
m_throttle_deadline = max_deadline;
}
else if (m_throttle_deadline < min_deadline)
// Zero disables fallback, or could be considered "infinite" fallback.
if (m_max_fallback != DT::zero())
{
DEBUG_LOG_FMT(COMMON, "System can not to keep up with timings! [relaxing timings by {} us]",
DT_us(min_deadline - m_throttle_deadline).count());
m_throttle_deadline = min_deadline;
}
// If Core fails to keep accurate time within the fallback range we adjust the reference values,
// no longer keeping proper overall emulation progress.
const TimePoint min_target = time - m_max_fallback;
const TimePoint max_target = time + m_max_fallback;

const TimePoint vi_deadline = time - std::min(m_max_fallback, m_max_variance) / 2;
if (target_time < min_target)
{
// Core is running too slow.. i.e. CPU bottleneck.
DEBUG_LOG_FMT(CORE, "Core can not keep up with timings! [relaxing timings by {} ms]",
DT_ms(min_target - target_time).count());
ResetThrottle(target_cycle);
target_time = min_target;
}
else if (target_time > max_target)
{
// Core has run too-far too-fast somehow..
ResetThrottle(target_cycle);
target_time = max_target;
}
}

// Skip the VI interrupt if the CPU is lagging by a certain amount.
// It doesn't matter what amount of lag we skip VI at, as long as it's constant.
m_throttle_disable_vi_int = 0.0 < speed && m_throttle_deadline < vi_deadline;
UpdateVISkip(time, target_time);

// Only sleep if we are behind the deadline
if (time < m_throttle_deadline)
// Sleep if we are behind the target.
if (time < target_time)
{
std::this_thread::sleep_until(m_throttle_deadline);
std::this_thread::sleep_until(target_time);

// Count amount of time sleeping for analytics
const TimePoint time_after_sleep = Clock::now();
const auto time_after_sleep = Clock::now();
g_perf_metrics.CountThrottleSleep(time_after_sleep - time);
}
}

void CoreTimingManager::UpdateSpeedLimit(s64 cycle, double new_speed)
{
m_emulation_speed = new_speed;

const u32 new_clock_per_sec =
std::lround(m_system.GetSystemTimers().GetTicksPerSecond() * new_speed);

const bool was_limited = m_throttle_adj_clock_per_sec != 0;
if (was_limited)
{
// Adjust throttle reference for graceful clock speed transition.
const s64 ticks = cycle - m_throttle_reference_cycle;
const s64 new_ticks = ticks * new_clock_per_sec / m_throttle_adj_clock_per_sec;
m_throttle_reference_cycle = cycle - new_ticks;
}

m_throttle_adj_clock_per_sec = new_clock_per_sec;
}

void CoreTimingManager::ResetThrottle(s64 cycle)
{
m_throttle_last_cycle = cycle;
m_throttle_deadline = Clock::now();
m_throttle_reference_cycle = cycle;
m_throttle_reference_time = Clock::now();
}

TimePoint CoreTimingManager::GetCPUTimePoint(s64 cyclesLate) const
{
return TimePoint(std::chrono::duration_cast<DT>(DT_s(m_globals.global_timer - cyclesLate) /
m_throttle_clock_per_sec));
m_system.GetSystemTimers().GetTicksPerSecond()));
}

void CoreTimingManager::UpdateVISkip(TimePoint current_time, TimePoint target_time)
{
const DT vi_fallback =
(m_max_fallback == DT::zero()) ? m_max_variance : std::min(m_max_variance, m_max_fallback);

// Skip the VI interrupt if the CPU is lagging by a certain amount.
// It doesn't matter what amount of lag we skip VI at, as long as it's constant.
const TimePoint vi_target = current_time - vi_fallback / 2;
m_throttle_disable_vi_int = target_time < vi_target;
}

bool CoreTimingManager::GetVISkip() const
Expand All @@ -441,13 +487,14 @@ void CoreTimingManager::LogPendingEvents() const
// Should only be called from the CPU thread after the PPC clock has changed
void CoreTimingManager::AdjustEventQueueTimes(u32 new_ppc_clock, u32 old_ppc_clock)
{
m_throttle_clock_per_sec = new_ppc_clock;
m_throttle_min_clock_per_sleep = new_ppc_clock / 1200;
const s64 ticks = m_globals.global_timer;

UpdateSpeedLimit(ticks, m_emulation_speed);

for (Event& ev : m_event_queue)
{
const s64 ticks = (ev.time - m_globals.global_timer) * new_ppc_clock / old_ppc_clock;
ev.time = m_globals.global_timer + ticks;
const s64 ev_ticks = (ev.time - ticks) * new_ppc_clock / old_ppc_clock;
ev.time = ticks + ev_ticks;
}
}

Expand Down
14 changes: 10 additions & 4 deletions Source/Core/Core/CoreTiming.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ class CoreTimingManager
void Init();
void Shutdown();

// Needed when the host-time changes from the guest's perspective.
// e.g. state-load or resume-from-pause
void Resume();

// This should only be called from the CPU thread, if you are calling it any other thread, you are
// doing something evil
u64 GetTicks() const;
Expand Down Expand Up @@ -200,16 +204,18 @@ class CoreTimingManager
float m_config_oc_inv_factor = 0.0f;
bool m_config_sync_on_skip_idle = false;

s64 m_throttle_last_cycle = 0;
TimePoint m_throttle_deadline = Clock::now();
s64 m_throttle_clock_per_sec = 0;
s64 m_throttle_min_clock_per_sleep = 0;
s64 m_throttle_reference_cycle = 0;
TimePoint m_throttle_reference_time = Clock::now();
u32 m_throttle_adj_clock_per_sec = 0;
bool m_throttle_disable_vi_int = false;

DT m_max_fallback = {};
DT m_max_variance = {};
double m_emulation_speed = 1.0;

void UpdateVISkip(TimePoint current_time, TimePoint target_time);

void UpdateSpeedLimit(s64 cycle, double new_speed);
void ResetThrottle(s64 cycle);

int DowncountToCycles(int downcount) const;
Expand Down
1 change: 1 addition & 0 deletions Source/Core/Core/PowerPC/PowerPC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@ void PowerPCManager::SingleStep()

void PowerPCManager::RunLoop()
{
m_system.GetCoreTiming().Resume();
m_cpu_core_base->Run();
Host_UpdateDisasmDialog();
}
Expand Down
17 changes: 17 additions & 0 deletions Source/Core/DolphinQt/Settings/AdvancedPane.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include "Core/System.h"

#include "DolphinQt/Config/ConfigControls/ConfigBool.h"
#include "DolphinQt/Config/ConfigControls/ConfigInteger.h"
#include "DolphinQt/QtUtils/QtUtils.h"
#include "DolphinQt/QtUtils/SignalBlocking.h"
#include "DolphinQt/Settings.h"
Expand Down Expand Up @@ -181,6 +182,22 @@ void AdvancedPane::CreateLayout()
custom_rtc_description->setWordWrap(true);
rtc_options->layout()->addWidget(custom_rtc_description);

{
auto* timing_box = new QGroupBox(tr("Core Timing"));
auto* const timing_layout = new QGridLayout{timing_box};
auto* const max_fallback = new ConfigInteger{0, 10000, Config::MAIN_MAX_FALLBACK};
timing_layout->addWidget(new ConfigIntegerLabel{tr("Maximum Fallback (ms):"), max_fallback}, 0,
0);
timing_layout->addWidget(max_fallback, 0, 1);
auto* max_fallback_desc = new QLabel(
tr("Allowed time drift before abandoning accurate overall emulation time.\n"
"Higher values will permit the emulator to run fast after stutters to catch up.\n"
"Use 0 to always seek accurate time unless paused or speed-adjusted,\n"
"which may be useful for internet play."));
timing_layout->addWidget(max_fallback_desc, 1, 0, 1, 2);
main_layout->addWidget(timing_box);
}

main_layout->addStretch(1);
}

Expand Down