diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index f4325f0f8..5462decee 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -111,6 +111,8 @@ add_library(core STATIC
     frontend/scope_acquire_window_context.h
     gdbstub/gdbstub.cpp
     gdbstub/gdbstub.h
+    hardware_interrupt_manager.cpp
+    hardware_interrupt_manager.h
     hle/ipc.h
     hle/ipc_helpers.h
     hle/kernel/address_arbiter.cpp
@@ -372,6 +374,7 @@ add_library(core STATIC
     hle/service/nvdrv/devices/nvmap.h
     hle/service/nvdrv/interface.cpp
     hle/service/nvdrv/interface.h
+    hle/service/nvdrv/nvdata.h
     hle/service/nvdrv/nvdrv.cpp
     hle/service/nvdrv/nvdrv.h
     hle/service/nvdrv/nvmemp.cpp
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 4aceee785..20d64f3b0 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -19,6 +19,7 @@
 #include "core/file_sys/vfs_concat.h"
 #include "core/file_sys/vfs_real.h"
 #include "core/gdbstub/gdbstub.h"
+#include "core/hardware_interrupt_manager.h"
 #include "core/hle/kernel/client_port.h"
 #include "core/hle/kernel/kernel.h"
 #include "core/hle/kernel/process.h"
@@ -151,7 +152,7 @@ struct System::Impl {
         if (!renderer->Init()) {
             return ResultStatus::ErrorVideoCore;
         }
-
+        interrupt_manager = std::make_unique<Core::Hardware::InterruptManager>(system);
         gpu_core = VideoCore::CreateGPU(system);
 
         is_powered_on = true;
@@ -298,6 +299,7 @@ struct System::Impl {
     std::unique_ptr<VideoCore::RendererBase> renderer;
     std::unique_ptr<Tegra::GPU> gpu_core;
     std::shared_ptr<Tegra::DebugContext> debug_context;
+    std::unique_ptr<Core::Hardware::InterruptManager> interrupt_manager;
     CpuCoreManager cpu_core_manager;
     bool is_powered_on = false;
 
@@ -444,6 +446,14 @@ const Tegra::GPU& System::GPU() const {
     return *impl->gpu_core;
 }
 
+Core::Hardware::InterruptManager& System::InterruptManager() {
+    return *impl->interrupt_manager;
+}
+
+const Core::Hardware::InterruptManager& System::InterruptManager() const {
+    return *impl->interrupt_manager;
+}
+
 VideoCore::RendererBase& System::Renderer() {
     return *impl->renderer;
 }
diff --git a/src/core/core.h b/src/core/core.h
index 8ebb385ac..0138d93b0 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -70,6 +70,10 @@ namespace Core::Timing {
 class CoreTiming;
 }
 
+namespace Core::Hardware {
+class InterruptManager;
+}
+
 namespace Core {
 
 class ARM_Interface;
@@ -234,6 +238,12 @@ public:
     /// Provides a constant reference to the core timing instance.
     const Timing::CoreTiming& CoreTiming() const;
 
+    /// Provides a reference to the interrupt manager instance.
+    Core::Hardware::InterruptManager& InterruptManager();
+
+    /// Provides a constant reference to the interrupt manager instance.
+    const Core::Hardware::InterruptManager& InterruptManager() const;
+
     /// Provides a reference to the kernel instance.
     Kernel::KernelCore& Kernel();
 
diff --git a/src/core/hardware_interrupt_manager.cpp b/src/core/hardware_interrupt_manager.cpp
new file mode 100644
index 000000000..c2115db2d
--- /dev/null
+++ b/src/core/hardware_interrupt_manager.cpp
@@ -0,0 +1,30 @@
+// Copyright 2019 Yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "core/core.h"
+#include "core/core_timing.h"
+#include "core/hardware_interrupt_manager.h"
+#include "core/hle/service/nvdrv/interface.h"
+#include "core/hle/service/sm/sm.h"
+
+namespace Core::Hardware {
+
+InterruptManager::InterruptManager(Core::System& system_in) : system(system_in) {
+    gpu_interrupt_event =
+        system.CoreTiming().RegisterEvent("GPUInterrupt", [this](u64 message, s64) {
+            auto nvdrv = system.ServiceManager().GetService<Service::Nvidia::NVDRV>("nvdrv");
+            const u32 syncpt = static_cast<u32>(message >> 32);
+            const u32 value = static_cast<u32>(message);
+            nvdrv->SignalGPUInterruptSyncpt(syncpt, value);
+        });
+}
+
+InterruptManager::~InterruptManager() = default;
+
+void InterruptManager::GPUInterruptSyncpt(const u32 syncpoint_id, const u32 value) {
+    const u64 msg = (static_cast<u64>(syncpoint_id) << 32ULL) | value;
+    system.CoreTiming().ScheduleEvent(10, gpu_interrupt_event, msg);
+}
+
+} // namespace Core::Hardware
diff --git a/src/core/hardware_interrupt_manager.h b/src/core/hardware_interrupt_manager.h
new file mode 100644
index 000000000..494db883a
--- /dev/null
+++ b/src/core/hardware_interrupt_manager.h
@@ -0,0 +1,31 @@
+// Copyright 2019 Yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "common/common_types.h"
+
+namespace Core {
+class System;
+}
+
+namespace Core::Timing {
+struct EventType;
+}
+
+namespace Core::Hardware {
+
+class InterruptManager {
+public:
+    explicit InterruptManager(Core::System& system);
+    ~InterruptManager();
+
+    void GPUInterruptSyncpt(u32 syncpoint_id, u32 value);
+
+private:
+    Core::System& system;
+    Core::Timing::EventType* gpu_interrupt_event{};
+};
+
+} // namespace Core::Hardware
diff --git a/src/core/hle/service/nvdrv/devices/nvdevice.h b/src/core/hle/service/nvdrv/devices/nvdevice.h
index 4f6042b00..5b8248433 100644
--- a/src/core/hle/service/nvdrv/devices/nvdevice.h
+++ b/src/core/hle/service/nvdrv/devices/nvdevice.h
@@ -8,6 +8,11 @@
 #include "common/bit_field.h"
 #include "common/common_types.h"
 #include "common/swap.h"
+#include "core/hle/service/nvdrv/nvdata.h"
+
+namespace Core {
+class System;
+}
 
 namespace Service::Nvidia::Devices {
 
@@ -15,7 +20,7 @@ namespace Service::Nvidia::Devices {
 /// implement the ioctl interface.
 class nvdevice {
 public:
-    nvdevice() = default;
+    explicit nvdevice(Core::System& system) : system{system} {};
     virtual ~nvdevice() = default;
     union Ioctl {
         u32_le raw;
@@ -33,7 +38,11 @@ public:
      * @param output A buffer where the output data will be written to.
      * @returns The result code of the ioctl.
      */
-    virtual u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) = 0;
+    virtual u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                      IoctlCtrl& ctrl) = 0;
+
+protected:
+    Core::System& system;
 };
 
 } // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
index 20c7c39aa..76494f0b7 100644
--- a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.cpp
@@ -13,10 +13,12 @@
 
 namespace Service::Nvidia::Devices {
 
-nvdisp_disp0::nvdisp_disp0(std::shared_ptr<nvmap> nvmap_dev) : nvmap_dev(std::move(nvmap_dev)) {}
+nvdisp_disp0::nvdisp_disp0(Core::System& system, std::shared_ptr<nvmap> nvmap_dev)
+    : nvdevice(system), nvmap_dev(std::move(nvmap_dev)) {}
 nvdisp_disp0 ::~nvdisp_disp0() = default;
 
-u32 nvdisp_disp0::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvdisp_disp0::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                        IoctlCtrl& ctrl) {
     UNIMPLEMENTED_MSG("Unimplemented ioctl");
     return 0;
 }
@@ -34,9 +36,8 @@ void nvdisp_disp0::flip(u32 buffer_handle, u32 offset, u32 format, u32 width, u3
         addr,      offset,   width, height, stride, static_cast<PixelFormat>(format),
         transform, crop_rect};
 
-    auto& instance = Core::System::GetInstance();
-    instance.GetPerfStats().EndGameFrame();
-    instance.GPU().SwapBuffers(framebuffer);
+    system.GetPerfStats().EndGameFrame();
+    system.GPU().SwapBuffers(framebuffer);
 }
 
 } // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
index 12f3ef825..e79e490ff 100644
--- a/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
+++ b/src/core/hle/service/nvdrv/devices/nvdisp_disp0.h
@@ -17,10 +17,11 @@ class nvmap;
 
 class nvdisp_disp0 final : public nvdevice {
 public:
-    explicit nvdisp_disp0(std::shared_ptr<nvmap> nvmap_dev);
+    explicit nvdisp_disp0(Core::System& system, std::shared_ptr<nvmap> nvmap_dev);
     ~nvdisp_disp0() override;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
     /// Performs a screen flip, drawing the buffer pointed to by the handle.
     void flip(u32 buffer_handle, u32 offset, u32 format, u32 width, u32 height, u32 stride,
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
index af62d33d2..24ab3f2e9 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -22,10 +22,12 @@ enum {
 };
 }
 
-nvhost_as_gpu::nvhost_as_gpu(std::shared_ptr<nvmap> nvmap_dev) : nvmap_dev(std::move(nvmap_dev)) {}
+nvhost_as_gpu::nvhost_as_gpu(Core::System& system, std::shared_ptr<nvmap> nvmap_dev)
+    : nvdevice(system), nvmap_dev(std::move(nvmap_dev)) {}
 nvhost_as_gpu::~nvhost_as_gpu() = default;
 
-u32 nvhost_as_gpu::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvhost_as_gpu::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                         IoctlCtrl& ctrl) {
     LOG_DEBUG(Service_NVDRV, "called, command=0x{:08X}, input_size=0x{:X}, output_size=0x{:X}",
               command.raw, input.size(), output.size());
 
@@ -65,7 +67,7 @@ u32 nvhost_as_gpu::AllocateSpace(const std::vector<u8>& input, std::vector<u8>&
     LOG_DEBUG(Service_NVDRV, "called, pages={:X}, page_size={:X}, flags={:X}", params.pages,
               params.page_size, params.flags);
 
-    auto& gpu = Core::System::GetInstance().GPU();
+    auto& gpu = system.GPU();
     const u64 size{static_cast<u64>(params.pages) * static_cast<u64>(params.page_size)};
     if (params.flags & 1) {
         params.offset = gpu.MemoryManager().AllocateSpace(params.offset, size, 1);
@@ -85,7 +87,7 @@ u32 nvhost_as_gpu::Remap(const std::vector<u8>& input, std::vector<u8>& output)
     std::vector<IoctlRemapEntry> entries(num_entries);
     std::memcpy(entries.data(), input.data(), input.size());
 
-    auto& gpu = Core::System::GetInstance().GPU();
+    auto& gpu = system.GPU();
     for (const auto& entry : entries) {
         LOG_WARNING(Service_NVDRV, "remap entry, offset=0x{:X} handle=0x{:X} pages=0x{:X}",
                     entry.offset, entry.nvmap_handle, entry.pages);
@@ -136,7 +138,7 @@ u32 nvhost_as_gpu::MapBufferEx(const std::vector<u8>& input, std::vector<u8>& ou
     // case to prevent unexpected behavior.
     ASSERT(object->id == params.nvmap_handle);
 
-    auto& gpu = Core::System::GetInstance().GPU();
+    auto& gpu = system.GPU();
 
     if (params.flags & 1) {
         params.offset = gpu.MemoryManager().MapBufferEx(object->addr, params.offset, object->size);
@@ -173,8 +175,7 @@ u32 nvhost_as_gpu::UnmapBuffer(const std::vector<u8>& input, std::vector<u8>& ou
         return 0;
     }
 
-    params.offset = Core::System::GetInstance().GPU().MemoryManager().UnmapBuffer(params.offset,
-                                                                                  itr->second.size);
+    params.offset = system.GPU().MemoryManager().UnmapBuffer(params.offset, itr->second.size);
     buffer_mappings.erase(itr->second.offset);
 
     std::memcpy(output.data(), &params, output.size());
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
index eb14b1da8..30ca5f4c3 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
@@ -17,10 +17,11 @@ class nvmap;
 
 class nvhost_as_gpu final : public nvdevice {
 public:
-    explicit nvhost_as_gpu(std::shared_ptr<nvmap> nvmap_dev);
+    explicit nvhost_as_gpu(Core::System& system, std::shared_ptr<nvmap> nvmap_dev);
     ~nvhost_as_gpu() override;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
 private:
     enum class IoctlCommand : u32_le {
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp
index b39fb9ef9..9a66a5f88 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp
@@ -7,14 +7,20 @@
 
 #include "common/assert.h"
 #include "common/logging/log.h"
+#include "core/core.h"
+#include "core/hle/kernel/readable_event.h"
+#include "core/hle/kernel/writable_event.h"
 #include "core/hle/service/nvdrv/devices/nvhost_ctrl.h"
+#include "video_core/gpu.h"
 
 namespace Service::Nvidia::Devices {
 
-nvhost_ctrl::nvhost_ctrl() = default;
+nvhost_ctrl::nvhost_ctrl(Core::System& system, EventInterface& events_interface)
+    : nvdevice(system), events_interface{events_interface} {}
 nvhost_ctrl::~nvhost_ctrl() = default;
 
-u32 nvhost_ctrl::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvhost_ctrl::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                       IoctlCtrl& ctrl) {
     LOG_DEBUG(Service_NVDRV, "called, command=0x{:08X}, input_size=0x{:X}, output_size=0x{:X}",
               command.raw, input.size(), output.size());
 
@@ -22,11 +28,15 @@ u32 nvhost_ctrl::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<
     case IoctlCommand::IocGetConfigCommand:
         return NvOsGetConfigU32(input, output);
     case IoctlCommand::IocCtrlEventWaitCommand:
-        return IocCtrlEventWait(input, output, false);
+        return IocCtrlEventWait(input, output, false, ctrl);
     case IoctlCommand::IocCtrlEventWaitAsyncCommand:
-        return IocCtrlEventWait(input, output, true);
+        return IocCtrlEventWait(input, output, true, ctrl);
     case IoctlCommand::IocCtrlEventRegisterCommand:
         return IocCtrlEventRegister(input, output);
+    case IoctlCommand::IocCtrlEventUnregisterCommand:
+        return IocCtrlEventUnregister(input, output);
+    case IoctlCommand::IocCtrlEventSignalCommand:
+        return IocCtrlEventSignal(input, output);
     }
     UNIMPLEMENTED_MSG("Unimplemented ioctl");
     return 0;
@@ -41,23 +51,137 @@ u32 nvhost_ctrl::NvOsGetConfigU32(const std::vector<u8>& input, std::vector<u8>&
 }
 
 u32 nvhost_ctrl::IocCtrlEventWait(const std::vector<u8>& input, std::vector<u8>& output,
-                                  bool is_async) {
+                                  bool is_async, IoctlCtrl& ctrl) {
     IocCtrlEventWaitParams params{};
     std::memcpy(&params, input.data(), sizeof(params));
-    LOG_WARNING(Service_NVDRV,
-                "(STUBBED) called, syncpt_id={}, threshold={}, timeout={}, is_async={}",
-                params.syncpt_id, params.threshold, params.timeout, is_async);
+    LOG_DEBUG(Service_NVDRV, "syncpt_id={}, threshold={}, timeout={}, is_async={}",
+              params.syncpt_id, params.threshold, params.timeout, is_async);
 
-    // TODO(Subv): Implement actual syncpt waiting.
-    params.value = 0;
+    if (params.syncpt_id >= MaxSyncPoints) {
+        return NvResult::BadParameter;
+    }
+
+    auto& gpu = system.GPU();
+    // This is mostly to take into account unimplemented features. As synced
+    // gpu is always synced.
+    if (!gpu.IsAsync()) {
+        return NvResult::Success;
+    }
+    auto lock = gpu.LockSync();
+    const u32 current_syncpoint_value = gpu.GetSyncpointValue(params.syncpt_id);
+    const s32 diff = current_syncpoint_value - params.threshold;
+    if (diff >= 0) {
+        params.value = current_syncpoint_value;
+        std::memcpy(output.data(), &params, sizeof(params));
+        return NvResult::Success;
+    }
+    const u32 target_value = current_syncpoint_value - diff;
+
+    if (!is_async) {
+        params.value = 0;
+    }
+
+    if (params.timeout == 0) {
+        std::memcpy(output.data(), &params, sizeof(params));
+        return NvResult::Timeout;
+    }
+
+    u32 event_id;
+    if (is_async) {
+        event_id = params.value & 0x00FF;
+        if (event_id >= MaxNvEvents) {
+            std::memcpy(output.data(), &params, sizeof(params));
+            return NvResult::BadParameter;
+        }
+    } else {
+        if (ctrl.fresh_call) {
+            const auto result = events_interface.GetFreeEvent();
+            if (result) {
+                event_id = *result;
+            } else {
+                LOG_CRITICAL(Service_NVDRV, "No Free Events available!");
+                event_id = params.value & 0x00FF;
+            }
+        } else {
+            event_id = ctrl.event_id;
+        }
+    }
+
+    EventState status = events_interface.status[event_id];
+    if (event_id < MaxNvEvents || status == EventState::Free || status == EventState::Registered) {
+        events_interface.SetEventStatus(event_id, EventState::Waiting);
+        events_interface.assigned_syncpt[event_id] = params.syncpt_id;
+        events_interface.assigned_value[event_id] = target_value;
+        if (is_async) {
+            params.value = params.syncpt_id << 4;
+        } else {
+            params.value = ((params.syncpt_id & 0xfff) << 16) | 0x10000000;
+        }
+        params.value |= event_id;
+        events_interface.events[event_id].writable->Clear();
+        gpu.RegisterSyncptInterrupt(params.syncpt_id, target_value);
+        if (!is_async && ctrl.fresh_call) {
+            ctrl.must_delay = true;
+            ctrl.timeout = params.timeout;
+            ctrl.event_id = event_id;
+            return NvResult::Timeout;
+        }
+        std::memcpy(output.data(), &params, sizeof(params));
+        return NvResult::Timeout;
+    }
     std::memcpy(output.data(), &params, sizeof(params));
-    return 0;
+    return NvResult::BadParameter;
 }
 
 u32 nvhost_ctrl::IocCtrlEventRegister(const std::vector<u8>& input, std::vector<u8>& output) {
-    LOG_WARNING(Service_NVDRV, "(STUBBED) called");
-    // TODO(bunnei): Implement this.
-    return 0;
+    IocCtrlEventRegisterParams params{};
+    std::memcpy(&params, input.data(), sizeof(params));
+    const u32 event_id = params.user_event_id & 0x00FF;
+    LOG_DEBUG(Service_NVDRV, " called, user_event_id: {:X}", event_id);
+    if (event_id >= MaxNvEvents) {
+        return NvResult::BadParameter;
+    }
+    if (events_interface.registered[event_id]) {
+        return NvResult::BadParameter;
+    }
+    events_interface.RegisterEvent(event_id);
+    return NvResult::Success;
+}
+
+u32 nvhost_ctrl::IocCtrlEventUnregister(const std::vector<u8>& input, std::vector<u8>& output) {
+    IocCtrlEventUnregisterParams params{};
+    std::memcpy(&params, input.data(), sizeof(params));
+    const u32 event_id = params.user_event_id & 0x00FF;
+    LOG_DEBUG(Service_NVDRV, " called, user_event_id: {:X}", event_id);
+    if (event_id >= MaxNvEvents) {
+        return NvResult::BadParameter;
+    }
+    if (!events_interface.registered[event_id]) {
+        return NvResult::BadParameter;
+    }
+    events_interface.UnregisterEvent(event_id);
+    return NvResult::Success;
+}
+
+u32 nvhost_ctrl::IocCtrlEventSignal(const std::vector<u8>& input, std::vector<u8>& output) {
+    IocCtrlEventSignalParams params{};
+    std::memcpy(&params, input.data(), sizeof(params));
+    // TODO(Blinkhawk): This is normally called when an NvEvents timeout on WaitSynchronization
+    // It is believed from RE to cancel the GPU Event. However, better research is required
+    u32 event_id = params.user_event_id & 0x00FF;
+    LOG_WARNING(Service_NVDRV, "(STUBBED) called, user_event_id: {:X}", event_id);
+    if (event_id >= MaxNvEvents) {
+        return NvResult::BadParameter;
+    }
+    if (events_interface.status[event_id] == EventState::Waiting) {
+        auto& gpu = system.GPU();
+        if (gpu.CancelSyncptInterrupt(events_interface.assigned_syncpt[event_id],
+                                      events_interface.assigned_value[event_id])) {
+            events_interface.LiberateEvent(event_id);
+            events_interface.events[event_id].writable->Signal();
+        }
+    }
+    return NvResult::Success;
 }
 
 } // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.h b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.h
index 6d0de2212..14e6e7e57 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.h
@@ -8,15 +8,17 @@
 #include <vector>
 #include "common/common_types.h"
 #include "core/hle/service/nvdrv/devices/nvdevice.h"
+#include "core/hle/service/nvdrv/nvdrv.h"
 
 namespace Service::Nvidia::Devices {
 
 class nvhost_ctrl final : public nvdevice {
 public:
-    nvhost_ctrl();
+    explicit nvhost_ctrl(Core::System& system, EventInterface& events_interface);
     ~nvhost_ctrl() override;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
 private:
     enum class IoctlCommand : u32_le {
@@ -132,9 +134,16 @@ private:
 
     u32 NvOsGetConfigU32(const std::vector<u8>& input, std::vector<u8>& output);
 
-    u32 IocCtrlEventWait(const std::vector<u8>& input, std::vector<u8>& output, bool is_async);
+    u32 IocCtrlEventWait(const std::vector<u8>& input, std::vector<u8>& output, bool is_async,
+                         IoctlCtrl& ctrl);
 
     u32 IocCtrlEventRegister(const std::vector<u8>& input, std::vector<u8>& output);
+
+    u32 IocCtrlEventUnregister(const std::vector<u8>& input, std::vector<u8>& output);
+
+    u32 IocCtrlEventSignal(const std::vector<u8>& input, std::vector<u8>& output);
+
+    EventInterface& events_interface;
 };
 
 } // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
index 0e28755bd..988effd90 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
@@ -12,10 +12,11 @@
 
 namespace Service::Nvidia::Devices {
 
-nvhost_ctrl_gpu::nvhost_ctrl_gpu() = default;
+nvhost_ctrl_gpu::nvhost_ctrl_gpu(Core::System& system) : nvdevice(system) {}
 nvhost_ctrl_gpu::~nvhost_ctrl_gpu() = default;
 
-u32 nvhost_ctrl_gpu::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvhost_ctrl_gpu::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                           IoctlCtrl& ctrl) {
     LOG_DEBUG(Service_NVDRV, "called, command=0x{:08X}, input_size=0x{:X}, output_size=0x{:X}",
               command.raw, input.size(), output.size());
 
@@ -185,7 +186,7 @@ u32 nvhost_ctrl_gpu::GetGpuTime(const std::vector<u8>& input, std::vector<u8>& o
 
     IoctlGetGpuTime params{};
     std::memcpy(&params, input.data(), input.size());
-    const auto ns = Core::Timing::CyclesToNs(Core::System::GetInstance().CoreTiming().GetTicks());
+    const auto ns = Core::Timing::CyclesToNs(system.CoreTiming().GetTicks());
     params.gpu_time = static_cast<u64_le>(ns.count());
     std::memcpy(output.data(), &params, output.size());
     return 0;
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.h
index 240435eea..2b035ae3f 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.h
@@ -13,10 +13,11 @@ namespace Service::Nvidia::Devices {
 
 class nvhost_ctrl_gpu final : public nvdevice {
 public:
-    nvhost_ctrl_gpu();
+    explicit nvhost_ctrl_gpu(Core::System& system);
     ~nvhost_ctrl_gpu() override;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
 private:
     enum class IoctlCommand : u32_le {
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
index 8ce7bc7a5..241dac881 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
@@ -13,10 +13,12 @@
 
 namespace Service::Nvidia::Devices {
 
-nvhost_gpu::nvhost_gpu(std::shared_ptr<nvmap> nvmap_dev) : nvmap_dev(std::move(nvmap_dev)) {}
+nvhost_gpu::nvhost_gpu(Core::System& system, std::shared_ptr<nvmap> nvmap_dev)
+    : nvdevice(system), nvmap_dev(std::move(nvmap_dev)) {}
 nvhost_gpu::~nvhost_gpu() = default;
 
-u32 nvhost_gpu::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvhost_gpu::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                      IoctlCtrl& ctrl) {
     LOG_DEBUG(Service_NVDRV, "called, command=0x{:08X}, input_size=0x{:X}, output_size=0x{:X}",
               command.raw, input.size(), output.size());
 
@@ -119,8 +121,10 @@ u32 nvhost_gpu::AllocGPFIFOEx2(const std::vector<u8>& input, std::vector<u8>& ou
                 params.num_entries, params.flags, params.unk0, params.unk1, params.unk2,
                 params.unk3);
 
-    params.fence_out.id = 0;
-    params.fence_out.value = 0;
+    auto& gpu = system.GPU();
+    params.fence_out.id = assigned_syncpoints;
+    params.fence_out.value = gpu.GetSyncpointValue(assigned_syncpoints);
+    assigned_syncpoints++;
     std::memcpy(output.data(), &params, output.size());
     return 0;
 }
@@ -143,7 +147,7 @@ u32 nvhost_gpu::SubmitGPFIFO(const std::vector<u8>& input, std::vector<u8>& outp
     IoctlSubmitGpfifo params{};
     std::memcpy(&params, input.data(), sizeof(IoctlSubmitGpfifo));
     LOG_WARNING(Service_NVDRV, "(STUBBED) called, gpfifo={:X}, num_entries={:X}, flags={:X}",
-                params.address, params.num_entries, params.flags);
+                params.address, params.num_entries, params.flags.raw);
 
     ASSERT_MSG(input.size() == sizeof(IoctlSubmitGpfifo) +
                                    params.num_entries * sizeof(Tegra::CommandListHeader),
@@ -153,10 +157,18 @@ u32 nvhost_gpu::SubmitGPFIFO(const std::vector<u8>& input, std::vector<u8>& outp
     std::memcpy(entries.data(), &input[sizeof(IoctlSubmitGpfifo)],
                 params.num_entries * sizeof(Tegra::CommandListHeader));
 
-    Core::System::GetInstance().GPU().PushGPUEntries(std::move(entries));
+    UNIMPLEMENTED_IF(params.flags.add_wait.Value() != 0);
+    UNIMPLEMENTED_IF(params.flags.add_increment.Value() != 0);
+
+    auto& gpu = system.GPU();
+    u32 current_syncpoint_value = gpu.GetSyncpointValue(params.fence_out.id);
+    if (params.flags.increment.Value()) {
+        params.fence_out.value += current_syncpoint_value;
+    } else {
+        params.fence_out.value = current_syncpoint_value;
+    }
+    gpu.PushGPUEntries(std::move(entries));
 
-    params.fence_out.id = 0;
-    params.fence_out.value = 0;
     std::memcpy(output.data(), &params, sizeof(IoctlSubmitGpfifo));
     return 0;
 }
@@ -168,16 +180,24 @@ u32 nvhost_gpu::KickoffPB(const std::vector<u8>& input, std::vector<u8>& output)
     IoctlSubmitGpfifo params{};
     std::memcpy(&params, input.data(), sizeof(IoctlSubmitGpfifo));
     LOG_WARNING(Service_NVDRV, "(STUBBED) called, gpfifo={:X}, num_entries={:X}, flags={:X}",
-                params.address, params.num_entries, params.flags);
+                params.address, params.num_entries, params.flags.raw);
 
     Tegra::CommandList entries(params.num_entries);
     Memory::ReadBlock(params.address, entries.data(),
                       params.num_entries * sizeof(Tegra::CommandListHeader));
 
-    Core::System::GetInstance().GPU().PushGPUEntries(std::move(entries));
+    UNIMPLEMENTED_IF(params.flags.add_wait.Value() != 0);
+    UNIMPLEMENTED_IF(params.flags.add_increment.Value() != 0);
+
+    auto& gpu = system.GPU();
+    u32 current_syncpoint_value = gpu.GetSyncpointValue(params.fence_out.id);
+    if (params.flags.increment.Value()) {
+        params.fence_out.value += current_syncpoint_value;
+    } else {
+        params.fence_out.value = current_syncpoint_value;
+    }
+    gpu.PushGPUEntries(std::move(entries));
 
-    params.fence_out.id = 0;
-    params.fence_out.value = 0;
     std::memcpy(output.data(), &params, output.size());
     return 0;
 }
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_gpu.h
index 62beb5c0c..d2e8fbae9 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.h
@@ -10,6 +10,7 @@
 #include "common/common_types.h"
 #include "common/swap.h"
 #include "core/hle/service/nvdrv/devices/nvdevice.h"
+#include "core/hle/service/nvdrv/nvdata.h"
 
 namespace Service::Nvidia::Devices {
 
@@ -20,10 +21,11 @@ constexpr u32 NVGPU_IOCTL_CHANNEL_KICKOFF_PB(0x1b);
 
 class nvhost_gpu final : public nvdevice {
 public:
-    explicit nvhost_gpu(std::shared_ptr<nvmap> nvmap_dev);
+    explicit nvhost_gpu(Core::System& system, std::shared_ptr<nvmap> nvmap_dev);
     ~nvhost_gpu() override;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
 private:
     enum class IoctlCommand : u32_le {
@@ -113,11 +115,7 @@ private:
     static_assert(sizeof(IoctlGetErrorNotification) == 16,
                   "IoctlGetErrorNotification is incorrect size");
 
-    struct IoctlFence {
-        u32_le id;
-        u32_le value;
-    };
-    static_assert(sizeof(IoctlFence) == 8, "IoctlFence is incorrect size");
+    static_assert(sizeof(Fence) == 8, "Fence is incorrect size");
 
     struct IoctlAllocGpfifoEx {
         u32_le num_entries;
@@ -132,13 +130,13 @@ private:
     static_assert(sizeof(IoctlAllocGpfifoEx) == 32, "IoctlAllocGpfifoEx is incorrect size");
 
     struct IoctlAllocGpfifoEx2 {
-        u32_le num_entries;   // in
-        u32_le flags;         // in
-        u32_le unk0;          // in (1 works)
-        IoctlFence fence_out; // out
-        u32_le unk1;          // in
-        u32_le unk2;          // in
-        u32_le unk3;          // in
+        u32_le num_entries; // in
+        u32_le flags;       // in
+        u32_le unk0;        // in (1 works)
+        Fence fence_out;    // out
+        u32_le unk1;        // in
+        u32_le unk2;        // in
+        u32_le unk3;        // in
     };
     static_assert(sizeof(IoctlAllocGpfifoEx2) == 32, "IoctlAllocGpfifoEx2 is incorrect size");
 
@@ -153,10 +151,16 @@ private:
     struct IoctlSubmitGpfifo {
         u64_le address;     // pointer to gpfifo entry structs
         u32_le num_entries; // number of fence objects being submitted
-        u32_le flags;
-        IoctlFence fence_out; // returned new fence object for others to wait on
-    };
-    static_assert(sizeof(IoctlSubmitGpfifo) == 16 + sizeof(IoctlFence),
+        union {
+            u32_le raw;
+            BitField<0, 1, u32_le> add_wait;      // append a wait sync_point to the list
+            BitField<1, 1, u32_le> add_increment; // append an increment to the list
+            BitField<2, 1, u32_le> new_hw_format; // Mostly ignored
+            BitField<8, 1, u32_le> increment;     // increment the returned fence
+        } flags;
+        Fence fence_out; // returned new fence object for others to wait on
+    };
+    static_assert(sizeof(IoctlSubmitGpfifo) == 16 + sizeof(Fence),
                   "IoctlSubmitGpfifo is incorrect size");
 
     struct IoctlGetWaitbase {
@@ -184,6 +188,7 @@ private:
     u32 ChannelSetTimeout(const std::vector<u8>& input, std::vector<u8>& output);
 
     std::shared_ptr<nvmap> nvmap_dev;
+    u32 assigned_syncpoints{};
 };
 
 } // namespace Service::Nvidia::Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp
index f5e8ea7c3..f572ad30f 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.cpp
@@ -10,10 +10,11 @@
 
 namespace Service::Nvidia::Devices {
 
-nvhost_nvdec::nvhost_nvdec() = default;
+nvhost_nvdec::nvhost_nvdec(Core::System& system) : nvdevice(system) {}
 nvhost_nvdec::~nvhost_nvdec() = default;
 
-u32 nvhost_nvdec::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvhost_nvdec::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                        IoctlCtrl& ctrl) {
     LOG_DEBUG(Service_NVDRV, "called, command=0x{:08X}, input_size=0x{:X}, output_size=0x{:X}",
               command.raw, input.size(), output.size());
 
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.h b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.h
index 0e7b284f8..2710f0511 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_nvdec.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_nvdec.h
@@ -13,10 +13,11 @@ namespace Service::Nvidia::Devices {
 
 class nvhost_nvdec final : public nvdevice {
 public:
-    nvhost_nvdec();
+    explicit nvhost_nvdec(Core::System& system);
     ~nvhost_nvdec() override;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
 private:
     enum class IoctlCommand : u32_le {
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_nvjpg.cpp b/src/core/hle/service/nvdrv/devices/nvhost_nvjpg.cpp
index 3e0951ab0..38282956f 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_nvjpg.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_nvjpg.cpp
@@ -10,10 +10,11 @@
 
 namespace Service::Nvidia::Devices {
 
-nvhost_nvjpg::nvhost_nvjpg() = default;
+nvhost_nvjpg::nvhost_nvjpg(Core::System& system) : nvdevice(system) {}
 nvhost_nvjpg::~nvhost_nvjpg() = default;
 
-u32 nvhost_nvjpg::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvhost_nvjpg::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                        IoctlCtrl& ctrl) {
     LOG_DEBUG(Service_NVDRV, "called, command=0x{:08X}, input_size=0x{:X}, output_size=0x{:X}",
               command.raw, input.size(), output.size());
 
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_nvjpg.h b/src/core/hle/service/nvdrv/devices/nvhost_nvjpg.h
index 89fd5e95e..379766693 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_nvjpg.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_nvjpg.h
@@ -13,10 +13,11 @@ namespace Service::Nvidia::Devices {
 
 class nvhost_nvjpg final : public nvdevice {
 public:
-    nvhost_nvjpg();
+    explicit nvhost_nvjpg(Core::System& system);
     ~nvhost_nvjpg() override;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
 private:
     enum class IoctlCommand : u32_le {
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_vic.cpp b/src/core/hle/service/nvdrv/devices/nvhost_vic.cpp
index d544f0f31..70e8091db 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_vic.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_vic.cpp
@@ -10,10 +10,11 @@
 
 namespace Service::Nvidia::Devices {
 
-nvhost_vic::nvhost_vic() = default;
+nvhost_vic::nvhost_vic(Core::System& system) : nvdevice(system) {}
 nvhost_vic::~nvhost_vic() = default;
 
-u32 nvhost_vic::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvhost_vic::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                      IoctlCtrl& ctrl) {
     LOG_DEBUG(Service_NVDRV, "called, command=0x{:08X}, input_size=0x{:X}, output_size=0x{:X}",
               command.raw, input.size(), output.size());
 
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_vic.h b/src/core/hle/service/nvdrv/devices/nvhost_vic.h
index fc24c3f9c..7d111977e 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_vic.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_vic.h
@@ -13,10 +13,11 @@ namespace Service::Nvidia::Devices {
 
 class nvhost_vic final : public nvdevice {
 public:
-    nvhost_vic();
+    explicit nvhost_vic(Core::System& system);
     ~nvhost_vic() override;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
 private:
     enum class IoctlCommand : u32_le {
diff --git a/src/core/hle/service/nvdrv/devices/nvmap.cpp b/src/core/hle/service/nvdrv/devices/nvmap.cpp
index 1ec796fc6..223b496b7 100644
--- a/src/core/hle/service/nvdrv/devices/nvmap.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvmap.cpp
@@ -18,7 +18,7 @@ enum {
 };
 }
 
-nvmap::nvmap() = default;
+nvmap::nvmap(Core::System& system) : nvdevice(system) {}
 nvmap::~nvmap() = default;
 
 VAddr nvmap::GetObjectAddress(u32 handle) const {
@@ -28,7 +28,8 @@ VAddr nvmap::GetObjectAddress(u32 handle) const {
     return object->addr;
 }
 
-u32 nvmap::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 nvmap::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+                 IoctlCtrl& ctrl) {
     switch (static_cast<IoctlCommand>(command.raw)) {
     case IoctlCommand::Create:
         return IocCreate(input, output);
diff --git a/src/core/hle/service/nvdrv/devices/nvmap.h b/src/core/hle/service/nvdrv/devices/nvmap.h
index 396230c19..bf4a101c2 100644
--- a/src/core/hle/service/nvdrv/devices/nvmap.h
+++ b/src/core/hle/service/nvdrv/devices/nvmap.h
@@ -16,13 +16,14 @@ namespace Service::Nvidia::Devices {
 
 class nvmap final : public nvdevice {
 public:
-    nvmap();
+    explicit nvmap(Core::System& system);
     ~nvmap() override;
 
     /// Returns the allocated address of an nvmap object given its handle.
     VAddr GetObjectAddress(u32 handle) const;
 
-    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output) override;
+    u32 ioctl(Ioctl command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl) override;
 
     /// Represents an nvmap object.
     struct Object {
diff --git a/src/core/hle/service/nvdrv/interface.cpp b/src/core/hle/service/nvdrv/interface.cpp
index b60fc748b..d5be64ed2 100644
--- a/src/core/hle/service/nvdrv/interface.cpp
+++ b/src/core/hle/service/nvdrv/interface.cpp
@@ -8,12 +8,18 @@
 #include "core/hle/ipc_helpers.h"
 #include "core/hle/kernel/kernel.h"
 #include "core/hle/kernel/readable_event.h"
+#include "core/hle/kernel/thread.h"
 #include "core/hle/kernel/writable_event.h"
 #include "core/hle/service/nvdrv/interface.h"
+#include "core/hle/service/nvdrv/nvdata.h"
 #include "core/hle/service/nvdrv/nvdrv.h"
 
 namespace Service::Nvidia {
 
+void NVDRV::SignalGPUInterruptSyncpt(const u32 syncpoint_id, const u32 value) {
+    nvdrv->SignalSyncpt(syncpoint_id, value);
+}
+
 void NVDRV::Open(Kernel::HLERequestContext& ctx) {
     LOG_DEBUG(Service_NVDRV, "called");
 
@@ -36,11 +42,31 @@ void NVDRV::Ioctl(Kernel::HLERequestContext& ctx) {
 
     std::vector<u8> output(ctx.GetWriteBufferSize());
 
+    IoctlCtrl ctrl{};
+
+    u32 result = nvdrv->Ioctl(fd, command, ctx.ReadBuffer(), output, ctrl);
+
+    if (ctrl.must_delay) {
+        ctrl.fresh_call = false;
+        ctx.SleepClientThread(
+            "NVServices::DelayedResponse", ctrl.timeout,
+            [=](Kernel::SharedPtr<Kernel::Thread> thread, Kernel::HLERequestContext& ctx,
+                Kernel::ThreadWakeupReason reason) {
+                IoctlCtrl ctrl2{ctrl};
+                std::vector<u8> output2 = output;
+                u32 result = nvdrv->Ioctl(fd, command, ctx.ReadBuffer(), output2, ctrl2);
+                ctx.WriteBuffer(output2);
+                IPC::ResponseBuilder rb{ctx, 3};
+                rb.Push(RESULT_SUCCESS);
+                rb.Push(result);
+            },
+            nvdrv->GetEventWriteable(ctrl.event_id));
+    } else {
+        ctx.WriteBuffer(output);
+    }
     IPC::ResponseBuilder rb{ctx, 3};
     rb.Push(RESULT_SUCCESS);
-    rb.Push(nvdrv->Ioctl(fd, command, ctx.ReadBuffer(), output));
-
-    ctx.WriteBuffer(output);
+    rb.Push(result);
 }
 
 void NVDRV::Close(Kernel::HLERequestContext& ctx) {
@@ -66,13 +92,19 @@ void NVDRV::Initialize(Kernel::HLERequestContext& ctx) {
 void NVDRV::QueryEvent(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
     u32 fd = rp.Pop<u32>();
-    u32 event_id = rp.Pop<u32>();
+    // TODO(Blinkhawk): Figure the meaning of the flag at bit 16
+    u32 event_id = rp.Pop<u32>() & 0x000000FF;
     LOG_WARNING(Service_NVDRV, "(STUBBED) called, fd={:X}, event_id={:X}", fd, event_id);
 
     IPC::ResponseBuilder rb{ctx, 3, 1};
     rb.Push(RESULT_SUCCESS);
-    rb.PushCopyObjects(query_event.readable);
-    rb.Push<u32>(0);
+    if (event_id < MaxNvEvents) {
+        rb.PushCopyObjects(nvdrv->GetEvent(event_id));
+        rb.Push<u32>(NvResult::Success);
+    } else {
+        rb.Push<u32>(0);
+        rb.Push<u32>(NvResult::BadParameter);
+    }
 }
 
 void NVDRV::SetClientPID(Kernel::HLERequestContext& ctx) {
@@ -127,10 +159,6 @@ NVDRV::NVDRV(std::shared_ptr<Module> nvdrv, const char* name)
         {13, &NVDRV::FinishInitialize, "FinishInitialize"},
     };
     RegisterHandlers(functions);
-
-    auto& kernel = Core::System::GetInstance().Kernel();
-    query_event = Kernel::WritableEvent::CreateEventPair(kernel, Kernel::ResetType::Automatic,
-                                                         "NVDRV::query_event");
 }
 
 NVDRV::~NVDRV() = default;
diff --git a/src/core/hle/service/nvdrv/interface.h b/src/core/hle/service/nvdrv/interface.h
index 5b4889910..10a0ecd52 100644
--- a/src/core/hle/service/nvdrv/interface.h
+++ b/src/core/hle/service/nvdrv/interface.h
@@ -19,6 +19,8 @@ public:
     NVDRV(std::shared_ptr<Module> nvdrv, const char* name);
     ~NVDRV() override;
 
+    void SignalGPUInterruptSyncpt(const u32 syncpoint_id, const u32 value);
+
 private:
     void Open(Kernel::HLERequestContext& ctx);
     void Ioctl(Kernel::HLERequestContext& ctx);
@@ -33,8 +35,6 @@ private:
     std::shared_ptr<Module> nvdrv;
 
     u64 pid{};
-
-    Kernel::EventPair query_event;
 };
 
 } // namespace Service::Nvidia
diff --git a/src/core/hle/service/nvdrv/nvdata.h b/src/core/hle/service/nvdrv/nvdata.h
new file mode 100644
index 000000000..ac03cbc23
--- /dev/null
+++ b/src/core/hle/service/nvdrv/nvdata.h
@@ -0,0 +1,48 @@
+#pragma once
+
+#include <array>
+#include "common/common_types.h"
+
+namespace Service::Nvidia {
+
+constexpr u32 MaxSyncPoints = 192;
+constexpr u32 MaxNvEvents = 64;
+
+struct Fence {
+    s32 id;
+    u32 value;
+};
+
+static_assert(sizeof(Fence) == 8, "Fence has wrong size");
+
+struct MultiFence {
+    u32 num_fences;
+    std::array<Fence, 4> fences;
+};
+
+enum NvResult : u32 {
+    Success = 0,
+    BadParameter = 4,
+    Timeout = 5,
+    ResourceError = 15,
+};
+
+enum class EventState {
+    Free = 0,
+    Registered = 1,
+    Waiting = 2,
+    Busy = 3,
+};
+
+struct IoctlCtrl {
+    // First call done to the servioce for services that call itself again after a call.
+    bool fresh_call{true};
+    // Tells the Ioctl Wrapper that it must delay the IPC response and send the thread to sleep
+    bool must_delay{};
+    // Timeout for the delay
+    s64 timeout{};
+    // NV Event Id
+    s32 event_id{-1};
+};
+
+} // namespace Service::Nvidia
diff --git a/src/core/hle/service/nvdrv/nvdrv.cpp b/src/core/hle/service/nvdrv/nvdrv.cpp
index 6e4b8f2c6..2011a226a 100644
--- a/src/core/hle/service/nvdrv/nvdrv.cpp
+++ b/src/core/hle/service/nvdrv/nvdrv.cpp
@@ -4,7 +4,10 @@
 
 #include <utility>
 
+#include <fmt/format.h>
 #include "core/hle/ipc_helpers.h"
+#include "core/hle/kernel/readable_event.h"
+#include "core/hle/kernel/writable_event.h"
 #include "core/hle/service/nvdrv/devices/nvdevice.h"
 #include "core/hle/service/nvdrv/devices/nvdisp_disp0.h"
 #include "core/hle/service/nvdrv/devices/nvhost_as_gpu.h"
@@ -22,8 +25,9 @@
 
 namespace Service::Nvidia {
 
-void InstallInterfaces(SM::ServiceManager& service_manager, NVFlinger::NVFlinger& nvflinger) {
-    auto module_ = std::make_shared<Module>();
+void InstallInterfaces(SM::ServiceManager& service_manager, NVFlinger::NVFlinger& nvflinger,
+                       Core::System& system) {
+    auto module_ = std::make_shared<Module>(system);
     std::make_shared<NVDRV>(module_, "nvdrv")->InstallAsService(service_manager);
     std::make_shared<NVDRV>(module_, "nvdrv:a")->InstallAsService(service_manager);
     std::make_shared<NVDRV>(module_, "nvdrv:s")->InstallAsService(service_manager);
@@ -32,17 +36,25 @@ void InstallInterfaces(SM::ServiceManager& service_manager, NVFlinger::NVFlinger
     nvflinger.SetNVDrvInstance(module_);
 }
 
-Module::Module() {
-    auto nvmap_dev = std::make_shared<Devices::nvmap>();
-    devices["/dev/nvhost-as-gpu"] = std::make_shared<Devices::nvhost_as_gpu>(nvmap_dev);
-    devices["/dev/nvhost-gpu"] = std::make_shared<Devices::nvhost_gpu>(nvmap_dev);
-    devices["/dev/nvhost-ctrl-gpu"] = std::make_shared<Devices::nvhost_ctrl_gpu>();
+Module::Module(Core::System& system) {
+    auto& kernel = system.Kernel();
+    for (u32 i = 0; i < MaxNvEvents; i++) {
+        std::string event_label = fmt::format("NVDRV::NvEvent_{}", i);
+        events_interface.events[i] = Kernel::WritableEvent::CreateEventPair(
+            kernel, Kernel::ResetType::Automatic, event_label);
+        events_interface.status[i] = EventState::Free;
+        events_interface.registered[i] = false;
+    }
+    auto nvmap_dev = std::make_shared<Devices::nvmap>(system);
+    devices["/dev/nvhost-as-gpu"] = std::make_shared<Devices::nvhost_as_gpu>(system, nvmap_dev);
+    devices["/dev/nvhost-gpu"] = std::make_shared<Devices::nvhost_gpu>(system, nvmap_dev);
+    devices["/dev/nvhost-ctrl-gpu"] = std::make_shared<Devices::nvhost_ctrl_gpu>(system);
     devices["/dev/nvmap"] = nvmap_dev;
-    devices["/dev/nvdisp_disp0"] = std::make_shared<Devices::nvdisp_disp0>(nvmap_dev);
-    devices["/dev/nvhost-ctrl"] = std::make_shared<Devices::nvhost_ctrl>();
-    devices["/dev/nvhost-nvdec"] = std::make_shared<Devices::nvhost_nvdec>();
-    devices["/dev/nvhost-nvjpg"] = std::make_shared<Devices::nvhost_nvjpg>();
-    devices["/dev/nvhost-vic"] = std::make_shared<Devices::nvhost_vic>();
+    devices["/dev/nvdisp_disp0"] = std::make_shared<Devices::nvdisp_disp0>(system, nvmap_dev);
+    devices["/dev/nvhost-ctrl"] = std::make_shared<Devices::nvhost_ctrl>(system, events_interface);
+    devices["/dev/nvhost-nvdec"] = std::make_shared<Devices::nvhost_nvdec>(system);
+    devices["/dev/nvhost-nvjpg"] = std::make_shared<Devices::nvhost_nvjpg>(system);
+    devices["/dev/nvhost-vic"] = std::make_shared<Devices::nvhost_vic>(system);
 }
 
 Module::~Module() = default;
@@ -59,12 +71,13 @@ u32 Module::Open(const std::string& device_name) {
     return fd;
 }
 
-u32 Module::Ioctl(u32 fd, u32 command, const std::vector<u8>& input, std::vector<u8>& output) {
+u32 Module::Ioctl(u32 fd, u32 command, const std::vector<u8>& input, std::vector<u8>& output,
+                  IoctlCtrl& ctrl) {
     auto itr = open_files.find(fd);
     ASSERT_MSG(itr != open_files.end(), "Tried to talk to an invalid device");
 
     auto& device = itr->second;
-    return device->ioctl({command}, input, output);
+    return device->ioctl({command}, input, output, ctrl);
 }
 
 ResultCode Module::Close(u32 fd) {
@@ -77,4 +90,22 @@ ResultCode Module::Close(u32 fd) {
     return RESULT_SUCCESS;
 }
 
+void Module::SignalSyncpt(const u32 syncpoint_id, const u32 value) {
+    for (u32 i = 0; i < MaxNvEvents; i++) {
+        if (events_interface.assigned_syncpt[i] == syncpoint_id &&
+            events_interface.assigned_value[i] == value) {
+            events_interface.LiberateEvent(i);
+            events_interface.events[i].writable->Signal();
+        }
+    }
+}
+
+Kernel::SharedPtr<Kernel::ReadableEvent> Module::GetEvent(const u32 event_id) const {
+    return events_interface.events[event_id].readable;
+}
+
+Kernel::SharedPtr<Kernel::WritableEvent> Module::GetEventWriteable(const u32 event_id) const {
+    return events_interface.events[event_id].writable;
+}
+
 } // namespace Service::Nvidia
diff --git a/src/core/hle/service/nvdrv/nvdrv.h b/src/core/hle/service/nvdrv/nvdrv.h
index 53564f696..a339ab672 100644
--- a/src/core/hle/service/nvdrv/nvdrv.h
+++ b/src/core/hle/service/nvdrv/nvdrv.h
@@ -8,8 +8,14 @@
 #include <unordered_map>
 #include <vector>
 #include "common/common_types.h"
+#include "core/hle/kernel/writable_event.h"
+#include "core/hle/service/nvdrv/nvdata.h"
 #include "core/hle/service/service.h"
 
+namespace Core {
+class System;
+}
+
 namespace Service::NVFlinger {
 class NVFlinger;
 }
@@ -20,16 +26,72 @@ namespace Devices {
 class nvdevice;
 }
 
-struct IoctlFence {
-    u32 id;
-    u32 value;
+struct EventInterface {
+    // Mask representing currently busy events
+    u64 events_mask{};
+    // Each kernel event associated to an NV event
+    std::array<Kernel::EventPair, MaxNvEvents> events;
+    // The status of the current NVEvent
+    std::array<EventState, MaxNvEvents> status{};
+    // Tells if an NVEvent is registered or not
+    std::array<bool, MaxNvEvents> registered{};
+    // When an NVEvent is waiting on GPU interrupt, this is the sync_point
+    // associated with it.
+    std::array<u32, MaxNvEvents> assigned_syncpt{};
+    // This is the value of the GPU interrupt for which the NVEvent is waiting
+    // for.
+    std::array<u32, MaxNvEvents> assigned_value{};
+    // Constant to denote an unasigned syncpoint.
+    static constexpr u32 unassigned_syncpt = 0xFFFFFFFF;
+    std::optional<u32> GetFreeEvent() const {
+        u64 mask = events_mask;
+        for (u32 i = 0; i < MaxNvEvents; i++) {
+            const bool is_free = (mask & 0x1) == 0;
+            if (is_free) {
+                if (status[i] == EventState::Registered || status[i] == EventState::Free) {
+                    return {i};
+                }
+            }
+            mask = mask >> 1;
+        }
+        return {};
+    }
+    void SetEventStatus(const u32 event_id, EventState new_status) {
+        EventState old_status = status[event_id];
+        if (old_status == new_status) {
+            return;
+        }
+        status[event_id] = new_status;
+        if (new_status == EventState::Registered) {
+            registered[event_id] = true;
+        }
+        if (new_status == EventState::Waiting || new_status == EventState::Busy) {
+            events_mask |= (1ULL << event_id);
+        }
+    }
+    void RegisterEvent(const u32 event_id) {
+        registered[event_id] = true;
+        if (status[event_id] == EventState::Free) {
+            status[event_id] = EventState::Registered;
+        }
+    }
+    void UnregisterEvent(const u32 event_id) {
+        registered[event_id] = false;
+        if (status[event_id] == EventState::Registered) {
+            status[event_id] = EventState::Free;
+        }
+    }
+    void LiberateEvent(const u32 event_id) {
+        status[event_id] = registered[event_id] ? EventState::Registered : EventState::Free;
+        events_mask &= ~(1ULL << event_id);
+        assigned_syncpt[event_id] = unassigned_syncpt;
+        assigned_value[event_id] = 0;
+    }
 };
 
-static_assert(sizeof(IoctlFence) == 8, "IoctlFence has wrong size");
-
 class Module final {
 public:
-    Module();
+    Module(Core::System& system);
     ~Module();
 
     /// Returns a pointer to one of the available devices, identified by its name.
@@ -44,10 +106,17 @@ public:
     /// Opens a device node and returns a file descriptor to it.
     u32 Open(const std::string& device_name);
     /// Sends an ioctl command to the specified file descriptor.
-    u32 Ioctl(u32 fd, u32 command, const std::vector<u8>& input, std::vector<u8>& output);
+    u32 Ioctl(u32 fd, u32 command, const std::vector<u8>& input, std::vector<u8>& output,
+              IoctlCtrl& ctrl);
     /// Closes a device file descriptor and returns operation success.
     ResultCode Close(u32 fd);
 
+    void SignalSyncpt(const u32 syncpoint_id, const u32 value);
+
+    Kernel::SharedPtr<Kernel::ReadableEvent> GetEvent(u32 event_id) const;
+
+    Kernel::SharedPtr<Kernel::WritableEvent> GetEventWriteable(u32 event_id) const;
+
 private:
     /// Id to use for the next open file descriptor.
     u32 next_fd = 1;
@@ -57,9 +126,12 @@ private:
 
     /// Mapping of device node names to their implementation.
     std::unordered_map<std::string, std::shared_ptr<Devices::nvdevice>> devices;
+
+    EventInterface events_interface;
 };
 
 /// Registers all NVDRV services with the specified service manager.
-void InstallInterfaces(SM::ServiceManager& service_manager, NVFlinger::NVFlinger& nvflinger);
+void InstallInterfaces(SM::ServiceManager& service_manager, NVFlinger::NVFlinger& nvflinger,
+                       Core::System& system);
 
 } // namespace Service::Nvidia
diff --git a/src/core/hle/service/nvflinger/buffer_queue.cpp b/src/core/hle/service/nvflinger/buffer_queue.cpp
index 5731e815f..e1a07d3ee 100644
--- a/src/core/hle/service/nvflinger/buffer_queue.cpp
+++ b/src/core/hle/service/nvflinger/buffer_queue.cpp
@@ -34,7 +34,8 @@ void BufferQueue::SetPreallocatedBuffer(u32 slot, const IGBPBuffer& igbp_buffer)
     buffer_wait_event.writable->Signal();
 }
 
-std::optional<u32> BufferQueue::DequeueBuffer(u32 width, u32 height) {
+std::optional<std::pair<u32, Service::Nvidia::MultiFence*>> BufferQueue::DequeueBuffer(u32 width,
+                                                                                       u32 height) {
     auto itr = std::find_if(queue.begin(), queue.end(), [&](const Buffer& buffer) {
         // Only consider free buffers. Buffers become free once again after they've been Acquired
         // and Released by the compositor, see the NVFlinger::Compose method.
@@ -51,7 +52,7 @@ std::optional<u32> BufferQueue::DequeueBuffer(u32 width, u32 height) {
     }
 
     itr->status = Buffer::Status::Dequeued;
-    return itr->slot;
+    return {{itr->slot, &itr->multi_fence}};
 }
 
 const IGBPBuffer& BufferQueue::RequestBuffer(u32 slot) const {
@@ -63,7 +64,8 @@ const IGBPBuffer& BufferQueue::RequestBuffer(u32 slot) const {
 }
 
 void BufferQueue::QueueBuffer(u32 slot, BufferTransformFlags transform,
-                              const Common::Rectangle<int>& crop_rect) {
+                              const Common::Rectangle<int>& crop_rect, u32 swap_interval,
+                              Service::Nvidia::MultiFence& multi_fence) {
     auto itr = std::find_if(queue.begin(), queue.end(),
                             [&](const Buffer& buffer) { return buffer.slot == slot; });
     ASSERT(itr != queue.end());
@@ -71,12 +73,21 @@ void BufferQueue::QueueBuffer(u32 slot, BufferTransformFlags transform,
     itr->status = Buffer::Status::Queued;
     itr->transform = transform;
     itr->crop_rect = crop_rect;
+    itr->swap_interval = swap_interval;
+    itr->multi_fence = multi_fence;
+    queue_sequence.push_back(slot);
 }
 
 std::optional<std::reference_wrapper<const BufferQueue::Buffer>> BufferQueue::AcquireBuffer() {
-    auto itr = std::find_if(queue.begin(), queue.end(), [](const Buffer& buffer) {
-        return buffer.status == Buffer::Status::Queued;
-    });
+    auto itr = queue.end();
+    // Iterate to find a queued buffer matching the requested slot.
+    while (itr == queue.end() && !queue_sequence.empty()) {
+        u32 slot = queue_sequence.front();
+        itr = std::find_if(queue.begin(), queue.end(), [&slot](const Buffer& buffer) {
+            return buffer.status == Buffer::Status::Queued && buffer.slot == slot;
+        });
+        queue_sequence.pop_front();
+    }
     if (itr == queue.end())
         return {};
     itr->status = Buffer::Status::Acquired;
diff --git a/src/core/hle/service/nvflinger/buffer_queue.h b/src/core/hle/service/nvflinger/buffer_queue.h
index e1ccb6171..356bedb81 100644
--- a/src/core/hle/service/nvflinger/buffer_queue.h
+++ b/src/core/hle/service/nvflinger/buffer_queue.h
@@ -4,6 +4,7 @@
 
 #pragma once
 
+#include <list>
 #include <optional>
 #include <vector>
 
@@ -12,6 +13,7 @@
 #include "common/swap.h"
 #include "core/hle/kernel/object.h"
 #include "core/hle/kernel/writable_event.h"
+#include "core/hle/service/nvdrv/nvdata.h"
 
 namespace Service::NVFlinger {
 
@@ -68,13 +70,17 @@ public:
         IGBPBuffer igbp_buffer;
         BufferTransformFlags transform;
         Common::Rectangle<int> crop_rect;
+        u32 swap_interval;
+        Service::Nvidia::MultiFence multi_fence;
     };
 
     void SetPreallocatedBuffer(u32 slot, const IGBPBuffer& igbp_buffer);
-    std::optional<u32> DequeueBuffer(u32 width, u32 height);
+    std::optional<std::pair<u32, Service::Nvidia::MultiFence*>> DequeueBuffer(u32 width,
+                                                                              u32 height);
     const IGBPBuffer& RequestBuffer(u32 slot) const;
     void QueueBuffer(u32 slot, BufferTransformFlags transform,
-                     const Common::Rectangle<int>& crop_rect);
+                     const Common::Rectangle<int>& crop_rect, u32 swap_interval,
+                     Service::Nvidia::MultiFence& multi_fence);
     std::optional<std::reference_wrapper<const Buffer>> AcquireBuffer();
     void ReleaseBuffer(u32 slot);
     u32 Query(QueryType type);
@@ -92,6 +98,7 @@ private:
     u64 layer_id;
 
     std::vector<Buffer> queue;
+    std::list<u32> queue_sequence;
     Kernel::EventPair buffer_wait_event;
 };
 
diff --git a/src/core/hle/service/nvflinger/nvflinger.cpp b/src/core/hle/service/nvflinger/nvflinger.cpp
index 3c5c53e24..f9db79370 100644
--- a/src/core/hle/service/nvflinger/nvflinger.cpp
+++ b/src/core/hle/service/nvflinger/nvflinger.cpp
@@ -37,15 +37,14 @@ NVFlinger::NVFlinger(Core::Timing::CoreTiming& core_timing) : core_timing{core_t
     displays.emplace_back(4, "Null");
 
     // Schedule the screen composition events
-    const auto ticks = Settings::values.force_30fps_mode ? frame_ticks_30fps : frame_ticks;
-
-    composition_event = core_timing.RegisterEvent(
-        "ScreenComposition", [this, ticks](u64 userdata, s64 cycles_late) {
-            Compose();
-            this->core_timing.ScheduleEvent(ticks - cycles_late, composition_event);
-        });
-
-    core_timing.ScheduleEvent(ticks, composition_event);
+    composition_event = core_timing.RegisterEvent("ScreenComposition", [this](u64 userdata,
+                                                                              s64 cycles_late) {
+        Compose();
+        const auto ticks = Settings::values.force_30fps_mode ? frame_ticks_30fps : GetNextTicks();
+        this->core_timing.ScheduleEvent(std::max<s64>(0LL, ticks - cycles_late), composition_event);
+    });
+
+    core_timing.ScheduleEvent(frame_ticks, composition_event);
 }
 
 NVFlinger::~NVFlinger() {
@@ -206,8 +205,14 @@ void NVFlinger::Compose() {
                      igbp_buffer.width, igbp_buffer.height, igbp_buffer.stride,
                      buffer->get().transform, buffer->get().crop_rect);
 
+        swap_interval = buffer->get().swap_interval;
         buffer_queue.ReleaseBuffer(buffer->get().slot);
     }
 }
 
+s64 NVFlinger::GetNextTicks() const {
+    constexpr s64 max_hertz = 120LL;
+    return (Core::Timing::BASE_CLOCK_RATE * (1LL << swap_interval)) / max_hertz;
+}
+
 } // namespace Service::NVFlinger
diff --git a/src/core/hle/service/nvflinger/nvflinger.h b/src/core/hle/service/nvflinger/nvflinger.h
index c0a83fffb..988be8726 100644
--- a/src/core/hle/service/nvflinger/nvflinger.h
+++ b/src/core/hle/service/nvflinger/nvflinger.h
@@ -74,6 +74,8 @@ public:
     /// finished.
     void Compose();
 
+    s64 GetNextTicks() const;
+
 private:
     /// Finds the display identified by the specified ID.
     VI::Display* FindDisplay(u64 display_id);
@@ -98,6 +100,8 @@ private:
     /// layers.
     u32 next_buffer_queue_id = 1;
 
+    u32 swap_interval = 1;
+
     /// Event that handles screen composition.
     Core::Timing::EventType* composition_event;
 
diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp
index 7eefd733f..2daa1ae49 100644
--- a/src/core/hle/service/service.cpp
+++ b/src/core/hle/service/service.cpp
@@ -236,7 +236,7 @@ void Init(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system) {
     NIM::InstallInterfaces(*sm);
     NPNS::InstallInterfaces(*sm);
     NS::InstallInterfaces(*sm);
-    Nvidia::InstallInterfaces(*sm, *nv_flinger);
+    Nvidia::InstallInterfaces(*sm, *nv_flinger, system);
     PCIe::InstallInterfaces(*sm);
     PCTL::InstallInterfaces(*sm);
     PCV::InstallInterfaces(*sm);
diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp
index f1fa6ccd1..199b30635 100644
--- a/src/core/hle/service/vi/vi.cpp
+++ b/src/core/hle/service/vi/vi.cpp
@@ -21,6 +21,7 @@
 #include "core/hle/kernel/readable_event.h"
 #include "core/hle/kernel/thread.h"
 #include "core/hle/kernel/writable_event.h"
+#include "core/hle/service/nvdrv/nvdata.h"
 #include "core/hle/service/nvdrv/nvdrv.h"
 #include "core/hle/service/nvflinger/buffer_queue.h"
 #include "core/hle/service/nvflinger/nvflinger.h"
@@ -328,32 +329,22 @@ public:
     Data data;
 };
 
-struct BufferProducerFence {
-    u32 is_valid;
-    std::array<Nvidia::IoctlFence, 4> fences;
-};
-static_assert(sizeof(BufferProducerFence) == 36, "BufferProducerFence has wrong size");
-
 class IGBPDequeueBufferResponseParcel : public Parcel {
 public:
-    explicit IGBPDequeueBufferResponseParcel(u32 slot) : slot(slot) {}
+    explicit IGBPDequeueBufferResponseParcel(u32 slot, Service::Nvidia::MultiFence& multi_fence)
+        : slot(slot), multi_fence(multi_fence) {}
     ~IGBPDequeueBufferResponseParcel() override = default;
 
 protected:
     void SerializeData() override {
-        // TODO(Subv): Find out how this Fence is used.
-        BufferProducerFence fence = {};
-        fence.is_valid = 1;
-        for (auto& fence_ : fence.fences)
-            fence_.id = -1;
-
         Write(slot);
         Write<u32_le>(1);
-        WriteObject(fence);
+        WriteObject(multi_fence);
         Write<u32_le>(0);
     }
 
     u32_le slot;
+    Service::Nvidia::MultiFence multi_fence;
 };
 
 class IGBPRequestBufferRequestParcel : public Parcel {
@@ -400,12 +391,6 @@ public:
         data = Read<Data>();
     }
 
-    struct Fence {
-        u32_le id;
-        u32_le value;
-    };
-    static_assert(sizeof(Fence) == 8, "Fence has wrong size");
-
     struct Data {
         u32_le slot;
         INSERT_PADDING_WORDS(3);
@@ -418,15 +403,15 @@ public:
         s32_le scaling_mode;
         NVFlinger::BufferQueue::BufferTransformFlags transform;
         u32_le sticky_transform;
-        INSERT_PADDING_WORDS(2);
-        u32_le fence_is_valid;
-        std::array<Fence, 2> fences;
+        INSERT_PADDING_WORDS(1);
+        u32_le swap_interval;
+        Service::Nvidia::MultiFence multi_fence;
 
         Common::Rectangle<int> GetCropRect() const {
             return {crop_left, crop_top, crop_right, crop_bottom};
         }
     };
-    static_assert(sizeof(Data) == 80, "ParcelData has wrong size");
+    static_assert(sizeof(Data) == 96, "ParcelData has wrong size");
 
     Data data;
 };
@@ -547,11 +532,11 @@ private:
             IGBPDequeueBufferRequestParcel request{ctx.ReadBuffer()};
             const u32 width{request.data.width};
             const u32 height{request.data.height};
-            std::optional<u32> slot = buffer_queue.DequeueBuffer(width, height);
+            auto result = buffer_queue.DequeueBuffer(width, height);
 
-            if (slot) {
+            if (result) {
                 // Buffer is available
-                IGBPDequeueBufferResponseParcel response{*slot};
+                IGBPDequeueBufferResponseParcel response{result->first, *result->second};
                 ctx.WriteBuffer(response.Serialize());
             } else {
                 // Wait the current thread until a buffer becomes available
@@ -561,10 +546,10 @@ private:
                         Kernel::ThreadWakeupReason reason) {
                         // Repeat TransactParcel DequeueBuffer when a buffer is available
                         auto& buffer_queue = nv_flinger->FindBufferQueue(id);
-                        std::optional<u32> slot = buffer_queue.DequeueBuffer(width, height);
-                        ASSERT_MSG(slot != std::nullopt, "Could not dequeue buffer.");
+                        auto result = buffer_queue.DequeueBuffer(width, height);
+                        ASSERT_MSG(result != std::nullopt, "Could not dequeue buffer.");
 
-                        IGBPDequeueBufferResponseParcel response{*slot};
+                        IGBPDequeueBufferResponseParcel response{result->first, *result->second};
                         ctx.WriteBuffer(response.Serialize());
                         IPC::ResponseBuilder rb{ctx, 2};
                         rb.Push(RESULT_SUCCESS);
@@ -582,7 +567,8 @@ private:
             IGBPQueueBufferRequestParcel request{ctx.ReadBuffer()};
 
             buffer_queue.QueueBuffer(request.data.slot, request.data.transform,
-                                     request.data.GetCropRect());
+                                     request.data.GetCropRect(), request.data.swap_interval,
+                                     request.data.multi_fence);
 
             IGBPQueueBufferResponseParcel response{1280, 720};
             ctx.WriteBuffer(response.Serialize());
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index 74c46ec04..125c53360 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -525,8 +525,9 @@ void Maxwell3D::ProcessSyncPoint() {
     const u32 sync_point = regs.sync_info.sync_point.Value();
     const u32 increment = regs.sync_info.increment.Value();
     const u32 cache_flush = regs.sync_info.unknown.Value();
-    LOG_DEBUG(HW_GPU, "Syncpoint set {}, increment: {}, unk: {}", sync_point, increment,
-              cache_flush);
+    if (increment) {
+        system.GPU().IncrementSyncPoint(sync_point);
+    }
 }
 
 void Maxwell3D::DrawArrays() {
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index 21007d8b2..1622332a4 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -29,7 +29,8 @@ u32 FramebufferConfig::BytesPerPixel(PixelFormat format) {
     UNREACHABLE();
 }
 
-GPU::GPU(Core::System& system, VideoCore::RendererBase& renderer) : renderer{renderer} {
+GPU::GPU(Core::System& system, VideoCore::RendererBase& renderer, bool is_async)
+    : system{system}, renderer{renderer}, is_async{is_async} {
     auto& rasterizer{renderer.Rasterizer()};
     memory_manager = std::make_unique<Tegra::MemoryManager>(system, rasterizer);
     dma_pusher = std::make_unique<Tegra::DmaPusher>(*this);
@@ -74,6 +75,51 @@ const DmaPusher& GPU::DmaPusher() const {
     return *dma_pusher;
 }
 
+void GPU::IncrementSyncPoint(const u32 syncpoint_id) {
+    syncpoints[syncpoint_id]++;
+    std::lock_guard lock{sync_mutex};
+    if (!syncpt_interrupts[syncpoint_id].empty()) {
+        u32 value = syncpoints[syncpoint_id].load();
+        auto it = syncpt_interrupts[syncpoint_id].begin();
+        while (it != syncpt_interrupts[syncpoint_id].end()) {
+            if (value >= *it) {
+                TriggerCpuInterrupt(syncpoint_id, *it);
+                it = syncpt_interrupts[syncpoint_id].erase(it);
+                continue;
+            }
+            it++;
+        }
+    }
+}
+
+u32 GPU::GetSyncpointValue(const u32 syncpoint_id) const {
+    return syncpoints[syncpoint_id].load();
+}
+
+void GPU::RegisterSyncptInterrupt(const u32 syncpoint_id, const u32 value) {
+    auto& interrupt = syncpt_interrupts[syncpoint_id];
+    bool contains = std::any_of(interrupt.begin(), interrupt.end(),
+                                [value](u32 in_value) { return in_value == value; });
+    if (contains) {
+        return;
+    }
+    syncpt_interrupts[syncpoint_id].emplace_back(value);
+}
+
+bool GPU::CancelSyncptInterrupt(const u32 syncpoint_id, const u32 value) {
+    std::lock_guard lock{sync_mutex};
+    auto& interrupt = syncpt_interrupts[syncpoint_id];
+    const auto iter =
+        std::find_if(interrupt.begin(), interrupt.end(),
+                     [value](u32 interrupt_value) { return value == interrupt_value; });
+
+    if (iter == interrupt.end()) {
+        return false;
+    }
+    interrupt.erase(iter);
+    return true;
+}
+
 u32 RenderTargetBytesPerPixel(RenderTargetFormat format) {
     ASSERT(format != RenderTargetFormat::NONE);
 
diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h
index 0055e5326..87c96f46b 100644
--- a/src/video_core/gpu.h
+++ b/src/video_core/gpu.h
@@ -5,8 +5,12 @@
 #pragma once
 
 #include <array>
+#include <atomic>
+#include <list>
 #include <memory>
+#include <mutex>
 #include "common/common_types.h"
+#include "core/hle/service/nvdrv/nvdata.h"
 #include "core/hle/service/nvflinger/buffer_queue.h"
 #include "video_core/dma_pusher.h"
 
@@ -127,7 +131,7 @@ class MemoryManager;
 
 class GPU {
 public:
-    explicit GPU(Core::System& system, VideoCore::RendererBase& renderer);
+    explicit GPU(Core::System& system, VideoCore::RendererBase& renderer, bool is_async);
 
     virtual ~GPU();
 
@@ -170,6 +174,22 @@ public:
     /// Returns a reference to the GPU DMA pusher.
     Tegra::DmaPusher& DmaPusher();
 
+    void IncrementSyncPoint(u32 syncpoint_id);
+
+    u32 GetSyncpointValue(u32 syncpoint_id) const;
+
+    void RegisterSyncptInterrupt(u32 syncpoint_id, u32 value);
+
+    bool CancelSyncptInterrupt(u32 syncpoint_id, u32 value);
+
+    std::unique_lock<std::mutex> LockSync() {
+        return std::unique_lock{sync_mutex};
+    }
+
+    bool IsAsync() const {
+        return is_async;
+    }
+
     /// Returns a const reference to the GPU DMA pusher.
     const Tegra::DmaPusher& DmaPusher() const;
 
@@ -239,6 +259,9 @@ public:
     /// Notify rasterizer that any caches of the specified region should be flushed and invalidated
     virtual void FlushAndInvalidateRegion(CacheAddr addr, u64 size) = 0;
 
+protected:
+    virtual void TriggerCpuInterrupt(u32 syncpoint_id, u32 value) const = 0;
+
 private:
     void ProcessBindMethod(const MethodCall& method_call);
     void ProcessSemaphoreTriggerMethod();
@@ -257,6 +280,7 @@ private:
 protected:
     std::unique_ptr<Tegra::DmaPusher> dma_pusher;
     VideoCore::RendererBase& renderer;
+    Core::System& system;
 
 private:
     std::unique_ptr<Tegra::MemoryManager> memory_manager;
@@ -273,6 +297,14 @@ private:
     std::unique_ptr<Engines::MaxwellDMA> maxwell_dma;
     /// Inline memory engine
     std::unique_ptr<Engines::KeplerMemory> kepler_memory;
+
+    std::array<std::atomic<u32>, Service::Nvidia::MaxSyncPoints> syncpoints{};
+
+    std::array<std::list<u32>, Service::Nvidia::MaxSyncPoints> syncpt_interrupts;
+
+    std::mutex sync_mutex;
+
+    const bool is_async;
 };
 
 #define ASSERT_REG_POSITION(field_name, position)                                                  \
diff --git a/src/video_core/gpu_asynch.cpp b/src/video_core/gpu_asynch.cpp
index d4e2553a9..ea67be831 100644
--- a/src/video_core/gpu_asynch.cpp
+++ b/src/video_core/gpu_asynch.cpp
@@ -2,6 +2,8 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include "core/core.h"
+#include "core/hardware_interrupt_manager.h"
 #include "video_core/gpu_asynch.h"
 #include "video_core/gpu_thread.h"
 #include "video_core/renderer_base.h"
@@ -9,7 +11,7 @@
 namespace VideoCommon {
 
 GPUAsynch::GPUAsynch(Core::System& system, VideoCore::RendererBase& renderer)
-    : GPU(system, renderer), gpu_thread{system} {}
+    : GPU(system, renderer, true), gpu_thread{system} {}
 
 GPUAsynch::~GPUAsynch() = default;
 
@@ -38,4 +40,9 @@ void GPUAsynch::FlushAndInvalidateRegion(CacheAddr addr, u64 size) {
     gpu_thread.FlushAndInvalidateRegion(addr, size);
 }
 
+void GPUAsynch::TriggerCpuInterrupt(const u32 syncpoint_id, const u32 value) const {
+    auto& interrupt_manager = system.InterruptManager();
+    interrupt_manager.GPUInterruptSyncpt(syncpoint_id, value);
+}
+
 } // namespace VideoCommon
diff --git a/src/video_core/gpu_asynch.h b/src/video_core/gpu_asynch.h
index 30be74cba..36377d677 100644
--- a/src/video_core/gpu_asynch.h
+++ b/src/video_core/gpu_asynch.h
@@ -27,6 +27,9 @@ public:
     void InvalidateRegion(CacheAddr addr, u64 size) override;
     void FlushAndInvalidateRegion(CacheAddr addr, u64 size) override;
 
+protected:
+    void TriggerCpuInterrupt(u32 syncpoint_id, u32 value) const override;
+
 private:
     GPUThread::ThreadManager gpu_thread;
 };
diff --git a/src/video_core/gpu_synch.cpp b/src/video_core/gpu_synch.cpp
index 45e43b1dc..d4ead9c47 100644
--- a/src/video_core/gpu_synch.cpp
+++ b/src/video_core/gpu_synch.cpp
@@ -8,7 +8,7 @@
 namespace VideoCommon {
 
 GPUSynch::GPUSynch(Core::System& system, VideoCore::RendererBase& renderer)
-    : GPU(system, renderer) {}
+    : GPU(system, renderer, false) {}
 
 GPUSynch::~GPUSynch() = default;
 
diff --git a/src/video_core/gpu_synch.h b/src/video_core/gpu_synch.h
index 3031fcf72..07bcc47f1 100644
--- a/src/video_core/gpu_synch.h
+++ b/src/video_core/gpu_synch.h
@@ -25,6 +25,10 @@ public:
     void FlushRegion(CacheAddr addr, u64 size) override;
     void InvalidateRegion(CacheAddr addr, u64 size) override;
     void FlushAndInvalidateRegion(CacheAddr addr, u64 size) override;
+
+protected:
+    void TriggerCpuInterrupt([[maybe_unused]] u32 syncpoint_id,
+                             [[maybe_unused]] u32 value) const override {}
 };
 
 } // namespace VideoCommon
diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp
index 3f0939ec9..b441e92b0 100644
--- a/src/video_core/gpu_thread.cpp
+++ b/src/video_core/gpu_thread.cpp
@@ -21,7 +21,8 @@ static void RunThread(VideoCore::RendererBase& renderer, Tegra::DmaPusher& dma_p
     MicroProfileOnThreadCreate("GpuThread");
 
     // Wait for first GPU command before acquiring the window context
-    state.WaitForCommands();
+    while (state.queue.Empty())
+        ;
 
     // If emulation was stopped during disk shader loading, abort before trying to acquire context
     if (!state.is_running) {
@@ -32,7 +33,6 @@ static void RunThread(VideoCore::RendererBase& renderer, Tegra::DmaPusher& dma_p
 
     CommandDataContainer next;
     while (state.is_running) {
-        state.WaitForCommands();
         while (!state.queue.Empty()) {
             state.queue.Pop(next);
             if (const auto submit_list = std::get_if<SubmitListCommand>(&next.data)) {
@@ -49,8 +49,7 @@ static void RunThread(VideoCore::RendererBase& renderer, Tegra::DmaPusher& dma_p
             } else {
                 UNREACHABLE();
             }
-            state.signaled_fence = next.fence;
-            state.TrySynchronize();
+            state.signaled_fence.store(next.fence);
         }
     }
 }
@@ -89,12 +88,7 @@ void ThreadManager::FlushRegion(CacheAddr addr, u64 size) {
 }
 
 void ThreadManager::InvalidateRegion(CacheAddr addr, u64 size) {
-    if (state.queue.Empty()) {
-        // It's quicker to invalidate a single region on the CPU if the queue is already empty
-        system.Renderer().Rasterizer().InvalidateRegion(addr, size);
-    } else {
-        PushCommand(InvalidateRegionCommand(addr, size));
-    }
+    system.Renderer().Rasterizer().InvalidateRegion(addr, size);
 }
 
 void ThreadManager::FlushAndInvalidateRegion(CacheAddr addr, u64 size) {
@@ -105,22 +99,13 @@ void ThreadManager::FlushAndInvalidateRegion(CacheAddr addr, u64 size) {
 u64 ThreadManager::PushCommand(CommandData&& command_data) {
     const u64 fence{++state.last_fence};
     state.queue.Push(CommandDataContainer(std::move(command_data), fence));
-    state.SignalCommands();
     return fence;
 }
 
 MICROPROFILE_DEFINE(GPU_wait, "GPU", "Wait for the GPU", MP_RGB(128, 128, 192));
 void SynchState::WaitForSynchronization(u64 fence) {
-    if (signaled_fence >= fence) {
-        return;
-    }
-
-    // Wait for the GPU to be idle (all commands to be executed)
-    {
-        MICROPROFILE_SCOPE(GPU_wait);
-        std::unique_lock lock{synchronization_mutex};
-        synchronization_condition.wait(lock, [this, fence] { return signaled_fence >= fence; });
-    }
+    while (signaled_fence.load() < fence)
+        ;
 }
 
 } // namespace VideoCommon::GPUThread
diff --git a/src/video_core/gpu_thread.h b/src/video_core/gpu_thread.h
index 05a168a72..1d9d0c39e 100644
--- a/src/video_core/gpu_thread.h
+++ b/src/video_core/gpu_thread.h
@@ -88,41 +88,9 @@ struct CommandDataContainer {
 /// Struct used to synchronize the GPU thread
 struct SynchState final {
     std::atomic_bool is_running{true};
-    std::atomic_int queued_frame_count{};
-    std::mutex synchronization_mutex;
-    std::mutex commands_mutex;
-    std::condition_variable commands_condition;
-    std::condition_variable synchronization_condition;
-
-    /// Returns true if the gap in GPU commands is small enough that we can consider the CPU and GPU
-    /// synchronized. This is entirely empirical.
-    bool IsSynchronized() const {
-        constexpr std::size_t max_queue_gap{5};
-        return queue.Size() <= max_queue_gap;
-    }
-
-    void TrySynchronize() {
-        if (IsSynchronized()) {
-            std::lock_guard lock{synchronization_mutex};
-            synchronization_condition.notify_one();
-        }
-    }
 
     void WaitForSynchronization(u64 fence);
 
-    void SignalCommands() {
-        if (queue.Empty()) {
-            return;
-        }
-
-        commands_condition.notify_one();
-    }
-
-    void WaitForCommands() {
-        std::unique_lock lock{commands_mutex};
-        commands_condition.wait(lock, [this] { return !queue.Empty(); });
-    }
-
     using CommandQueue = Common::SPSCQueue<CommandDataContainer>;
     CommandQueue queue;
     u64 last_fence{};