diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp
index 4c9df099ee804dfe91cac39f512593856e15046f..fab7a12e46bdbb371a9fef4366a6ff1de47b6dcf 100644
--- a/src/core/hle/service/vi/vi.cpp
+++ b/src/core/hle/service/vi/vi.cpp
@@ -3,6 +3,7 @@
 // Refer to the license.txt file included.
 
 #include "common/alignment.h"
+#include "core/core_timing.h"
 #include "core/hle/ipc_helpers.h"
 #include "core/hle/service/vi/vi.h"
 #include "core/hle/service/vi/vi_m.h"
@@ -10,6 +11,9 @@
 namespace Service {
 namespace VI {
 
+constexpr size_t SCREEN_REFRESH_RATE = 60;
+constexpr u64 frame_ticks = static_cast<u64>(BASE_CLOCK_RATE / SCREEN_REFRESH_RATE);
+
 class Parcel {
 public:
     // This default size was chosen arbitrarily.
@@ -637,6 +641,19 @@ NVFlinger::NVFlinger() {
     displays.emplace_back(external);
     displays.emplace_back(edid);
     displays.emplace_back(internal);
+
+    // Schedule the screen composition events
+    composition_event =
+        CoreTiming::RegisterEvent("ScreenCompositioin", [this](u64 userdata, int cycles_late) {
+            Compose();
+            CoreTiming::ScheduleEvent(frame_ticks - cycles_late, composition_event);
+        });
+
+    CoreTiming::ScheduleEvent(frame_ticks, composition_event);
+}
+
+NVFlinger::~NVFlinger() {
+    CoreTiming::UnscheduleEvent(composition_event, 0);
 }
 
 u64 NVFlinger::OpenDisplay(const std::string& name) {
@@ -702,6 +719,13 @@ Layer& NVFlinger::GetLayer(u64 display_id, u64 layer_id) {
     return *itr;
 }
 
+void NVFlinger::Compose() {
+    for (auto& display : displays) {
+        // TODO(Subv): Gather the surfaces and forward them to the GPU for drawing.
+        display.vsync_event->Signal();
+    }
+}
+
 BufferQueue::BufferQueue(u32 id, u64 layer_id) : id(id), layer_id(layer_id) {}
 
 void BufferQueue::SetPreallocatedBuffer(u32 slot, IGBPBuffer& igbp_buffer) {
diff --git a/src/core/hle/service/vi/vi.h b/src/core/hle/service/vi/vi.h
index 029bd68312828d6750262cf23595250b32acbd94..1bd8f747290a38ef5d1b09a34d10ee74f58b75a6 100644
--- a/src/core/hle/service/vi/vi.h
+++ b/src/core/hle/service/vi/vi.h
@@ -80,7 +80,7 @@ struct Display {
 class NVFlinger {
 public:
     NVFlinger();
-    ~NVFlinger() = default;
+    ~NVFlinger();
 
     /// Opens the specified display and returns the id.
     u64 OpenDisplay(const std::string& name);
@@ -97,6 +97,10 @@ public:
     /// Obtains a buffer queue identified by the id.
     std::shared_ptr<BufferQueue> GetBufferQueue(u32 id) const;
 
+    /// Performs a composition request to the emulated nvidia GPU and triggers the vsync events when
+    /// finished.
+    void Compose();
+
 private:
     /// Returns the display identified by the specified id.
     Display& GetDisplay(u64 display_id);
@@ -112,6 +116,9 @@ private:
     /// Id to use for the next buffer queue that is created, this counter is shared among all
     /// layers.
     u32 next_buffer_queue_id = 1;
+
+    /// CoreTiming event that handles screen composition.
+    int composition_event;
 };
 
 class IApplicationDisplayService final : public ServiceFramework<IApplicationDisplayService> {