diff --git a/src/core/cdrom.cpp b/src/core/cdrom.cpp
index e223c4063..29545d805 100644
--- a/src/core/cdrom.cpp
+++ b/src/core/cdrom.cpp
@@ -299,10 +299,12 @@ static void CreateFileMap();
 static void CreateFileMap(IsoReader& iso, std::string_view dir);
 static const std::string* LookupFileMap(u32 lba, u32* start_lba, u32* end_lba);
 
-static std::unique_ptr<TimingEvent> s_command_event;
-static std::unique_ptr<TimingEvent> s_command_second_response_event;
-static std::unique_ptr<TimingEvent> s_async_interrupt_event;
-static std::unique_ptr<TimingEvent> s_drive_event;
+static TimingEvent s_command_event{"CDROM Command Event", 1, 1, &CDROM::ExecuteCommand, nullptr};
+static TimingEvent s_command_second_response_event{"CDROM Command Second Response Event", 1, 1,
+                                                   &CDROM::ExecuteCommandSecondResponse, nullptr};
+static TimingEvent s_async_interrupt_event{"CDROM Async Interrupt Event", INTERRUPT_DELAY_CYCLES, 1,
+                                           &CDROM::DeliverAsyncInterrupt, nullptr};
+static TimingEvent s_drive_event{"CDROM Drive Event", 1, 1, &CDROM::ExecuteDrive, nullptr};
 
 static Command s_command = Command::None;
 static Command s_command_second_response = Command::None;
@@ -443,14 +445,6 @@ static std::array<CommandInfo, 255> s_command_info = {{
 
 void CDROM::Initialize()
 {
-  s_command_event =
-    TimingEvents::CreateTimingEvent("CDROM Command Event", 1, 1, &CDROM::ExecuteCommand, nullptr, false);
-  s_command_second_response_event = TimingEvents::CreateTimingEvent(
-    "CDROM Command Second Response Event", 1, 1, &CDROM::ExecuteCommandSecondResponse, nullptr, false);
-  s_async_interrupt_event = TimingEvents::CreateTimingEvent("CDROM Async Interrupt Event", INTERRUPT_DELAY_CYCLES, 1,
-                                                            &CDROM::DeliverAsyncInterrupt, nullptr, false);
-  s_drive_event = TimingEvents::CreateTimingEvent("CDROM Drive Event", 1, 1, &CDROM::ExecuteDrive, nullptr, false);
-
   if (g_settings.cdrom_readahead_sectors > 0)
     s_reader.StartThread(g_settings.cdrom_readahead_sectors);
 
@@ -463,10 +457,10 @@ void CDROM::Shutdown()
   s_file_map_created = false;
   s_show_current_file = false;
 
-  s_drive_event.reset();
-  s_async_interrupt_event.reset();
-  s_command_second_response_event.reset();
-  s_command_event.reset();
+  s_drive_event.Deactivate();
+  s_async_interrupt_event.Deactivate();
+  s_command_second_response_event.Deactivate();
+  s_command_event.Deactivate();
   s_reader.StopThread();
   s_reader.RemoveMedia();
 }
@@ -474,7 +468,7 @@ void CDROM::Shutdown()
 void CDROM::Reset()
 {
   s_command = Command::None;
-  s_command_event->Deactivate();
+  s_command_event.Deactivate();
   ClearCommandSecondResponse();
   ClearDriveState();
   s_status.bits = 0;
@@ -569,7 +563,7 @@ void CDROM::SoftReset(TickCount ticks_late)
     if (s_current_lba != 0)
     {
       s_drive_state = DriveState::SeekingImplicit;
-      s_drive_event->SetIntervalAndSchedule(total_ticks);
+      s_drive_event.SetIntervalAndSchedule(total_ticks);
       s_requested_lba = 0;
       s_reader.QueueReadSector(s_requested_lba);
       s_seek_start_lba = s_current_lba;
@@ -578,7 +572,7 @@ void CDROM::SoftReset(TickCount ticks_late)
     else
     {
       s_drive_state = DriveState::ChangingSpeedOrTOCRead;
-      s_drive_event->Schedule(total_ticks);
+      s_drive_event.Schedule(total_ticks);
     }
   }
 }
@@ -697,10 +691,10 @@ bool CDROM::DoState(StateWrapper& sw)
     if (s_reader.HasMedia())
       s_reader.QueueReadSector(s_requested_lba);
     UpdateCommandEvent();
-    s_drive_event->SetState(!IsDriveIdle());
+    s_drive_event.SetState(!IsDriveIdle());
 
     // Time will get fixed up later.
-    s_command_second_response_event->SetState(s_command_second_response != Command::None);
+    s_command_second_response_event.SetState(s_command_second_response != Command::None);
   }
 
   return !sw.HasError();
@@ -824,7 +818,7 @@ std::unique_ptr<CDImage> CDROM::RemoveMedia(bool for_disc_swap)
   ClearDriveState();
   ClearCommandSecondResponse();
   s_command = Command::None;
-  s_command_event->Deactivate();
+  s_command_event.Deactivate();
 
   // The console sends an interrupt when the shell is opened regardless of whether a command was executing.
   ClearAsyncInterrupt();
@@ -834,7 +828,7 @@ std::unique_ptr<CDImage> CDROM::RemoveMedia(bool for_disc_swap)
   if (for_disc_swap)
   {
     s_drive_state = DriveState::ShellOpening;
-    s_drive_event->SetIntervalAndSchedule(stop_ticks);
+    s_drive_event.SetIntervalAndSchedule(stop_ticks);
   }
 
   return image;
@@ -890,7 +884,7 @@ void CDROM::CPUClockChanged()
 {
   // reschedule the disc read event
   if (IsReadingOrPlaying())
-    s_drive_event->SetInterval(GetTicksForRead());
+    s_drive_event.SetInterval(GetTicksForRead());
 }
 
 u8 CDROM::ReadRegister(u32 offset)
@@ -1115,7 +1109,7 @@ void CDROM::WriteRegister(u32 offset, u8 value)
                                        sizeof(s_cd_audio_volume_matrix)) != 0))
       {
         if (HasPendingDiscEvent())
-          s_drive_event->InvokeEarly();
+          s_drive_event.InvokeEarly();
         SPU::GeneratePendingSamples();
       }
 
@@ -1210,7 +1204,7 @@ void CDROM::SetAsyncInterrupt(Interrupt interrupt)
 void CDROM::ClearAsyncInterrupt()
 {
   s_pending_async_interrupt = 0;
-  s_async_interrupt_event->Deactivate();
+  s_async_interrupt_event.Deactivate();
   s_async_response_fifo.Clear();
 }
 
@@ -1235,7 +1229,7 @@ void CDROM::QueueDeliverAsyncInterrupt()
   {
     DEV_LOG("Delaying async interrupt {} because it's been {} cycles since last interrupt", s_pending_async_interrupt,
             diff);
-    s_async_interrupt_event->Schedule(INTERRUPT_DELAY_CYCLES);
+    s_async_interrupt_event.Schedule(INTERRUPT_DELAY_CYCLES);
   }
 }
 
@@ -1244,12 +1238,12 @@ void CDROM::DeliverAsyncInterrupt(void*, TickCount ticks, TickCount ticks_late)
   if (HasPendingInterrupt())
   {
     // This shouldn't really happen, because we should block command execution.. but just in case.
-    if (!s_async_interrupt_event->IsActive())
-      s_async_interrupt_event->Schedule(INTERRUPT_DELAY_CYCLES);
+    if (!s_async_interrupt_event.IsActive())
+      s_async_interrupt_event.Schedule(INTERRUPT_DELAY_CYCLES);
   }
   else
   {
-    s_async_interrupt_event->Deactivate();
+    s_async_interrupt_event.Deactivate();
 
     Assert(s_pending_async_interrupt != 0 && !HasPendingInterrupt());
     DEBUG_LOG("Delivering async interrupt {}", s_pending_async_interrupt);
@@ -1308,7 +1302,7 @@ void CDROM::UpdateInterruptRequest()
 
 bool CDROM::HasPendingDiscEvent()
 {
-  return (s_drive_event->IsActive() && s_drive_event->GetTicksUntilNextExecution() <= 0);
+  return (s_drive_event.IsActive() && s_drive_event.GetTicksUntilNextExecution() <= 0);
 }
 
 TickCount CDROM::GetAckDelayForCommand(Command command)
@@ -1336,7 +1330,7 @@ TickCount CDROM::GetTicksForIDRead()
 {
   TickCount ticks = ID_READ_TICKS;
   if (s_drive_state == DriveState::SpinningUp)
-    ticks += s_drive_event->GetTicksUntilNextExecution();
+    ticks += s_drive_event.GetTicksUntilNextExecution();
 
   return ticks;
 }
@@ -1376,7 +1370,7 @@ TickCount CDROM::GetTicksForSeek(CDImage::LBA new_lba, bool ignore_speed_change)
   if (!IsMotorOn())
   {
     ticks +=
-      (s_drive_state == DriveState::SpinningUp) ? s_drive_event->GetTicksUntilNextExecution() : GetTicksForSpinUp();
+      (s_drive_state == DriveState::SpinningUp) ? s_drive_event.GetTicksUntilNextExecution() : GetTicksForSpinUp();
     if (s_drive_state == DriveState::ShellOpening || s_drive_state == DriveState::SpinningUp)
       ClearDriveState();
   }
@@ -1416,7 +1410,7 @@ TickCount CDROM::GetTicksForSeek(CDImage::LBA new_lba, bool ignore_speed_change)
   if (s_drive_state == DriveState::ChangingSpeedOrTOCRead && !ignore_speed_change)
   {
     // we're still reading the TOC, so add that time in
-    const TickCount remaining_change_ticks = s_drive_event->GetTicksUntilNextExecution();
+    const TickCount remaining_change_ticks = s_drive_event.GetTicksUntilNextExecution();
     ticks += remaining_change_ticks;
 
     DEV_LOG("Seek time for {} LBAs: {} ({:.3f} ms) ({} for speed change/implicit TOC read)", lba_diff, ticks,
@@ -1492,16 +1486,16 @@ void CDROM::BeginCommand(Command command)
                 s_command_info[static_cast<u8>(command)].name);
 
     // subtract the currently-elapsed ack ticks from the new command
-    if (s_command_event->IsActive())
+    if (s_command_event.IsActive())
     {
-      const TickCount elapsed_ticks = s_command_event->GetInterval() - s_command_event->GetTicksUntilNextExecution();
+      const TickCount elapsed_ticks = s_command_event.GetInterval() - s_command_event.GetTicksUntilNextExecution();
       ack_delay = std::max(ack_delay - elapsed_ticks, 1);
-      s_command_event->Deactivate();
+      s_command_event.Deactivate();
     }
   }
 
   s_command = command;
-  s_command_event->SetIntervalAndSchedule(ack_delay);
+  s_command_event.SetIntervalAndSchedule(ack_delay);
   UpdateCommandEvent();
   UpdateStatusRegister();
 }
@@ -1511,7 +1505,7 @@ void CDROM::EndCommand()
   s_param_fifo.Clear();
 
   s_command = Command::None;
-  s_command_event->Deactivate();
+  s_command_event.Deactivate();
   UpdateStatusRegister();
 }
 
@@ -1633,7 +1627,7 @@ void CDROM::ExecuteCommand(void*, TickCount ticks, TickCount ticks_late)
         if (s_drive_state == DriveState::ChangingSpeedOrTOCRead)
         {
           // cancel the speed change if it's less than a quarter complete
-          if (s_drive_event->GetTicksUntilNextExecution() >= (GetTicksForSpeedChange() / 4))
+          if (s_drive_event.GetTicksUntilNextExecution() >= (GetTicksForSpeedChange() / 4))
           {
             DEV_LOG("Cancelling speed change event");
             ClearDriveState();
@@ -1648,19 +1642,19 @@ void CDROM::ExecuteCommand(void*, TickCount ticks, TickCount ticks_late)
             DEV_LOG("Drive is {}, delaying event by {} ticks for speed change to {}-speed",
                     s_drive_state_names[static_cast<u8>(s_drive_state)], change_ticks,
                     s_mode.double_speed ? "double" : "single");
-            s_drive_event->Delay(change_ticks);
+            s_drive_event.Delay(change_ticks);
 
             if (IsReadingOrPlaying())
             {
               WARNING_LOG("Speed change while reading/playing, reads will be temporarily delayed.");
-              s_drive_event->SetInterval(GetTicksForRead());
+              s_drive_event.SetInterval(GetTicksForRead());
             }
           }
           else
           {
             DEV_LOG("Drive is idle, speed change takes {} ticks", change_ticks);
             s_drive_state = DriveState::ChangingSpeedOrTOCRead;
-            s_drive_event->Schedule(change_ticks);
+            s_drive_event.Schedule(change_ticks);
           }
         }
       }
@@ -1736,7 +1730,7 @@ void CDROM::ExecuteCommand(void*, TickCount ticks, TickCount ticks_late)
 
         s_async_command_parameter = session;
         s_drive_state = DriveState::ChangingSession;
-        s_drive_event->Schedule(GetTicksForTOCRead());
+        s_drive_event.Schedule(GetTicksForTOCRead());
       }
 
       EndCommand();
@@ -1870,7 +1864,7 @@ void CDROM::ExecuteCommand(void*, TickCount ticks, TickCount ticks_late)
       {
         // Stop reading.
         s_drive_state = DriveState::Idle;
-        s_drive_event->Deactivate();
+        s_drive_event.Deactivate();
         s_secondary_status.ClearActiveBits();
       }
 
@@ -2119,7 +2113,7 @@ void CDROM::ExecuteCommand(void*, TickCount ticks, TickCount ticks_late)
 
       // According to nocash this doesn't clear the parameter FIFO.
       s_command = Command::None;
-      s_command_event->Deactivate();
+      s_command_event.Deactivate();
       UpdateStatusRegister();
     }
     break;
@@ -2283,14 +2277,14 @@ void CDROM::ExecuteCommandSecondResponse(void*, TickCount ticks, TickCount ticks
   }
 
   s_command_second_response = Command::None;
-  s_command_second_response_event->Deactivate();
+  s_command_second_response_event.Deactivate();
 }
 
 void CDROM::QueueCommandSecondResponse(Command command, TickCount ticks)
 {
   ClearCommandSecondResponse();
   s_command_second_response = command;
-  s_command_second_response_event->Schedule(ticks);
+  s_command_second_response_event.Schedule(ticks);
 }
 
 void CDROM::ClearCommandSecondResponse()
@@ -2301,7 +2295,7 @@ void CDROM::ClearCommandSecondResponse()
             s_command_info[static_cast<u16>(s_command_second_response)].name);
   }
 
-  s_command_second_response_event->Deactivate();
+  s_command_second_response_event.Deactivate();
   s_command_second_response = Command::None;
 }
 
@@ -2311,12 +2305,12 @@ void CDROM::UpdateCommandEvent()
   // so deactivate it until the interrupt is acknowledged
   if (!HasPendingCommand() || HasPendingInterrupt() || HasPendingAsyncInterrupt())
   {
-    s_command_event->Deactivate();
+    s_command_event.Deactivate();
     return;
   }
   else if (HasPendingCommand())
   {
-    s_command_event->Activate();
+    s_command_event.Activate();
   }
 }
 
@@ -2395,7 +2389,7 @@ void CDROM::ExecuteDrive(void*, TickCount ticks, TickCount ticks_late)
 void CDROM::ClearDriveState()
 {
   s_drive_state = DriveState::Idle;
-  s_drive_event->Deactivate();
+  s_drive_event.Deactivate();
 }
 
 void CDROM::BeginReading(TickCount ticks_late /* = 0 */, bool after_seek /* = false */)
@@ -2411,7 +2405,7 @@ void CDROM::BeginReading(TickCount ticks_late /* = 0 */, bool after_seek /* = fa
   if (IsSeeking())
   {
     DEV_LOG("Read command while seeking, scheduling read after seek {} -> {} finishes in {} ticks", s_seek_start_lba,
-            s_seek_end_lba, s_drive_event->GetTicksUntilNextExecution());
+            s_seek_end_lba, s_drive_event.GetTicksUntilNextExecution());
 
     // Implicit seeks won't trigger the read, so swap it for a logical.
     if (s_drive_state == DriveState::SeekingImplicit)
@@ -2433,8 +2427,8 @@ void CDROM::BeginReading(TickCount ticks_late /* = 0 */, bool after_seek /* = fa
   ResetAudioDecoder();
 
   s_drive_state = DriveState::Reading;
-  s_drive_event->SetInterval(ticks);
-  s_drive_event->Schedule(first_sector_ticks);
+  s_drive_event.SetInterval(ticks);
+  s_drive_event.Schedule(first_sector_ticks);
 
   s_requested_lba = s_current_lba;
   s_reader.QueueReadSector(s_requested_lba);
@@ -2476,8 +2470,8 @@ void CDROM::BeginPlaying(u8 track, TickCount ticks_late /* = 0 */, bool after_se
   ResetAudioDecoder();
 
   s_drive_state = DriveState::Playing;
-  s_drive_event->SetInterval(ticks);
-  s_drive_event->Schedule(first_sector_ticks);
+  s_drive_event.SetInterval(ticks);
+  s_drive_event.Schedule(first_sector_ticks);
 
   s_requested_lba = s_current_lba;
   s_reader.QueueReadSector(s_requested_lba);
@@ -2509,7 +2503,7 @@ void CDROM::BeginSeeking(bool logical, bool read_after_seek, bool play_after_see
   s_last_sector_header_valid = false;
 
   s_drive_state = logical ? DriveState::SeekingLogical : DriveState::SeekingPhysical;
-  s_drive_event->SetIntervalAndSchedule(seek_time);
+  s_drive_event.SetIntervalAndSchedule(seek_time);
 
   s_seek_start_lba = s_current_lba;
   s_seek_end_lba = seek_lba;
@@ -2521,8 +2515,8 @@ void CDROM::UpdatePositionWhileSeeking()
 {
   DebugAssert(IsSeeking());
 
-  const float completed_frac = 1.0f - std::min(static_cast<float>(s_drive_event->GetTicksUntilNextExecution()) /
-                                                 static_cast<float>(s_drive_event->GetInterval()),
+  const float completed_frac = 1.0f - std::min(static_cast<float>(s_drive_event.GetTicksUntilNextExecution()) /
+                                                 static_cast<float>(s_drive_event.GetInterval()),
                                                1.0f);
 
   CDImage::LBA current_lba;
@@ -2804,7 +2798,7 @@ void CDROM::DoSpinUpComplete()
 {
   DEBUG_LOG("Spinup complete");
   s_drive_state = DriveState::Idle;
-  s_drive_event->Deactivate();
+  s_drive_event.Deactivate();
   s_secondary_status.ClearActiveBits();
   s_secondary_status.motor_on = true;
 }
@@ -2813,7 +2807,7 @@ void CDROM::DoSpeedChangeOrImplicitTOCReadComplete()
 {
   DEBUG_LOG("Speed change/implicit TOC read complete");
   s_drive_state = DriveState::Idle;
-  s_drive_event->Deactivate();
+  s_drive_event.Deactivate();
 }
 
 void CDROM::DoIDRead()
@@ -2878,7 +2872,7 @@ void CDROM::StartMotor()
 
   DEV_LOG("Starting motor");
   s_drive_state = DriveState::SpinningUp;
-  s_drive_event->Schedule(GetTicksForSpinUp());
+  s_drive_event.Schedule(GetTicksForSpinUp());
 }
 
 void CDROM::StopMotor()
@@ -3387,7 +3381,7 @@ void CDROM::CheckForSectorBufferReadComplete()
     DEV_LOG("Sending additional INT1 for missed sector in buffer {}", s_current_write_sector_buffer);
     s_async_response_fifo.Push(s_secondary_status.bits);
     s_pending_async_interrupt = static_cast<u8>(Interrupt::DataReady);
-    s_async_interrupt_event->Schedule(INTERRUPT_DELAY_CYCLES);
+    s_async_interrupt_event.Schedule(INTERRUPT_DELAY_CYCLES);
   }
 }
 
@@ -3669,7 +3663,7 @@ void CDROM::DrawDebugWindow()
     {
       ImGui::TextColored(active_color, "Command: %s (0x%02X) (%d ticks remaining)",
                          s_command_info[static_cast<u8>(s_command)].name, static_cast<u8>(s_command),
-                         s_command_event->IsActive() ? s_command_event->GetTicksUntilNextExecution() : 0);
+                         s_command_event.IsActive() ? s_command_event.GetTicksUntilNextExecution() : 0);
     }
     else
     {
@@ -3684,7 +3678,7 @@ void CDROM::DrawDebugWindow()
     {
       ImGui::TextColored(active_color, "Drive: %s (%d ticks remaining)",
                          s_drive_state_names[static_cast<u8>(s_drive_state)],
-                         s_drive_event->IsActive() ? s_drive_event->GetTicksUntilNextExecution() : 0);
+                         s_drive_event.IsActive() ? s_drive_event.GetTicksUntilNextExecution() : 0);
     }
 
     ImGui::Text("Interrupt Enable Register: 0x%02X", s_interrupt_enable_register);
diff --git a/src/core/dma.cpp b/src/core/dma.cpp
index ecc336945..dd01f0ee7 100644
--- a/src/core/dma.cpp
+++ b/src/core/dma.cpp
@@ -204,7 +204,7 @@ struct DMAState
   TickCount halt_ticks = 100;
 
   std::vector<u32> transfer_buffer;
-  std::unique_ptr<TimingEvent> unhalt_event;
+  TimingEvent unhalt_event{"DMA Transfer Unhalt", 1, 1, &DMA::UnhaltTransfer, nullptr};
   TickCount halt_ticks_remaining = 0;
 
   std::array<ChannelState, NUM_CHANNELS> channels;
@@ -243,22 +243,20 @@ void DMA::Initialize()
 {
   s_state.max_slice_ticks = g_settings.dma_max_slice_ticks;
   s_state.halt_ticks = g_settings.dma_halt_ticks;
-
-  s_state.unhalt_event = TimingEvents::CreateTimingEvent("DMA Transfer Unhalt", 1, s_state.max_slice_ticks,
-                                                         &DMA::UnhaltTransfer, nullptr, false);
+  s_state.unhalt_event.SetInterval(s_state.max_slice_ticks);
   Reset();
 }
 
 void DMA::Shutdown()
 {
   ClearState();
-  s_state.unhalt_event.reset();
+  s_state.unhalt_event.Deactivate();
 }
 
 void DMA::Reset()
 {
   ClearState();
-  s_state.unhalt_event->Deactivate();
+  s_state.unhalt_event.Deactivate();
 }
 
 void DMA::ClearState()
@@ -297,9 +295,9 @@ bool DMA::DoState(StateWrapper& sw)
   if (sw.IsReading())
   {
     if (s_state.halt_ticks_remaining > 0)
-      s_state.unhalt_event->SetIntervalAndSchedule(s_state.halt_ticks_remaining);
+      s_state.unhalt_event.SetIntervalAndSchedule(s_state.halt_ticks_remaining);
     else
-      s_state.unhalt_event->Deactivate();
+      s_state.unhalt_event.Deactivate();
   }
 
   return !sw.HasError();
@@ -502,7 +500,7 @@ ALWAYS_INLINE_RELEASE bool DMA::CanTransferChannel(Channel channel, bool ignore_
 
 bool DMA::IsTransferHalted()
 {
-  return s_state.unhalt_event->IsActive();
+  return s_state.unhalt_event.IsActive();
 }
 
 void DMA::UpdateIRQ()
@@ -733,7 +731,7 @@ bool DMA::TransferChannel()
         if (cs.request)
         {
           // we got halted
-          if (!s_state.unhalt_event->IsActive())
+          if (!s_state.unhalt_event.IsActive())
             HaltTransfer(s_state.halt_ticks);
 
           return false;
@@ -757,18 +755,18 @@ void DMA::HaltTransfer(TickCount duration)
 {
   s_state.halt_ticks_remaining += duration;
   DEBUG_LOG("Halting DMA for {} ticks", s_state.halt_ticks_remaining);
-  if (s_state.unhalt_event->IsActive())
+  if (s_state.unhalt_event.IsActive())
     return;
 
-  DebugAssert(!s_state.unhalt_event->IsActive());
-  s_state.unhalt_event->SetIntervalAndSchedule(s_state.halt_ticks_remaining);
+  DebugAssert(!s_state.unhalt_event.IsActive());
+  s_state.unhalt_event.SetIntervalAndSchedule(s_state.halt_ticks_remaining);
 }
 
 void DMA::UnhaltTransfer(void*, TickCount ticks, TickCount ticks_late)
 {
   DEBUG_LOG("Resuming DMA after {} ticks, {} ticks late", ticks, -(s_state.halt_ticks_remaining - ticks));
   s_state.halt_ticks_remaining -= ticks;
-  s_state.unhalt_event->Deactivate();
+  s_state.unhalt_event.Deactivate();
 
   // TODO: Use channel priority. But doing it in ascending order is probably good enough.
   // Main thing is that OTC happens after GPU, because otherwise it'll wipe out the LL.
diff --git a/src/core/gpu.cpp b/src/core/gpu.cpp
index 57f33bc34..189581be2 100644
--- a/src/core/gpu.cpp
+++ b/src/core/gpu.cpp
@@ -60,12 +60,22 @@ static u32 s_active_gpu_cycles_frames = 0;
 static constexpr GPUTexture::Format DISPLAY_INTERNAL_POSTFX_FORMAT = GPUTexture::Format::RGBA8;
 
 GPU::GPU()
+  : m_crtc_tick_event(
+      "GPU CRTC Tick", 1, 1,
+      [](void* param, TickCount ticks, TickCount ticks_late) { static_cast<GPU*>(param)->CRTCTickEvent(ticks); }, this),
+    m_command_tick_event(
+      "GPU Command Tick", 1, 1,
+      [](void* param, TickCount ticks, TickCount ticks_late) { static_cast<GPU*>(param)->CommandTickEvent(ticks); },
+      this)
 {
   ResetStatistics();
 }
 
 GPU::~GPU()
 {
+  m_command_tick_event.Deactivate();
+  m_crtc_tick_event.Deactivate();
+
   JoinScreenshotThreads();
   DestroyDeinterlaceTextures();
   g_gpu_device->RecycleTexture(std::move(m_chroma_smoothing_texture));
@@ -78,14 +88,7 @@ bool GPU::Initialize()
 {
   m_force_progressive_scan = g_settings.gpu_disable_interlacing;
   m_force_ntsc_timings = g_settings.gpu_force_ntsc_timings;
-  m_crtc_tick_event = TimingEvents::CreateTimingEvent(
-    "GPU CRTC Tick", 1, 1,
-    [](void* param, TickCount ticks, TickCount ticks_late) { static_cast<GPU*>(param)->CRTCTickEvent(ticks); }, this,
-    true);
-  m_command_tick_event = TimingEvents::CreateTimingEvent(
-    "GPU Command Tick", 1, 1,
-    [](void* param, TickCount ticks, TickCount ticks_late) { static_cast<GPU*>(param)->CommandTickEvent(ticks); }, this,
-    true);
+  m_crtc_tick_event.Activate();
   m_fifo_size = g_settings.gpu_fifo_size;
   m_max_run_ahead = g_settings.gpu_max_run_ahead;
   m_console_is_pal = System::IsPALRegion();
@@ -187,8 +190,8 @@ void GPU::Reset(bool clear_vram)
   m_blitter_state = BlitterState::Idle;
 
   // Force event to reschedule itself.
-  m_crtc_tick_event->Deactivate();
-  m_command_tick_event->Deactivate();
+  m_crtc_tick_event.Deactivate();
+  m_command_tick_event.Deactivate();
 
   SoftReset();
   UpdateDisplay();
@@ -455,7 +458,7 @@ u32 GPU::ReadRegister(u32 offset)
       if (IsCRTCScanlinePending())
         SynchronizeCRTC();
       if (IsCommandCompletionPending())
-        m_command_tick_event->InvokeEarly();
+        m_command_tick_event.InvokeEarly();
 
       return m_GPUSTAT.bits;
     }
@@ -547,7 +550,7 @@ void GPU::AddCommandTicks(TickCount ticks)
 
 void GPU::SynchronizeCRTC()
 {
-  m_crtc_tick_event->InvokeEarly();
+  m_crtc_tick_event.InvokeEarly();
 }
 
 float GPU::ComputeHorizontalFrequency() const
@@ -833,17 +836,17 @@ void GPU::UpdateCRTCDisplayParameters()
 
 TickCount GPU::GetPendingCRTCTicks() const
 {
-  const TickCount pending_sysclk_ticks = m_crtc_tick_event->GetTicksSinceLastExecution();
+  const TickCount pending_sysclk_ticks = m_crtc_tick_event.GetTicksSinceLastExecution();
   TickCount fractional_ticks = m_crtc_state.fractional_ticks;
   return SystemTicksToCRTCTicks(pending_sysclk_ticks, &fractional_ticks);
 }
 
 TickCount GPU::GetPendingCommandTicks() const
 {
-  if (!m_command_tick_event->IsActive())
+  if (!m_command_tick_event.IsActive())
     return 0;
 
-  return SystemTicksToGPUTicks(m_command_tick_event->GetTicksSinceLastExecution());
+  return SystemTicksToGPUTicks(m_command_tick_event.GetTicksSinceLastExecution());
 }
 
 void GPU::UpdateCRTCTickEvent()
@@ -900,7 +903,7 @@ void GPU::UpdateCRTCTickEvent()
     ticks_until_event = std::min(ticks_until_event, ticks_until_hblank_start_or_end);
   }
 
-  m_crtc_tick_event->Schedule(CRTCTicksToSystemTicks(ticks_until_event, m_crtc_state.fractional_ticks));
+  m_crtc_tick_event.Schedule(CRTCTicksToSystemTicks(ticks_until_event, m_crtc_state.fractional_ticks));
 }
 
 bool GPU::IsCRTCScanlinePending() const
@@ -1080,11 +1083,11 @@ void GPU::UpdateCommandTickEvent()
   if (m_pending_command_ticks <= 0)
   {
     m_pending_command_ticks = 0;
-    m_command_tick_event->Deactivate();
+    m_command_tick_event.Deactivate();
   }
   else
   {
-    m_command_tick_event->SetIntervalAndSchedule(GPUTicksToSystemTicks(m_pending_command_ticks));
+    m_command_tick_event.SetIntervalAndSchedule(GPUTicksToSystemTicks(m_pending_command_ticks));
   }
 }
 
@@ -1209,7 +1212,7 @@ void GPU::WriteGP1(u32 value)
     case 0x00: // Reset GPU
     {
       DEBUG_LOG("GP1 reset GPU");
-      m_command_tick_event->InvokeEarly();
+      m_command_tick_event.InvokeEarly();
       SynchronizeCRTC();
       SoftReset();
     }
@@ -1218,7 +1221,7 @@ void GPU::WriteGP1(u32 value)
     case 0x01: // Clear FIFO
     {
       DEBUG_LOG("GP1 clear FIFO");
-      m_command_tick_event->InvokeEarly();
+      m_command_tick_event.InvokeEarly();
       SynchronizeCRTC();
 
       // flush partial writes
@@ -1232,7 +1235,7 @@ void GPU::WriteGP1(u32 value)
       m_blit_buffer.clear();
       m_blit_remaining_words = 0;
       m_pending_command_ticks = 0;
-      m_command_tick_event->Deactivate();
+      m_command_tick_event.Deactivate();
       UpdateDMARequest();
       UpdateGPUIdle();
     }
@@ -1350,7 +1353,7 @@ void GPU::WriteGP1(u32 value)
       {
         // Have to be careful when setting this because Synchronize() can modify GPUSTAT.
         static constexpr u32 SET_MASK = UINT32_C(0b00000000011111110100000000000000);
-        m_command_tick_event->InvokeEarly();
+        m_command_tick_event.InvokeEarly();
         SynchronizeCRTC();
         m_GPUSTAT.bits = (m_GPUSTAT.bits & ~SET_MASK) | (new_GPUSTAT.bits & SET_MASK);
         UpdateCRTCConfig();
diff --git a/src/core/gpu.h b/src/core/gpu.h
index 019138b28..42ec451c1 100644
--- a/src/core/gpu.h
+++ b/src/core/gpu.h
@@ -5,6 +5,7 @@
 #include "gpu_types.h"
 #include "timers.h"
 #include "types.h"
+#include "timing_event.h"
 
 #include "util/gpu_texture.h"
 
@@ -29,7 +30,6 @@ class GPUTexture;
 class GPUPipeline;
 
 struct Settings;
-class TimingEvent;
 
 namespace Threading {
 class Thread;
@@ -370,8 +370,8 @@ protected:
     AddCommandTicks(std::max(drawn_width, drawn_height));
   }
 
-  std::unique_ptr<TimingEvent> m_crtc_tick_event;
-  std::unique_ptr<TimingEvent> m_command_tick_event;
+  TimingEvent m_crtc_tick_event;
+  TimingEvent m_command_tick_event;
 
   union GPUSTAT
   {
diff --git a/src/core/justifier.cpp b/src/core/justifier.cpp
index 72df27b00..d623ad43b 100644
--- a/src/core/justifier.cpp
+++ b/src/core/justifier.cpp
@@ -28,16 +28,21 @@ static u32 s_irq_current_line;
 #endif
 
 static constexpr std::array<u8, static_cast<size_t>(Justifier::Binding::ButtonCount)> s_button_indices = {{15, 3, 14}};
+static constexpr std::array<const char*, NUM_CONTROLLER_AND_CARD_PORTS> s_event_names = {
+  {"Justifier IRQ P0", "Justifier IRQ P1", "Justifier IRQ P2", "Justifier IRQ P3", "Justifier IRQ P4",
+   "Justifier IRQ P5", "Justifier IRQ P6", "Justifier IRQ P7"}};
 
-Justifier::Justifier(u32 index) : Controller(index)
+Justifier::Justifier(u32 index)
+  : Controller(index), m_irq_event(
+                         s_event_names[index], 1, 1,
+                         [](void* param, TickCount, TickCount) { static_cast<Justifier*>(param)->IRQEvent(); }, this)
 {
-  m_irq_event = TimingEvents::CreateTimingEvent(
-    "Justifier IRQ", 1, 1, [](void* param, TickCount, TickCount) { static_cast<Justifier*>(param)->IRQEvent(); }, this,
-    false);
 }
 
 Justifier::~Justifier()
 {
+  m_irq_event.Deactivate();
+
   if (!m_cursor_path.empty())
   {
     const u32 cursor_index = GetSoftwarePointerIndex();
@@ -245,7 +250,7 @@ void Justifier::UpdatePosition()
 void Justifier::UpdateIRQEvent()
 {
   // TODO: Avoid deactivate and event sort.
-  m_irq_event->Deactivate();
+  m_irq_event.Deactivate();
 
   if (!m_position_valid)
     return;
@@ -261,7 +266,7 @@ void Justifier::UpdateIRQEvent()
 
   const TickCount ticks_until_pos = g_gpu->GetSystemTicksUntilTicksAndLine(m_irq_tick, target_line);
   DEBUG_LOG("Triggering IRQ in {} ticks @ tick {} line {}", ticks_until_pos, m_irq_tick, target_line);
-  m_irq_event->Schedule(ticks_until_pos);
+  m_irq_event.Schedule(ticks_until_pos);
 }
 
 void Justifier::IRQEvent()
diff --git a/src/core/justifier.h b/src/core/justifier.h
index ec19db955..51895bdcc 100644
--- a/src/core/justifier.h
+++ b/src/core/justifier.h
@@ -2,7 +2,10 @@
 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
 
 #pragma once
+
 #include "controller.h"
+#include "timing_event.h"
+
 #include <memory>
 #include <optional>
 #include <string_view>
@@ -77,8 +80,6 @@ private:
   static constexpr u8 DEFAULT_OFFSCREEN_TRIGGER_FRAMES = 5;
   static constexpr u8 DEFAULT_OFFSCREEN_RELEASE_FRAMES = 5;
 
-  std::unique_ptr<TimingEvent> m_irq_event;
-
   s8 m_first_line_offset = 0;
   s8 m_last_line_offset = 0;
   s16 m_tick_offset = 0;
@@ -98,6 +99,8 @@ private:
 
   TransferState m_transfer_state = TransferState::Idle;
 
+  TimingEvent m_irq_event;
+
   bool m_has_relative_binds = false;
   float m_relative_pos[4] = {};
 
diff --git a/src/core/mdec.cpp b/src/core/mdec.cpp
index 937c8301c..4605157a5 100644
--- a/src/core/mdec.cpp
+++ b/src/core/mdec.cpp
@@ -151,7 +151,7 @@ struct MDECState
   u16 current_q_scale = 0;
 
   alignas(16) std::array<u32, 256> block_rgb{};
-  std::unique_ptr<TimingEvent> block_copy_out_event;
+  TimingEvent block_copy_out_event{"MDEC Block Copy Out", 1, 1, &MDEC::CopyOutBlock, nullptr};
 
   u32 total_blocks_decoded = 0;
 };
@@ -162,20 +162,18 @@ ALIGN_TO_CACHE_LINE static MDECState s_state;
 
 void MDEC::Initialize()
 {
-  s_state.block_copy_out_event =
-    TimingEvents::CreateTimingEvent("MDEC Block Copy Out", 1, 1, &MDEC::CopyOutBlock, nullptr, false);
   s_state.total_blocks_decoded = 0;
   Reset();
 }
 
 void MDEC::Shutdown()
 {
-  s_state.block_copy_out_event.reset();
+  s_state.block_copy_out_event.Deactivate();
 }
 
 void MDEC::Reset()
 {
-  s_state.block_copy_out_event->Deactivate();
+  s_state.block_copy_out_event.Deactivate();
   SoftReset();
 }
 
@@ -211,7 +209,7 @@ bool MDEC::DoState(StateWrapper& sw)
   bool block_copy_out_pending = HasPendingBlockCopyOut();
   sw.Do(&block_copy_out_pending);
   if (sw.IsReading())
-    s_state.block_copy_out_event->SetState(block_copy_out_pending);
+    s_state.block_copy_out_event.SetState(block_copy_out_pending);
 
   return !sw.HasError();
 }
@@ -304,7 +302,7 @@ void MDEC::DMAWrite(const u32* words, u32 word_count)
 
 bool MDEC::HasPendingBlockCopyOut()
 {
-  return s_state.block_copy_out_event->IsActive();
+  return s_state.block_copy_out_event.IsActive();
 }
 
 void MDEC::SoftReset()
@@ -319,7 +317,7 @@ void MDEC::SoftReset()
   s_state.current_block = 0;
   s_state.current_coefficient = 64;
   s_state.current_q_scale = 0;
-  s_state.block_copy_out_event->Deactivate();
+  s_state.block_copy_out_event.Deactivate();
   UpdateStatus();
 }
 
@@ -358,7 +356,7 @@ u32 MDEC::ReadDataRegister()
     if (HasPendingBlockCopyOut())
     {
       DEV_LOG("MDEC data out FIFO empty on read - stalling CPU");
-      CPU::AddPendingTicks(s_state.block_copy_out_event->GetTicksUntilNextExecution());
+      CPU::AddPendingTicks(s_state.block_copy_out_event.GetTicksUntilNextExecution());
     }
     else
     {
@@ -617,13 +615,13 @@ void MDEC::ScheduleBlockCopyOut(TickCount ticks)
   DebugAssert(!HasPendingBlockCopyOut());
   DEBUG_LOG("Scheduling block copy out in {} ticks", ticks);
 
-  s_state.block_copy_out_event->SetIntervalAndSchedule(ticks);
+  s_state.block_copy_out_event.SetIntervalAndSchedule(ticks);
 }
 
 void MDEC::CopyOutBlock(void* param, TickCount ticks, TickCount ticks_late)
 {
   Assert(s_state.state == State::WritingMacroblock);
-  s_state.block_copy_out_event->Deactivate();
+  s_state.block_copy_out_event.Deactivate();
 
   switch (s_state.status.data_output_depth)
   {
diff --git a/src/core/memory_card.cpp b/src/core/memory_card.cpp
index c99451e25..e6dd2b007 100644
--- a/src/core/memory_card.cpp
+++ b/src/core/memory_card.cpp
@@ -22,13 +22,12 @@
 Log_SetChannel(MemoryCard);
 
 MemoryCard::MemoryCard()
+  : m_save_event(
+      "Memory Card Host Flush", GetSaveDelayInTicks(), GetSaveDelayInTicks(),
+      [](void* param, TickCount ticks, TickCount ticks_late) { static_cast<MemoryCard*>(param)->SaveIfChanged(true); },
+      this)
 {
   m_FLAG.no_write_yet = true;
-
-  m_save_event = TimingEvents::CreateTimingEvent(
-    "Memory Card Host Flush", GetSaveDelayInTicks(), GetSaveDelayInTicks(),
-    [](void* param, TickCount ticks, TickCount ticks_late) { static_cast<MemoryCard*>(param)->SaveIfChanged(true); },
-    this, false);
 }
 
 MemoryCard::~MemoryCard()
@@ -285,7 +284,7 @@ bool MemoryCard::Transfer(const u8 data_in, u8* data_out)
 
 bool MemoryCard::IsOrWasRecentlyWriting() const
 {
-  return (m_state == State::WriteData || m_save_event->IsActive());
+  return (m_state == State::WriteData || m_save_event.IsActive());
 }
 
 std::unique_ptr<MemoryCard> MemoryCard::Create()
@@ -324,7 +323,7 @@ bool MemoryCard::LoadFromFile()
 
 bool MemoryCard::SaveIfChanged(bool display_osd_message)
 {
-  m_save_event->Deactivate();
+  m_save_event.Deactivate();
 
   if (!m_changed)
     return true;
@@ -369,9 +368,9 @@ bool MemoryCard::SaveIfChanged(bool display_osd_message)
 void MemoryCard::QueueFileSave()
 {
   // skip if the event is already pending, or we don't have a backing file
-  if (m_save_event->IsActive() || m_filename.empty())
+  if (m_save_event.IsActive() || m_filename.empty())
     return;
 
   // save in one second, that should be long enough for everything to finish writing
-  m_save_event->Schedule(GetSaveDelayInTicks());
+  m_save_event.Schedule(GetSaveDelayInTicks());
 }
diff --git a/src/core/memory_card.h b/src/core/memory_card.h
index 41403219b..80faf3085 100644
--- a/src/core/memory_card.h
+++ b/src/core/memory_card.h
@@ -1,17 +1,19 @@
-// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
 
 #pragma once
-#include "common/bitfield.h"
+
 #include "controller.h"
 #include "memory_card_image.h"
+#include "timing_event.h"
+
+#include "common/bitfield.h"
+
 #include <array>
 #include <memory>
 #include <string>
 #include <string_view>
 
-class TimingEvent;
-
 class MemoryCard final
 {
 public:
@@ -97,8 +99,6 @@ private:
   bool SaveIfChanged(bool display_osd_message);
   void QueueFileSave();
 
-  std::unique_ptr<TimingEvent> m_save_event;
-
   State m_state = State::Idle;
   FLAG m_FLAG = {};
   u16 m_address = 0;
@@ -107,7 +107,8 @@ private:
   u8 m_last_byte = 0;
   bool m_changed = false;
 
-  MemoryCardImage::DataArray m_data{};
-
+  TimingEvent m_save_event;
   std::string m_filename;
+
+  MemoryCardImage::DataArray m_data{};
 };
diff --git a/src/core/pad.cpp b/src/core/pad.cpp
index 300064ce8..ef8b89e52 100644
--- a/src/core/pad.cpp
+++ b/src/core/pad.cpp
@@ -117,7 +117,7 @@ static std::array<std::unique_ptr<MemoryCard>, NUM_CONTROLLER_AND_CARD_PORTS> s_
 
 static std::array<Multitap, NUM_MULTITAPS> s_multitaps;
 
-static std::unique_ptr<TimingEvent> s_transfer_event;
+static TimingEvent s_transfer_event{"Pad Serial Transfer", 1, 1, &Pad::TransferEvent, nullptr};
 static State s_state = State::Idle;
 
 static JOY_CTRL s_JOY_CTRL = {};
@@ -140,7 +140,6 @@ static std::unique_ptr<MemoryCard> s_dummy_card;
 
 void Pad::Initialize()
 {
-  s_transfer_event = TimingEvents::CreateTimingEvent("Pad Serial Transfer", 1, 1, &Pad::TransferEvent, nullptr, false);
   Reset();
 }
 
@@ -148,7 +147,7 @@ void Pad::Shutdown()
 {
   s_memory_card_backup.reset();
 
-  s_transfer_event.reset();
+  s_transfer_event.Deactivate();
 
   for (u32 i = 0; i < NUM_CONTROLLER_AND_CARD_PORTS; i++)
   {
@@ -533,7 +532,7 @@ bool Pad::DoState(StateWrapper& sw, bool is_memory_state)
   sw.Do(&s_transmit_buffer_full);
 
   if (sw.IsReading() && IsTransmitting())
-    s_transfer_event->Activate();
+    s_transfer_event.Activate();
 
   return !sw.HasError();
 }
@@ -581,7 +580,7 @@ u32 Pad::ReadRegister(u32 offset)
     case 0x00: // JOY_DATA
     {
       if (IsTransmitting())
-        s_transfer_event->InvokeEarly();
+        s_transfer_event.InvokeEarly();
 
       const u8 value = s_receive_buffer_full ? s_receive_buffer : 0xFF;
       DEBUG_LOG("JOY_DATA (R) -> 0x{:02X}{}", value, s_receive_buffer_full ? "" : "(EMPTY)");
@@ -595,7 +594,7 @@ u32 Pad::ReadRegister(u32 offset)
     case 0x04: // JOY_STAT
     {
       if (IsTransmitting())
-        s_transfer_event->InvokeEarly();
+        s_transfer_event.InvokeEarly();
 
       const u32 bits = s_JOY_STAT.bits;
       s_JOY_STAT.ACKINPUT = false;
@@ -763,7 +762,7 @@ void Pad::BeginTransfer()
   // until after (4) and (5) have been completed.
 
   s_state = State::Transmitting;
-  s_transfer_event->SetPeriodAndSchedule(GetTransferTicks());
+  s_transfer_event.SetPeriodAndSchedule(GetTransferTicks());
 }
 
 void Pad::DoTransfer(TickCount ticks_late)
@@ -882,7 +881,7 @@ void Pad::DoTransfer(TickCount ticks_late)
     const TickCount ack_timer = GetACKTicks(memcard_transfer);
     DEBUG_LOG("Delaying ACK for {} ticks", ack_timer);
     s_state = State::WaitingForACK;
-    s_transfer_event->SetPeriodAndSchedule(ack_timer);
+    s_transfer_event.SetPeriodAndSchedule(ack_timer);
   }
 
   UpdateJoyStat();
@@ -912,7 +911,7 @@ void Pad::EndTransfer()
   DEBUG_LOG("Ending transfer");
 
   s_state = State::Idle;
-  s_transfer_event->Deactivate();
+  s_transfer_event.Deactivate();
 }
 
 void Pad::ResetDeviceTransferState()
diff --git a/src/core/spu.cpp b/src/core/spu.cpp
index d6b50483a..29bed3992 100644
--- a/src/core/spu.cpp
+++ b/src/core/spu.cpp
@@ -355,8 +355,9 @@ static void CreateOutputStream();
 namespace {
 struct SPUState
 {
-  std::unique_ptr<TimingEvent> s_tick_event;
-  std::unique_ptr<TimingEvent> transfer_event;
+  TimingEvent transfer_event{"SPU Transfer", TRANSFER_TICKS_PER_HALFWORD, TRANSFER_TICKS_PER_HALFWORD,
+                             &SPU::ExecuteTransfer, nullptr};
+  TimingEvent tick_event{"SPU Sample", SYSCLK_TICKS_PER_SPU_TICK, SYSCLK_TICKS_PER_SPU_TICK, &SPU::Execute, nullptr};
 
   TickCount ticks_carry = 0;
   TickCount cpu_ticks_per_spu_tick = 0;
@@ -426,10 +427,8 @@ void SPU::Initialize()
   // (X * D) / N / 768 -> (X * D) / (N * 768)
   s_state.cpu_ticks_per_spu_tick = System::ScaleTicksToOverclock(SYSCLK_TICKS_PER_SPU_TICK);
   s_state.cpu_tick_divider = static_cast<TickCount>(g_settings.cpu_overclock_numerator * SYSCLK_TICKS_PER_SPU_TICK);
-  s_state.s_tick_event = TimingEvents::CreateTimingEvent("SPU Sample", s_state.cpu_ticks_per_spu_tick,
-                                                         s_state.cpu_ticks_per_spu_tick, &SPU::Execute, nullptr, false);
-  s_state.transfer_event = TimingEvents::CreateTimingEvent(
-    "SPU Transfer", TRANSFER_TICKS_PER_HALFWORD, TRANSFER_TICKS_PER_HALFWORD, &SPU::ExecuteTransfer, nullptr, false);
+  s_state.tick_event.SetInterval(s_state.cpu_ticks_per_spu_tick);
+  s_state.tick_event.SetPeriod(s_state.cpu_ticks_per_spu_tick);
   s_state.null_audio_stream = AudioStream::CreateNullStream(SAMPLE_RATE, g_settings.audio_stream_parameters.buffer_ms);
 
   CreateOutputStream();
@@ -482,8 +481,8 @@ void SPU::CPUClockChanged()
 void SPU::Shutdown()
 {
   StopDumpingAudio();
-  s_state.s_tick_event.reset();
-  s_state.transfer_event.reset();
+  s_state.tick_event.Deactivate();
+  s_state.transfer_event.Deactivate();
   s_state.audio_stream.reset();
 }
 
@@ -539,8 +538,8 @@ void SPU::Reset()
     v.ignore_loop_address = false;
   }
 
-  s_state.s_tick_event->Deactivate();
-  s_state.transfer_event->Deactivate();
+  s_state.tick_event.Deactivate();
+  s_state.transfer_event.Deactivate();
   s_state.transfer_fifo.Clear();
   s_ram.fill(0);
   UpdateEventInterval();
@@ -885,7 +884,7 @@ void SPU::WriteRegister(u32 offset, u16 value)
     case 0x1F801DA6 - SPU_BASE:
     {
       DEBUG_LOG("SPU transfer address register <- 0x{:04X}", value);
-      s_state.transfer_event->InvokeEarly();
+      s_state.transfer_event.InvokeEarly();
       s_state.transfer_address_reg = value;
       s_state.transfer_address = ZeroExtend32(value) * 8;
       if (IsRAMIRQTriggerable() && CheckRAMIRQ(s_state.transfer_address))
@@ -1283,14 +1282,14 @@ void SPU::ExecuteTransfer(void* param, TickCount ticks, TickCount ticks_late)
     if (s_state.transfer_fifo.IsFull())
     {
       s_state.SPUSTAT.transfer_busy = false;
-      s_state.transfer_event->Deactivate();
+      s_state.transfer_event.Deactivate();
       return;
     }
 
     s_state.SPUSTAT.transfer_busy = true;
     const TickCount ticks_until_complete =
       TickCount(s_state.transfer_fifo.GetSpace() * u32(TRANSFER_TICKS_PER_HALFWORD)) + ((ticks < 0) ? -ticks : 0);
-    s_state.transfer_event->Schedule(ticks_until_complete);
+    s_state.transfer_event.Schedule(ticks_until_complete);
   }
   else
   {
@@ -1307,14 +1306,14 @@ void SPU::ExecuteTransfer(void* param, TickCount ticks, TickCount ticks_late)
     if (s_state.transfer_fifo.IsEmpty())
     {
       s_state.SPUSTAT.transfer_busy = false;
-      s_state.transfer_event->Deactivate();
+      s_state.transfer_event.Deactivate();
       return;
     }
 
     s_state.SPUSTAT.transfer_busy = true;
     const TickCount ticks_until_complete =
       TickCount(s_state.transfer_fifo.GetSize() * u32(TRANSFER_TICKS_PER_HALFWORD)) + ((ticks < 0) ? -ticks : 0);
-    s_state.transfer_event->Schedule(ticks_until_complete);
+    s_state.transfer_event.Schedule(ticks_until_complete);
   }
 }
 
@@ -1343,26 +1342,26 @@ void SPU::UpdateTransferEvent()
   const RAMTransferMode mode = s_state.SPUCNT.ram_transfer_mode;
   if (mode == RAMTransferMode::Stopped)
   {
-    s_state.transfer_event->Deactivate();
+    s_state.transfer_event.Deactivate();
   }
   else if (mode == RAMTransferMode::DMARead)
   {
     // transfer event fills the fifo
     if (s_state.transfer_fifo.IsFull())
-      s_state.transfer_event->Deactivate();
-    else if (!s_state.transfer_event->IsActive())
-      s_state.transfer_event->Schedule(TickCount(s_state.transfer_fifo.GetSpace() * u32(TRANSFER_TICKS_PER_HALFWORD)));
+      s_state.transfer_event.Deactivate();
+    else if (!s_state.transfer_event.IsActive())
+      s_state.transfer_event.Schedule(TickCount(s_state.transfer_fifo.GetSpace() * u32(TRANSFER_TICKS_PER_HALFWORD)));
   }
   else
   {
     // transfer event copies from fifo to ram
     if (s_state.transfer_fifo.IsEmpty())
-      s_state.transfer_event->Deactivate();
-    else if (!s_state.transfer_event->IsActive())
-      s_state.transfer_event->Schedule(TickCount(s_state.transfer_fifo.GetSize() * u32(TRANSFER_TICKS_PER_HALFWORD)));
+      s_state.transfer_event.Deactivate();
+    else if (!s_state.transfer_event.IsActive())
+      s_state.transfer_event.Schedule(TickCount(s_state.transfer_fifo.GetSize() * u32(TRANSFER_TICKS_PER_HALFWORD)));
   }
 
-  s_state.SPUSTAT.transfer_busy = s_state.transfer_event->IsActive();
+  s_state.SPUSTAT.transfer_busy = s_state.transfer_event.IsActive();
 }
 
 void SPU::UpdateDMARequest()
@@ -1480,10 +1479,10 @@ void SPU::DMAWrite(const u32* words, u32 word_count)
 
 void SPU::GeneratePendingSamples()
 {
-  if (s_state.transfer_event->IsActive())
-    s_state.transfer_event->InvokeEarly();
+  if (s_state.transfer_event.IsActive())
+    s_state.transfer_event.InvokeEarly();
 
-  const TickCount ticks_pending = s_state.s_tick_event->GetTicksSinceLastExecution();
+  const TickCount ticks_pending = s_state.tick_event.GetTicksSinceLastExecution();
   TickCount frames_to_execute;
   if (g_settings.cpu_overclock_active)
   {
@@ -1494,11 +1493,11 @@ void SPU::GeneratePendingSamples()
   else
   {
     frames_to_execute =
-      (s_state.s_tick_event->GetTicksSinceLastExecution() + s_state.ticks_carry) / SYSCLK_TICKS_PER_SPU_TICK;
+      (s_state.tick_event.GetTicksSinceLastExecution() + s_state.ticks_carry) / SYSCLK_TICKS_PER_SPU_TICK;
   }
 
   const bool force_exec = (frames_to_execute > 0);
-  s_state.s_tick_event->InvokeEarly(force_exec);
+  s_state.tick_event.InvokeEarly(force_exec);
 }
 
 bool SPU::IsDumpingAudio()
@@ -2446,18 +2445,18 @@ void SPU::UpdateEventInterval()
   // TODO: Make this predict how long until the interrupt will be hit instead...
   const u32 interval = (s_state.SPUCNT.enable && s_state.SPUCNT.irq9_enable) ? 1 : max_slice_frames;
   const TickCount interval_ticks = static_cast<TickCount>(interval) * s_state.cpu_ticks_per_spu_tick;
-  if (s_state.s_tick_event->IsActive() && s_state.s_tick_event->GetInterval() == interval_ticks)
+  if (s_state.tick_event.IsActive() && s_state.tick_event.GetInterval() == interval_ticks)
     return;
 
   // Ensure all pending ticks have been executed, since we won't get them back after rescheduling.
-  s_state.s_tick_event->InvokeEarly(true);
-  s_state.s_tick_event->SetInterval(interval_ticks);
+  s_state.tick_event.InvokeEarly(true);
+  s_state.tick_event.SetInterval(interval_ticks);
 
   TickCount downcount = interval_ticks;
   if (!g_settings.cpu_overclock_active)
     downcount -= s_state.ticks_carry;
 
-  s_state.s_tick_event->Schedule(downcount);
+  s_state.tick_event.Schedule(downcount);
 }
 
 void SPU::DrawDebugStateWindow()
@@ -2534,7 +2533,7 @@ void SPU::DrawDebugStateWindow()
 
     ImGui::Text("Transfer FIFO: ");
     ImGui::SameLine(offsets[0]);
-    ImGui::TextColored(s_state.transfer_event->IsActive() ? active_color : inactive_color, "%u halfwords (%u bytes)",
+    ImGui::TextColored(s_state.transfer_event.IsActive() ? active_color : inactive_color, "%u halfwords (%u bytes)",
                        s_state.transfer_fifo.GetSize(), s_state.transfer_fifo.GetSize() * 2);
   }
 
diff --git a/src/core/timers.cpp b/src/core/timers.cpp
index b1e7ae4ae..3dc0ff66d 100644
--- a/src/core/timers.cpp
+++ b/src/core/timers.cpp
@@ -75,7 +75,7 @@ static void UpdateSysClkEvent();
 namespace {
 struct TimersState
 {
-  std::unique_ptr<TimingEvent> sysclk_event;
+  TimingEvent sysclk_event{ "Timer SysClk Interrupt", 1, 1, &Timers::AddSysClkTicks, nullptr };
 
   std::array<CounterState, NUM_TIMERS> counters{};
   TickCount sysclk_ticks_carry = 0; // 0 unless overclocking is enabled
@@ -88,14 +88,12 @@ ALIGN_TO_CACHE_LINE static TimersState s_state;
 
 void Timers::Initialize()
 {
-  s_state.sysclk_event =
-    TimingEvents::CreateTimingEvent("Timer SysClk Interrupt", 1, 1, &Timers::AddSysClkTicks, nullptr, false);
   Reset();
 }
 
 void Timers::Shutdown()
 {
-  s_state.sysclk_event.reset();
+  s_state.sysclk_event.Deactivate();
 }
 
 void Timers::Reset()
@@ -112,7 +110,7 @@ void Timers::Reset()
     cs.irq_done = false;
   }
 
-  s_state.sysclk_event->Deactivate();
+  s_state.sysclk_event.Deactivate();
   s_state.sysclk_ticks_carry = 0;
   s_state.sysclk_div_8_carry = 0;
   UpdateSysClkEvent();
@@ -176,7 +174,7 @@ void Timers::SetGate(u32 timer, bool state)
   // Because the gate prevents counting in or outside of the gate, we need a correct counter.
   // For reset, we _can_ skip it, until the gate clears.
   if (!cs.use_external_clock && (cs.mode.sync_mode != SyncMode::ResetOnGateEnd || !state))
-    s_state.sysclk_event->InvokeEarly();
+    s_state.sysclk_event.InvokeEarly();
 
   switch (cs.mode.sync_mode)
   {
@@ -321,7 +319,7 @@ u32 Timers::ReadRegister(u32 offset)
           g_gpu->SynchronizeCRTC();
       }
 
-      s_state.sysclk_event->InvokeEarly();
+      s_state.sysclk_event.InvokeEarly();
 
       return cs.counter;
     }
@@ -335,7 +333,7 @@ u32 Timers::ReadRegister(u32 offset)
           g_gpu->SynchronizeCRTC();
       }
 
-      s_state.sysclk_event->InvokeEarly();
+      s_state.sysclk_event.InvokeEarly();
 
       const u32 bits = cs.mode.bits;
       cs.mode.reached_overflow = false;
@@ -371,7 +369,7 @@ void Timers::WriteRegister(u32 offset, u32 value)
       g_gpu->SynchronizeCRTC();
   }
 
-  s_state.sysclk_event->InvokeEarly();
+  s_state.sysclk_event.InvokeEarly();
 
   // Strictly speaking these IRQ checks should probably happen on the next tick.
   switch (port_offset)
@@ -488,7 +486,7 @@ TickCount Timers::GetTicksUntilNextInterrupt()
 
 void Timers::UpdateSysClkEvent()
 {
-  s_state.sysclk_event->Schedule(GetTicksUntilNextInterrupt());
+  s_state.sysclk_event.Schedule(GetTicksUntilNextInterrupt());
 }
 
 void Timers::DrawDebugStateWindow()
diff --git a/src/core/timing_event.cpp b/src/core/timing_event.cpp
index 2baab9a59..542dea66c 100644
--- a/src/core/timing_event.cpp
+++ b/src/core/timing_event.cpp
@@ -60,18 +60,6 @@ void TimingEvents::Shutdown()
   Assert(s_state.active_event_count == 0);
 }
 
-std::unique_ptr<TimingEvent> TimingEvents::CreateTimingEvent(std::string name, TickCount period, TickCount interval,
-                                                             TimingEventCallback callback, void* callback_param,
-                                                             bool activate)
-{
-  std::unique_ptr<TimingEvent> event =
-    std::make_unique<TimingEvent>(std::move(name), period, interval, callback, callback_param);
-  if (activate)
-    event->Activate();
-
-  return event;
-}
-
 void TimingEvents::UpdateCPUDowncount()
 {
   const u32 event_downcount = s_state.active_events_head->GetDowncount();
@@ -377,7 +365,7 @@ bool TimingEvents::DoState(StateWrapper& sw)
 
     for (u32 i = 0; i < event_count; i++)
     {
-      std::string event_name;
+      TinyString event_name;
       TickCount downcount, time_since_last_run, period, interval;
       sw.Do(&event_name);
       sw.Do(&downcount);
@@ -430,17 +418,16 @@ bool TimingEvents::DoState(StateWrapper& sw)
   return !sw.HasError();
 }
 
-TimingEvent::TimingEvent(std::string name, TickCount period, TickCount interval, TimingEventCallback callback,
-                         void* callback_param)
+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(std::move(name))
+    m_period(period), m_interval(interval), m_name(name)
 {
 }
 
 TimingEvent::~TimingEvent()
 {
-  if (m_active)
-    TimingEvents::RemoveActiveEvent(this);
+  DebugAssert(!m_active);
 }
 
 TickCount TimingEvent::GetTicksSinceLastExecution() const
diff --git a/src/core/timing_event.h b/src/core/timing_event.h
index 01733ba51..4e3b9f116 100644
--- a/src/core/timing_event.h
+++ b/src/core/timing_event.h
@@ -1,14 +1,12 @@
-// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
+// SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
 
 #pragma once
-#include <functional>
-#include <memory>
-#include <string>
-#include <vector>
 
 #include "types.h"
 
+#include <string_view>
+
 class StateWrapper;
 
 // Event callback type. Second parameter is the number of cycles the event was executed "late".
@@ -17,11 +15,11 @@ using TimingEventCallback = void (*)(void* param, TickCount ticks, TickCount tic
 class TimingEvent
 {
 public:
-  TimingEvent(std::string name, TickCount period, TickCount interval, TimingEventCallback callback,
+  TimingEvent(const std::string_view name, TickCount period, TickCount interval, TimingEventCallback callback,
               void* callback_param);
   ~TimingEvent();
 
-  ALWAYS_INLINE const std::string& GetName() const { return m_name; }
+  ALWAYS_INLINE const std::string_view GetName() const { return m_name; }
   ALWAYS_INLINE bool IsActive() const { return m_active; }
 
   // Returns the number of ticks between each event.
@@ -75,7 +73,7 @@ public:
   TickCount m_interval;
   bool m_active = false;
 
-  std::string m_name;
+  std::string_view m_name;
 };
 
 namespace TimingEvents {
@@ -87,11 +85,6 @@ void Initialize();
 void Reset();
 void Shutdown();
 
-/// Creates a new event.
-std::unique_ptr<TimingEvent> CreateTimingEvent(std::string name, TickCount period, TickCount interval,
-                                               TimingEventCallback callback, void* callback_param, bool activate);
-
-/// Serialization.
 bool DoState(StateWrapper& sw);
 
 bool IsRunningEvents();
diff --git a/src/util/state_wrapper.cpp b/src/util/state_wrapper.cpp
index dba2fb9c5..f221c75db 100644
--- a/src/util/state_wrapper.cpp
+++ b/src/util/state_wrapper.cpp
@@ -77,6 +77,14 @@ void StateWrapper::Do(SmallStringBase* value_ptr)
   value_ptr->update_size();
 }
 
+void StateWrapper::Do(std::string_view* value_ptr)
+{
+  Assert(m_mode == Mode::Write);
+  u32 length = static_cast<u32>(value_ptr->length());
+  Do(&length);
+  DoBytes(const_cast<char*>(value_ptr->data()), length);
+}
+
 bool StateWrapper::DoMarker(const char* marker)
 {
   SmallString file_value(marker);
diff --git a/src/util/state_wrapper.h b/src/util/state_wrapper.h
index 2a473262b..91812de66 100644
--- a/src/util/state_wrapper.h
+++ b/src/util/state_wrapper.h
@@ -108,6 +108,7 @@ public:
 
   void Do(bool* value_ptr);
   void Do(std::string* value_ptr);
+  void Do(std::string_view* value_ptr);
   void Do(SmallStringBase* value_ptr);
 
   template<typename T, size_t N>