diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp
index ee99ab280..45711d686 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.cpp
@@ -17,6 +17,8 @@ u32 nvhost_ctrl::ioctl(Ioctl command, const std::vector<u8>& input, std::vector<
     switch (static_cast<IoctlCommand>(command.raw)) {
     case IoctlCommand::IocGetConfigCommand:
         return NvOsGetConfigU32(input, output);
+    case IoctlCommand::IocCtrlEventWaitCommand:
+        return IocCtrlEventWait(input, output);
     }
     UNIMPLEMENTED();
     return 0;
@@ -45,6 +47,18 @@ u32 nvhost_ctrl::NvOsGetConfigU32(const std::vector<u8>& input, std::vector<u8>&
     return 0;
 }
 
+u32 nvhost_ctrl::IocCtrlEventWait(const std::vector<u8>& input, std::vector<u8>& output) {
+    IocCtrlEventWaitParams params{};
+    std::memcpy(&params, input.data(), sizeof(params));
+    LOG_WARNING(Service_NVDRV, "(STUBBED) called, syncpt_id=%u threshold=%u timeout=%d",
+                params.syncpt_id, params.threshold, params.timeout);
+
+    // TODO(Subv): Implement actual syncpt waiting.
+    params.value = 0;
+    std::memcpy(output.data(), &params, sizeof(params));
+    return 0;
+}
+
 } // namespace Devices
 } // namespace Nvidia
 } // namespace Service
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.h b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.h
index fd02a5e45..0ca01aa6d 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl.h
@@ -31,6 +31,7 @@ private:
         IocModuleRegRDWRCommand = 0xC008010E,
         IocSyncptWaitexCommand = 0xC0100019,
         IocSyncptReadMaxCommand = 0xC008001A,
+        IocCtrlEventWaitCommand = 0xC010001D,
         IocGetConfigCommand = 0xC183001B,
     };
 
@@ -41,7 +42,17 @@ private:
     };
     static_assert(sizeof(IocGetConfigParams) == 387, "IocGetConfigParams is incorrect size");
 
+    struct IocCtrlEventWaitParams {
+        u32_le syncpt_id;
+        u32_le threshold;
+        s32_le timeout;
+        u32_le value;
+    };
+    static_assert(sizeof(IocCtrlEventWaitParams) == 16, "IocCtrlEventWaitParams is incorrect size");
+
     u32 NvOsGetConfigU32(const std::vector<u8>& input, std::vector<u8>& output);
+
+    u32 IocCtrlEventWait(const std::vector<u8>& input, std::vector<u8>& output);
 };
 
 } // namespace Devices
diff --git a/src/core/hle/service/nvdrv/devices/nvmap.cpp b/src/core/hle/service/nvdrv/devices/nvmap.cpp
index cd8c0c605..b3842eb4c 100644
--- a/src/core/hle/service/nvdrv/devices/nvmap.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvmap.cpp
@@ -103,11 +103,8 @@ u32 nvmap::IocFromId(const std::vector<u8>& input, std::vector<u8>& output) {
                             [&](const auto& entry) { return entry.second->id == params.id; });
     ASSERT(itr != handles.end());
 
-    // Make a new handle for the object
-    u32 handle = next_handle++;
-    handles[handle] = itr->second;
-
-    params.handle = handle;
+    // Return the existing handle instead of creating a new one.
+    params.handle = itr->first;
 
     std::memcpy(output.data(), &params, sizeof(params));
     return 0;
diff --git a/src/core/hle/service/nvdrv/nvdrv.h b/src/core/hle/service/nvdrv/nvdrv.h
index e44644624..6a55ff96d 100644
--- a/src/core/hle/service/nvdrv/nvdrv.h
+++ b/src/core/hle/service/nvdrv/nvdrv.h
@@ -17,6 +17,13 @@ namespace Devices {
 class nvdevice;
 }
 
+struct IoctlFence {
+    u32 id;
+    u32 value;
+};
+
+static_assert(sizeof(IoctlFence) == 8, "IoctlFence has wrong size");
+
 class Module final {
 public:
     Module();
diff --git a/src/core/hle/service/vi/vi.cpp b/src/core/hle/service/vi/vi.cpp
index ff5005f71..0aa621dfe 100644
--- a/src/core/hle/service/vi/vi.cpp
+++ b/src/core/hle/service/vi/vi.cpp
@@ -8,6 +8,7 @@
 #include "common/scope_exit.h"
 #include "core/core_timing.h"
 #include "core/hle/ipc_helpers.h"
+#include "core/hle/service/nvdrv/nvdrv.h"
 #include "core/hle/service/nvflinger/buffer_queue.h"
 #include "core/hle/service/vi/vi.h"
 #include "core/hle/service/vi/vi_m.h"
@@ -38,6 +39,7 @@ public:
 
     template <typename T>
     T Read() {
+        ASSERT(read_index + sizeof(T) <= buffer.size());
         T val;
         std::memcpy(&val, buffer.data() + read_index, sizeof(T));
         read_index += sizeof(T);
@@ -47,6 +49,7 @@ public:
 
     template <typename T>
     T ReadUnaligned() {
+        ASSERT(read_index + sizeof(T) <= buffer.size());
         T val;
         std::memcpy(&val, buffer.data() + read_index, sizeof(T));
         read_index += sizeof(T);
@@ -54,6 +57,7 @@ public:
     }
 
     std::vector<u8> ReadBlock(size_t length) {
+        ASSERT(read_index + length <= buffer.size());
         const u8* const begin = buffer.data() + read_index;
         const u8* const end = begin + length;
         std::vector<u8> data(begin, end);
@@ -86,7 +90,18 @@ public:
         write_index = Common::AlignUp(write_index, 4);
     }
 
+    template <typename T>
+    void WriteObject(const T& val) {
+        u32_le size = static_cast<u32>(sizeof(val));
+        Write(size);
+        // TODO(Subv): Support file descriptors.
+        Write<u32_le>(0); // Fd count.
+        Write(val);
+    }
+
     void Deserialize() {
+        ASSERT(buffer.size() > sizeof(Header));
+
         Header header{};
         std::memcpy(&header, buffer.data(), sizeof(Header));
 
@@ -262,10 +277,11 @@ public:
     Data data;
 };
 
-// TODO(bunnei): Remove this. When set to 1, games will think a fence is valid and boot further.
-// This will break libnx and potentially other apps that more stringently check this. This is here
-// purely as a convenience, and should go away once we implement fences.
-static constexpr u32 FENCE_HACK = 0;
+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:
@@ -274,20 +290,16 @@ public:
 
 protected:
     void SerializeData() override {
-        // TODO(bunnei): Find out what this all means. Writing anything non-zero here breaks libnx.
-        Write<u32>(0);
-        Write<u32>(FENCE_HACK);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
-        Write<u32>(0);
+        // 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);
+        Write<u32_le>(0);
     }
 
     u32_le slot;
@@ -316,11 +328,10 @@ public:
 
 protected:
     void SerializeData() override {
-        // TODO(bunnei): Find out what this all means. Writing anything non-zero here breaks libnx.
-        Write<u32_le>(0);
-        Write<u32_le>(FENCE_HACK);
-        Write<u32_le>(0);
-        Write(buffer);
+        // TODO(Subv): Figure out what this value means, writing non-zero here will make libnx try
+        // to read an IGBPBuffer object from the parcel.
+        Write<u32_le>(1);
+        WriteObject(buffer);
         Write<u32_le>(0);
     }