diff --git a/src/common/thread.h b/src/common/thread.h
index 1552f58e0..e17a7850f 100644
--- a/src/common/thread.h
+++ b/src/common/thread.h
@@ -54,6 +54,10 @@ public:
         is_set = false;
     }
 
+    [[nodiscard]] bool IsSet() {
+        return is_set;
+    }
+
 private:
     std::condition_variable condvar;
     std::mutex mutex;
diff --git a/src/core/core_timing.cpp b/src/core/core_timing.cpp
index 5375a5d59..f6c4567ba 100644
--- a/src/core/core_timing.cpp
+++ b/src/core/core_timing.cpp
@@ -134,13 +134,17 @@ void CoreTiming::ScheduleLoopingEvent(std::chrono::nanoseconds start_time,
                                       std::chrono::nanoseconds resched_time,
                                       const std::shared_ptr<EventType>& event_type,
                                       std::uintptr_t user_data, bool absolute_time) {
-    std::scoped_lock scope{basic_lock};
-    const auto next_time{absolute_time ? start_time : GetGlobalTimeNs() + start_time};
+    {
+        std::scoped_lock scope{basic_lock};
+        const auto next_time{absolute_time ? start_time : GetGlobalTimeNs() + start_time};
 
-    event_queue.emplace_back(
-        Event{next_time.count(), event_fifo_id++, user_data, event_type, resched_time.count()});
+        event_queue.emplace_back(
+            Event{next_time.count(), event_fifo_id++, user_data, event_type, resched_time.count()});
 
-    std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>());
+        std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>());
+    }
+
+    event.Set();
 }
 
 void CoreTiming::UnscheduleEvent(const std::shared_ptr<EventType>& event_type,
@@ -229,17 +233,17 @@ std::optional<s64> CoreTiming::Advance() {
             basic_lock.lock();
 
             if (evt.reschedule_time != 0) {
-                // If this event was scheduled into a pause, its time now is going to be way behind.
-                // Re-set this event to continue from the end of the pause.
-                auto next_time{evt.time + evt.reschedule_time};
-                if (evt.time < pause_end_time) {
-                    next_time = pause_end_time + evt.reschedule_time;
-                }
-
                 const auto next_schedule_time{new_schedule_time.has_value()
                                                   ? new_schedule_time.value().count()
                                                   : evt.reschedule_time};
 
+                // If this event was scheduled into a pause, its time now is going to be way behind.
+                // Re-set this event to continue from the end of the pause.
+                auto next_time{evt.time + next_schedule_time};
+                if (evt.time < pause_end_time) {
+                    next_time = pause_end_time + next_schedule_time;
+                }
+
                 event_queue.emplace_back(
                     Event{next_time, event_fifo_id++, evt.user_data, evt.type, next_schedule_time});
                 std::push_heap(event_queue.begin(), event_queue.end(), std::greater<>());
@@ -250,8 +254,7 @@ std::optional<s64> CoreTiming::Advance() {
     }
 
     if (!event_queue.empty()) {
-        const s64 next_time = event_queue.front().time - global_timer;
-        return next_time;
+        return event_queue.front().time;
     } else {
         return std::nullopt;
     }
@@ -264,11 +267,29 @@ void CoreTiming::ThreadLoop() {
             paused_set = false;
             const auto next_time = Advance();
             if (next_time) {
-                if (*next_time > 0) {
-                    std::chrono::nanoseconds next_time_ns = std::chrono::nanoseconds(*next_time);
-                    event.WaitFor(next_time_ns);
+                // There are more events left in the queue, wait until the next event.
+                const auto wait_time = *next_time - GetGlobalTimeNs().count();
+                if (wait_time > 0) {
+                    // Assume a timer resolution of 1ms.
+                    static constexpr s64 TimerResolutionNS = 1000000;
+
+                    // Sleep in discrete intervals of the timer resolution, and spin the rest.
+                    const auto sleep_time = wait_time - (wait_time % TimerResolutionNS);
+                    if (sleep_time > 0) {
+                        event.WaitFor(std::chrono::nanoseconds(sleep_time));
+                    }
+
+                    while (!paused && !event.IsSet() && GetGlobalTimeNs().count() < *next_time) {
+                        // Yield to reduce thread starvation.
+                        std::this_thread::yield();
+                    }
+
+                    if (event.IsSet()) {
+                        event.Reset();
+                    }
                 }
             } else {
+                // Queue is empty, wait until another event is scheduled and signals us to continue.
                 wait_set = true;
                 event.Wait();
             }
diff --git a/src/core/hle/service/nvflinger/nvflinger.cpp b/src/core/hle/service/nvflinger/nvflinger.cpp
index 5574269eb..9b382bf56 100644
--- a/src/core/hle/service/nvflinger/nvflinger.cpp
+++ b/src/core/hle/service/nvflinger/nvflinger.cpp
@@ -38,20 +38,16 @@ void NVFlinger::SplitVSync(std::stop_token stop_token) {
 
     Common::SetCurrentThreadName(name.c_str());
     Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
-    s64 delay = 0;
+
     while (!stop_token.stop_requested()) {
+        vsync_signal.wait(false);
+        vsync_signal.store(false);
+
         guard->lock();
-        const s64 time_start = system.CoreTiming().GetGlobalTimeNs().count();
+
         Compose();
-        const auto ticks = GetNextTicks();
-        const s64 time_end = system.CoreTiming().GetGlobalTimeNs().count();
-        const s64 time_passed = time_end - time_start;
-        const s64 next_time = std::max<s64>(0, ticks - time_passed - delay);
+
         guard->unlock();
-        if (next_time > 0) {
-            std::this_thread::sleep_for(std::chrono::nanoseconds{next_time});
-        }
-        delay = (system.CoreTiming().GetGlobalTimeNs().count() - time_end) - next_time;
     }
 }
 
@@ -66,27 +62,41 @@ NVFlinger::NVFlinger(Core::System& system_, HosBinderDriverServer& hos_binder_dr
     guard = std::make_shared<std::mutex>();
 
     // Schedule the screen composition events
-    composition_event = Core::Timing::CreateEvent(
+    multi_composition_event = Core::Timing::CreateEvent(
+        "ScreenComposition",
+        [this](std::uintptr_t, s64 time,
+               std::chrono::nanoseconds ns_late) -> std::optional<std::chrono::nanoseconds> {
+            vsync_signal.store(true);
+            vsync_signal.notify_all();
+            return std::chrono::nanoseconds(GetNextTicks());
+        });
+
+    single_composition_event = Core::Timing::CreateEvent(
         "ScreenComposition",
         [this](std::uintptr_t, s64 time,
                std::chrono::nanoseconds ns_late) -> std::optional<std::chrono::nanoseconds> {
             const auto lock_guard = Lock();
             Compose();
 
-            return std::max(std::chrono::nanoseconds::zero(),
-                            std::chrono::nanoseconds(GetNextTicks()) - ns_late);
+            return std::chrono::nanoseconds(GetNextTicks());
         });
 
     if (system.IsMulticore()) {
+        system.CoreTiming().ScheduleLoopingEvent(frame_ns, frame_ns, multi_composition_event);
         vsync_thread = std::jthread([this](std::stop_token token) { SplitVSync(token); });
     } else {
-        system.CoreTiming().ScheduleLoopingEvent(frame_ns, frame_ns, composition_event);
+        system.CoreTiming().ScheduleLoopingEvent(frame_ns, frame_ns, single_composition_event);
     }
 }
 
 NVFlinger::~NVFlinger() {
-    if (!system.IsMulticore()) {
-        system.CoreTiming().UnscheduleEvent(composition_event, 0);
+    if (system.IsMulticore()) {
+        system.CoreTiming().UnscheduleEvent(multi_composition_event, {});
+        vsync_thread.request_stop();
+        vsync_signal.store(true);
+        vsync_signal.notify_all();
+    } else {
+        system.CoreTiming().UnscheduleEvent(single_composition_event, {});
     }
 
     for (auto& display : displays) {
diff --git a/src/core/hle/service/nvflinger/nvflinger.h b/src/core/hle/service/nvflinger/nvflinger.h
index 4775597cc..044ac6ac8 100644
--- a/src/core/hle/service/nvflinger/nvflinger.h
+++ b/src/core/hle/service/nvflinger/nvflinger.h
@@ -126,12 +126,15 @@ private:
     u32 swap_interval = 1;
 
     /// Event that handles screen composition.
-    std::shared_ptr<Core::Timing::EventType> composition_event;
+    std::shared_ptr<Core::Timing::EventType> multi_composition_event;
+    std::shared_ptr<Core::Timing::EventType> single_composition_event;
 
     std::shared_ptr<std::mutex> guard;
 
     Core::System& system;
 
+    std::atomic<bool> vsync_signal;
+
     std::jthread vsync_thread;
 
     KernelHelpers::ServiceContext service_context;