diff --git a/include/cpu_dynarmic.hpp b/include/cpu_dynarmic.hpp
index 1ec50d9e..97acddd0 100644
--- a/include/cpu_dynarmic.hpp
+++ b/include/cpu_dynarmic.hpp
@@ -126,7 +126,7 @@ class CPU {
 	Emulator& emu;
 
   public:
-    static constexpr u64 ticksPerSec = 268111856;
+    static constexpr u64 ticksPerSec = Scheduler::arm11Clock;
 
     CPU(Memory& mem, Kernel& kernel, Emulator& emu);
     void reset();
@@ -175,6 +175,10 @@ class CPU {
         return scheduler.currentTimestamp;
     }
 
+	Scheduler& getScheduler() {
+		return scheduler;
+	}
+
     void clearCache() { jit->ClearCache(); }
     void runFrame();
 };
\ No newline at end of file
diff --git a/include/kernel/kernel.hpp b/include/kernel/kernel.hpp
index 3f09bf12..217dd148 100644
--- a/include/kernel/kernel.hpp
+++ b/include/kernel/kernel.hpp
@@ -70,6 +70,7 @@ public:
 	Handle makeMutex(bool locked = false); // Needs to be public to be accessible to the APT/DSP services
 	Handle makeSemaphore(u32 initialCount, u32 maximumCount); // Needs to be public to be accessible to the service manager port
 	Handle makeTimer(ResetType resetType);
+	void pollTimers();
 
 	// Signals an event, returns true on success or false if the event does not exist
 	bool signalEvent(Handle e);
@@ -94,7 +95,6 @@ public:
 	void releaseMutex(Mutex* moo);
 	void cancelTimer(Timer* timer);
 	void signalTimer(Handle timerHandle, Timer* timer);
-	void updateTimer(Handle timerHandle, Timer* timer);
 
 	// Wake up the thread with the highest priority out of all threads in the waitlist
 	// Returns the index of the woken up thread
diff --git a/include/kernel/kernel_types.hpp b/include/kernel/kernel_types.hpp
index 53c60774..79684e17 100644
--- a/include/kernel/kernel_types.hpp
+++ b/include/kernel/kernel_types.hpp
@@ -177,13 +177,12 @@ struct Timer {
 	u64 waitlist;  // Refer to the getWaitlist function below for documentation
 	ResetType resetType = ResetType::OneShot;
 
-	u64 startTick;     // CPU tick the timer started
-	u64 currentDelay;  // Number of ns until the timer fires next time
+	u64 fireTick;      // CPU tick the timer will be fired
 	u64 interval;      // Number of ns until the timer fires for the second and future times
 	bool fired;        // Has this timer been signalled?
 	bool running;      // Is this timer running or stopped?
 
-	Timer(ResetType type) : resetType(type), startTick(0), currentDelay(0), interval(0), waitlist(0), fired(false), running(false) {}
+	Timer(ResetType type) : resetType(type), fireTick(0), interval(0), waitlist(0), fired(false), running(false) {}
 };
 
 struct MemoryBlock {
diff --git a/include/scheduler.hpp b/include/scheduler.hpp
index fb097618..69c91f06 100644
--- a/include/scheduler.hpp
+++ b/include/scheduler.hpp
@@ -5,14 +5,17 @@
 #include <ranges>
 
 #include "helpers.hpp"
+#include "logger.hpp"
 
 struct Scheduler {
 	enum class EventType {
 		VBlank = 0,          // End of frame event
-		Panic = 1,           // Dummy event that is always pending and should never be triggered (Timestamp = UINT64_MAX)
+		UpdateTimers = 1,    // Update kernel timer objects
+		Panic = 2,           // Dummy event that is always pending and should never be triggered (Timestamp = UINT64_MAX)
 		TotalNumberOfEvents  // How many event types do we have in total?
 	};
 	static constexpr usize totalNumberOfEvents = static_cast<usize>(EventType::TotalNumberOfEvents);
+	static constexpr u64 arm11Clock = 268111856;
 
 	template <typename Key, typename Val, usize size>
 	using EventMap = boost::container::flat_multimap<Key, Val, std::less<Key>, boost::container::static_vector<std::pair<Key, Val>, size>>;
@@ -46,4 +49,37 @@ struct Scheduler {
 		// Add a dummy event to always keep the scheduler non-empty
 		addEvent(EventType::Panic, std::numeric_limits<u64>::max());
 	}
+
+  private:
+	static constexpr u64 MAX_VALUE_TO_MULTIPLY = std::numeric_limits<s64>::max() / arm11Clock;
+
+  public:
+	// Function for converting time units to cycles for various kernel functions
+	// Thank you Citra
+	static constexpr s64 nsToCycles(float ns) { return s64(arm11Clock * (0.000000001f) * ns); }
+	static constexpr s64 nsToCycles(int ns) { return arm11Clock * s64(ns) / 1000000000; }
+
+	static constexpr s64 nsToCycles(s64 ns) {
+		if (ns / 1000000000 > static_cast<s64>(MAX_VALUE_TO_MULTIPLY)) {
+			return std::numeric_limits<s64>::max();
+		}
+
+		if (ns > static_cast<s64>(MAX_VALUE_TO_MULTIPLY)) {
+			return arm11Clock * (ns / 1000000000);
+		}
+
+		return (arm11Clock * ns) / 1000000000;
+	}
+
+	static constexpr s64 nsToCycles(u64 ns) {
+		if (ns / 1000000000 > MAX_VALUE_TO_MULTIPLY) {
+			return std::numeric_limits<s64>::max();
+		}
+
+		if (ns > MAX_VALUE_TO_MULTIPLY) {
+			return arm11Clock * (s64(ns) / 1000000000);
+		}
+
+		return (arm11Clock * s64(ns)) / 1000000000;
+	}
 };
\ No newline at end of file
diff --git a/src/core/CPU/cpu_dynarmic.cpp b/src/core/CPU/cpu_dynarmic.cpp
index f373dc06..94ce335e 100644
--- a/src/core/CPU/cpu_dynarmic.cpp
+++ b/src/core/CPU/cpu_dynarmic.cpp
@@ -36,7 +36,7 @@ void CPU::runFrame() {
 
 	while (!emu.frameDone) {
 		// Run CPU until the next scheduler event
-		env.ticksLeft = scheduler.nextTimestamp;
+		env.ticksLeft = scheduler.nextTimestamp - scheduler.currentTimestamp;
 
 	execute:
 		const auto exitReason = jit->Run();
@@ -54,6 +54,8 @@ void CPU::runFrame() {
 			}
 		}
 	}
+
+	printf("CPU END!\n");
 }
 
 #endif  // CPU_DYNARMIC
\ No newline at end of file
diff --git a/src/core/kernel/timers.cpp b/src/core/kernel/timers.cpp
index a9c95292..f245f9dc 100644
--- a/src/core/kernel/timers.cpp
+++ b/src/core/kernel/timers.cpp
@@ -1,5 +1,8 @@
-#include "kernel.hpp"
+#include <limits>
+
 #include "cpu.hpp"
+#include "kernel.hpp"
+#include "scheduler.hpp"
 
 Handle Kernel::makeTimer(ResetType type) {
 	Handle ret = makeObject(KernelObjectType::Timer);
@@ -13,30 +16,48 @@ Handle Kernel::makeTimer(ResetType type) {
 	return ret;
 }
 
-void Kernel::updateTimer(Handle handle, Timer* timer) {
-	if (timer->running) {
-		const u64 currentTicks = cpu.getTicks();
-		u64 elapsedTicks = currentTicks - timer->startTick;
+void Kernel::pollTimers() {
+	u64 currentTick = cpu.getTicks();
 
-		constexpr double ticksPerSec = double(CPU::ticksPerSec);
-		constexpr double nsPerTick = ticksPerSec / 1000000000.0;
-		const s64 elapsedNs = s64(double(elapsedTicks) * nsPerTick);
+	// Find the next timestamp we'll poll KTimers on. To do this, we find the minimum tick one of our timers will fire
+	u64 nextTimestamp = std::numeric_limits<u64>::max();
+	// Do we have any active timers anymore? If not, then we won't need to schedule a new timer poll event
+	bool haveActiveTimers = false;
 
-		// Timer has fired
-		if (elapsedNs >= timer->currentDelay) {
-			timer->startTick = currentTicks;
-			timer->currentDelay = timer->interval;
-			signalTimer(handle, timer);
+	for (auto handle : timerHandles) {
+		KernelObject* object = getObject(handle, KernelObjectType::Timer);
+		if (object != nullptr) {
+			Timer* timer = object->getData<Timer>();
+
+			if (timer->running) {
+				// If timer has fired, signal it and set the tick it will next time
+				if (currentTick >= timer->fireTick) {
+					signalTimer(handle, timer);
+				}
+
+				// Update our next timer fire timestamp and mark that we should schedule a new event to poll timers
+				// We recheck timer->running because signalling a timer stops it if interval == 0
+				if (timer->running) {
+					nextTimestamp = std::min<u64>(nextTimestamp, timer->fireTick);
+					haveActiveTimers = true;
+				}
+			}
 		}
 	}
+
+	// If we still have active timers, schedule next poll event
+	if (haveActiveTimers) {
+		Scheduler& scheduler = cpu.getScheduler();
+		scheduler.addEvent(Scheduler::EventType::UpdateTimers, nextTimestamp);
+	}
 }
 
 void Kernel::cancelTimer(Timer* timer) {
 	timer->running = false;
-	// TODO: When we have a scheduler this should properly cancel timer events in the scheduler
 }
 
 void Kernel::signalTimer(Handle timerHandle, Timer* timer) {
+	printf("DEEPFRIED\n");
 	timer->fired = true;
 	requireReschedule();
 
@@ -54,6 +75,8 @@ void Kernel::signalTimer(Handle timerHandle, Timer* timer) {
 
 	if (timer->interval == 0) {
 		cancelTimer(timer);
+	} else {
+		timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(timer->interval);
 	}
 }
 
@@ -87,18 +110,20 @@ void Kernel::svcSetTimer() {
 
 	Timer* timer = object->getData<Timer>();
 	cancelTimer(timer);
-	timer->currentDelay = initial;
 	timer->interval = interval;
 	timer->running = true;
-	timer->startTick = cpu.getTicks();
+	timer->fireTick = cpu.getTicks() + Scheduler::nsToCycles(initial);
+	
+	Scheduler& scheduler = cpu.getScheduler();
+	// Signal an event to poll timers as soon as possible
+	scheduler.removeEvent(Scheduler::EventType::UpdateTimers);
+	scheduler.addEvent(Scheduler::EventType::UpdateTimers, cpu.getTicks() + 1);
 
 	// If the initial delay is 0 then instantly signal the timer
 	if (initial == 0) {
 		signalTimer(handle, timer);
-	} else {
-		// This should schedule an event in the scheduler when we have one
 	}
-	
+
 	regs[0] = Result::Success;
 }
 
diff --git a/src/emulator.cpp b/src/emulator.cpp
index 46efd209..058fe868 100644
--- a/src/emulator.cpp
+++ b/src/emulator.cpp
@@ -120,7 +120,7 @@ void Emulator::pollScheduler() {
 	auto& events = scheduler.events;
 
 	// Pop events until there's none pending anymore
-	while (scheduler.currentTimestamp > scheduler.nextTimestamp) {
+	while (scheduler.currentTimestamp >= scheduler.nextTimestamp) {
 		// Read event timestamp and type, pop it from the scheduler and handle it
 		auto [time, eventType] = std::move(*events.begin());
 		events.erase(events.begin());
@@ -129,6 +129,7 @@ void Emulator::pollScheduler() {
 
 		switch (eventType) {
 			case Scheduler::EventType::VBlank: {
+				printf("VBLANK!!!!!!\n");
 				// Signal that we've reached the end of a frame
 				frameDone = true;
 				lua.signalEvent(LuaEvent::Frame);
@@ -143,6 +144,8 @@ void Emulator::pollScheduler() {
 				break;
 			}
 
+			case Scheduler::EventType::UpdateTimers: kernel.pollTimers(); break;
+
 			default: {
 				Helpers::panic("Scheduler: Unimplemented event type received: %d\n", static_cast<int>(eventType));
 				break;