diff --git a/Source/Core/Core/CoreTiming.cpp b/Source/Core/Core/CoreTiming.cpp
index fe06a3329d..39c0809e37 100644
--- a/Source/Core/Core/CoreTiming.cpp
+++ b/Source/Core/Core/CoreTiming.cpp
@@ -74,8 +74,6 @@ static u64 s_fake_dec_start_ticks;
 // Are we in a function that has been called from Advance()
 static bool s_is_global_timer_sane;
 
-Globals g;
-
 static EventType* s_ev_lost = nullptr;
 
 static size_t s_registered_config_callback_id;
@@ -94,7 +92,7 @@ static void EmptyTimedCallback(Core::System& system, u64 userdata, s64 cyclesLat
 //
 // Technically it might be more accurate to call this changing the IPC instead of the CPU speed,
 // but the effect is largely the same.
-static int DowncountToCycles(int downcount)
+static int DowncountToCycles(CoreTiming::Globals& g, int downcount)
 {
   return static_cast<int>(downcount * g.last_OC_factor_inverted);
 }
@@ -127,6 +125,8 @@ void UnregisterAllEvents()
 
 void Init()
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   s_registered_config_callback_id =
       Config::AddConfigChangedCallback([]() { Core::RunAsCPUThread([]() { RefreshConfig(); }); });
   RefreshConfig();
@@ -167,6 +167,8 @@ void RefreshConfig()
 
 void DoState(PointerWrap& p)
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   std::lock_guard lk(s_ts_write_lock);
   p.Do(g.slice_length);
   p.Do(g.global_timer);
@@ -226,10 +228,12 @@ void DoState(PointerWrap& p)
 // it from any other thread, you are doing something evil
 u64 GetTicks()
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   u64 ticks = static_cast<u64>(g.global_timer);
   if (!s_is_global_timer_sane)
   {
-    int downcount = DowncountToCycles(PowerPC::ppcState.downcount);
+    int downcount = DowncountToCycles(g, PowerPC::ppcState.downcount);
     ticks += g.slice_length - downcount;
   }
   return ticks;
@@ -249,6 +253,8 @@ void ScheduleEvent(s64 cycles_into_future, EventType* event_type, u64 userdata,
 {
   ASSERT_MSG(POWERPC, event_type, "Event type is nullptr, will crash now.");
 
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   bool from_cpu_thread;
   if (from == FromThread::ANY)
   {
@@ -309,12 +315,14 @@ void RemoveAllEvents(EventType* event_type)
 
 void ForceExceptionCheck(s64 cycles)
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   cycles = std::max<s64>(0, cycles);
-  if (DowncountToCycles(PowerPC::ppcState.downcount) > cycles)
+  if (DowncountToCycles(g, PowerPC::ppcState.downcount) > cycles)
   {
     // downcount is always (much) smaller than MAX_INT so we can safely cast cycles to an int here.
     // Account for cycles already executed by adjusting the g.slice_length
-    g.slice_length -= DowncountToCycles(PowerPC::ppcState.downcount) - static_cast<int>(cycles);
+    g.slice_length -= DowncountToCycles(g, PowerPC::ppcState.downcount) - static_cast<int>(cycles);
     PowerPC::ppcState.downcount = CyclesToDowncount(static_cast<int>(cycles));
   }
 }
@@ -331,9 +339,11 @@ void MoveEvents()
 
 void Advance()
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   MoveEvents();
 
-  int cyclesExecuted = g.slice_length - DowncountToCycles(PowerPC::ppcState.downcount);
+  int cyclesExecuted = g.slice_length - DowncountToCycles(g, PowerPC::ppcState.downcount);
   g.global_timer += cyclesExecuted;
   s_last_OC_factor = s_config_OC_factor;
   g.last_OC_factor_inverted = s_config_OC_inv_factor;
@@ -369,6 +379,8 @@ void Advance()
 
 void LogPendingEvents()
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   auto clone = s_event_queue;
   std::sort(clone.begin(), clone.end());
   for (const Event& ev : clone)
@@ -381,6 +393,8 @@ void LogPendingEvents()
 // Should only be called from the CPU thread after the PPC clock has changed
 void AdjustEventQueueTimes(u32 new_ppc_clock, u32 old_ppc_clock)
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   for (Event& ev : s_event_queue)
   {
     const s64 ticks = (ev.time - g.global_timer) * new_ppc_clock / old_ppc_clock;
@@ -390,6 +404,8 @@ void AdjustEventQueueTimes(u32 new_ppc_clock, u32 old_ppc_clock)
 
 void Idle()
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
+
   if (s_config_sync_on_skip_idle)
   {
     // When the FIFO is processing data we must not advance because in this way
@@ -399,7 +415,7 @@ void Idle()
   }
 
   PowerPC::UpdatePerformanceMonitor(PowerPC::ppcState.downcount, 0, 0);
-  s_idled_cycles += DowncountToCycles(PowerPC::ppcState.downcount);
+  s_idled_cycles += DowncountToCycles(g, PowerPC::ppcState.downcount);
   PowerPC::ppcState.downcount = 0;
 }
 
@@ -439,21 +455,25 @@ void SetFakeDecStartTicks(u64 val)
 
 u64 GetFakeTBStartValue()
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
   return g.fake_TB_start_value;
 }
 
 void SetFakeTBStartValue(u64 val)
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
   g.fake_TB_start_value = val;
 }
 
 u64 GetFakeTBStartTicks()
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
   return g.fake_TB_start_ticks;
 }
 
 void SetFakeTBStartTicks(u64 val)
 {
+  auto& g = Core::System::GetInstance().GetCoreTimingGlobals();
   g.fake_TB_start_ticks = val;
 }
 
diff --git a/Source/Core/Core/CoreTiming.h b/Source/Core/Core/CoreTiming.h
index 64a5e81a74..55aef4bc72 100644
--- a/Source/Core/Core/CoreTiming.h
+++ b/Source/Core/Core/CoreTiming.h
@@ -37,7 +37,6 @@ struct Globals
   u64 fake_TB_start_ticks;
   float last_OC_factor_inverted;
 };
-extern Globals g;
 
 // CoreTiming begins at the boundary of timing slice -1. An initial call to Advance() is
 // required to end slice -1 and start slice 0 before the first cycle of code is executed.
diff --git a/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp b/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp
index 597a07c9c0..ec3d0229b9 100644
--- a/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp
+++ b/Source/Core/Core/PowerPC/Interpreter/Interpreter.cpp
@@ -212,13 +212,15 @@ int Interpreter::SingleStepInner()
 
 void Interpreter::SingleStep()
 {
+  auto& core_timing_globals = Core::System::GetInstance().GetCoreTimingGlobals();
+
   // Declare start of new slice
   CoreTiming::Advance();
 
   SingleStepInner();
 
   // The interpreter ignores instruction timing information outside the 'fast runloop'.
-  CoreTiming::g.slice_length = 1;
+  core_timing_globals.slice_length = 1;
   PowerPC::ppcState.downcount = 0;
 
   if (PowerPC::ppcState.Exceptions != 0)
diff --git a/Source/Core/Core/PowerPC/Jit64/Jit_SystemRegisters.cpp b/Source/Core/Core/PowerPC/Jit64/Jit_SystemRegisters.cpp
index f611fd7ee1..eb2e938648 100644
--- a/Source/Core/Core/PowerPC/Jit64/Jit_SystemRegisters.cpp
+++ b/Source/Core/Core/PowerPC/Jit64/Jit_SystemRegisters.cpp
@@ -14,6 +14,7 @@
 #include "Core/PowerPC/Jit64/RegCache/JitRegCache.h"
 #include "Core/PowerPC/Jit64Common/Jit64PowerPCState.h"
 #include "Core/PowerPC/PowerPC.h"
+#include "Core/System.h"
 
 using namespace Gen;
 
@@ -322,7 +323,8 @@ void Jit64::mfspr(UGeckoInstruction inst)
     RCX64Reg rax = gpr.Scratch(RAX);
     RCX64Reg rcx = gpr.Scratch(RCX);
 
-    MOV(64, rcx, ImmPtr(&CoreTiming::g));
+    auto& core_timing_globals = Core::System::GetInstance().GetCoreTimingGlobals();
+    MOV(64, rcx, ImmPtr(&core_timing_globals));
 
     // An inline implementation of CoreTiming::GetFakeTimeBase, since in timer-heavy games the
     // cost of calling out to C for this is actually significant.
diff --git a/Source/Core/Core/PowerPC/JitArm64/JitArm64_SystemRegisters.cpp b/Source/Core/Core/PowerPC/JitArm64/JitArm64_SystemRegisters.cpp
index 6a8245b8bb..d93700e742 100644
--- a/Source/Core/Core/PowerPC/JitArm64/JitArm64_SystemRegisters.cpp
+++ b/Source/Core/Core/PowerPC/JitArm64/JitArm64_SystemRegisters.cpp
@@ -13,6 +13,7 @@
 #include "Core/PowerPC/Interpreter/ExceptionUtils.h"
 #include "Core/PowerPC/PPCTables.h"
 #include "Core/PowerPC/PowerPC.h"
+#include "Core/System.h"
 
 using namespace Arm64Gen;
 
@@ -306,7 +307,8 @@ void JitArm64::mfspr(UGeckoInstruction inst)
     // An inline implementation of CoreTiming::GetFakeTimeBase, since in timer-heavy games the
     // cost of calling out to C for this is actually significant.
 
-    MOVP2R(Xg, &CoreTiming::g);
+    auto& core_timing_globals = Core::System::GetInstance().GetCoreTimingGlobals();
+    MOVP2R(Xg, &core_timing_globals);
 
     LDR(IndexType::Unsigned, WA, PPC_REG, PPCSTATE_OFF(downcount));
     m_float_emit.SCVTF(SC, WA);
diff --git a/Source/Core/Core/System.cpp b/Source/Core/Core/System.cpp
index 62d119f071..552c4c4196 100644
--- a/Source/Core/Core/System.cpp
+++ b/Source/Core/Core/System.cpp
@@ -7,6 +7,7 @@
 
 #include "AudioCommon/SoundStream.h"
 #include "Core/Config/MainSettings.h"
+#include "Core/CoreTiming.h"
 #include "Core/HW/AudioInterface.h"
 #include "Core/HW/DSP.h"
 #include "Core/HW/DVD/DVDInterface.h"
@@ -26,6 +27,7 @@ struct System::Impl
   bool m_audio_dump_started = false;
 
   AudioInterface::AudioInterfaceState m_audio_interface_state;
+  CoreTiming::Globals m_core_timing_globals;
   DSP::DSPState m_dsp_state;
   DVDInterface::DVDInterfaceState m_dvd_interface_state;
   DVDThread::DVDThreadState m_dvd_thread_state;
@@ -84,6 +86,11 @@ AudioInterface::AudioInterfaceState& System::GetAudioInterfaceState() const
   return m_impl->m_audio_interface_state;
 }
 
+CoreTiming::Globals& System::GetCoreTimingGlobals() const
+{
+  return m_impl->m_core_timing_globals;
+}
+
 DSP::DSPState& System::GetDSPState() const
 {
   return m_impl->m_dsp_state;
diff --git a/Source/Core/Core/System.h b/Source/Core/Core/System.h
index 3e70f215e4..b2ad82d12d 100644
--- a/Source/Core/Core/System.h
+++ b/Source/Core/Core/System.h
@@ -12,6 +12,10 @@ namespace AudioInterface
 {
 class AudioInterfaceState;
 };
+namespace CoreTiming
+{
+struct Globals;
+}
 namespace DSP
 {
 class DSPState;
@@ -76,6 +80,7 @@ public:
   void SetAudioDumpStarted(bool started);
 
   AudioInterface::AudioInterfaceState& GetAudioInterfaceState() const;
+  CoreTiming::Globals& GetCoreTimingGlobals() const;
   DSP::DSPState& GetDSPState() const;
   DVDInterface::DVDInterfaceState& GetDVDInterfaceState() const;
   DVDThread::DVDThreadState& GetDVDThreadState() const;
diff --git a/Source/UnitTests/Core/CoreTimingTest.cpp b/Source/UnitTests/Core/CoreTimingTest.cpp
index b8ca8cc01b..83bb06097e 100644
--- a/Source/UnitTests/Core/CoreTimingTest.cpp
+++ b/Source/UnitTests/Core/CoreTimingTest.cpp
@@ -14,6 +14,7 @@
 #include "Core/Core.h"
 #include "Core/CoreTiming.h"
 #include "Core/PowerPC/PowerPC.h"
+#include "Core/System.h"
 #include "UICommon/UICommon.h"
 
 // Numbers are chosen randomly to make sure the correct one is given.
@@ -279,9 +280,10 @@ TEST(CoreTiming, ScheduleIntoPast)
   // the stale value, i.e. effectively half-way through the previous slice.
   // NOTE: We're only testing that the scheduler doesn't break, not whether this makes sense.
   Core::UndeclareAsCPUThread();
-  CoreTiming::g.global_timer -= 1000;
+  auto& core_timing_globals = Core::System::GetInstance().GetCoreTimingGlobals();
+  core_timing_globals.global_timer -= 1000;
   CoreTiming::ScheduleEvent(0, cb_b, CB_IDS[1], CoreTiming::FromThread::NON_CPU);
-  CoreTiming::g.global_timer += 1000;
+  core_timing_globals.global_timer += 1000;
   Core::DeclareAsCPUThread();
   AdvanceAndCheck(1, MAX_SLICE_LENGTH, MAX_SLICE_LENGTH + 1000);