diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 889a2d2f80..83da304182 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -149,8 +149,6 @@ add_library(core STATIC
     hle/kernel/svc_results.h
     hle/kernel/global_scheduler_context.cpp
     hle/kernel/global_scheduler_context.h
-    hle/kernel/handle_table.cpp
-    hle/kernel/handle_table.h
     hle/kernel/hle_ipc.cpp
     hle/kernel/hle_ipc.h
     hle/kernel/init/init_slab_setup.cpp
@@ -174,6 +172,8 @@ add_library(core STATIC
     hle/kernel/k_condition_variable.h
     hle/kernel/k_event.cpp
     hle/kernel/k_event.h
+    hle/kernel/k_handle_table.cpp
+    hle/kernel/k_handle_table.h
     hle/kernel/k_light_condition_variable.h
     hle/kernel/k_light_lock.cpp
     hle/kernel/k_light_lock.h
diff --git a/src/core/hle/kernel/handle_table.cpp b/src/core/hle/kernel/handle_table.cpp
deleted file mode 100644
index 16c528f5b5..0000000000
--- a/src/core/hle/kernel/handle_table.cpp
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright 2014 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#include <utility>
-#include "common/assert.h"
-#include "common/logging/log.h"
-#include "core/core.h"
-#include "core/hle/kernel/handle_table.h"
-#include "core/hle/kernel/k_process.h"
-#include "core/hle/kernel/k_scheduler.h"
-#include "core/hle/kernel/k_thread.h"
-#include "core/hle/kernel/kernel.h"
-#include "core/hle/kernel/svc_results.h"
-
-namespace Kernel {
-namespace {
-constexpr u16 GetSlot(Handle handle) {
-    return static_cast<u16>(handle >> 15);
-}
-
-constexpr u16 GetGeneration(Handle handle) {
-    return static_cast<u16>(handle & 0x7FFF);
-}
-} // Anonymous namespace
-
-HandleTable::HandleTable(KernelCore& kernel) : kernel{kernel} {
-    Clear();
-}
-
-HandleTable::~HandleTable() = default;
-
-ResultCode HandleTable::SetSize(s32 handle_table_size) {
-    if (static_cast<u32>(handle_table_size) > MAX_COUNT) {
-        LOG_ERROR(Kernel, "Handle table size {} is greater than {}", handle_table_size, MAX_COUNT);
-        return ResultOutOfMemory;
-    }
-
-    // Values less than or equal to zero indicate to use the maximum allowable
-    // size for the handle table in the actual kernel, so we ignore the given
-    // value in that case, since we assume this by default unless this function
-    // is called.
-    if (handle_table_size > 0) {
-        table_size = static_cast<u16>(handle_table_size);
-    }
-
-    return RESULT_SUCCESS;
-}
-
-ResultCode HandleTable::Add(Handle* out_handle, KAutoObject* obj, u16 type) {
-    ASSERT(obj != nullptr);
-
-    const u16 slot = next_free_slot;
-    if (slot >= table_size) {
-        LOG_ERROR(Kernel, "Unable to allocate Handle, too many slots in use.");
-        return ResultOutOfHandles;
-    }
-    next_free_slot = generations[slot];
-
-    const u16 generation = next_generation++;
-
-    // Overflow count so it fits in the 15 bits dedicated to the generation in the handle.
-    // Horizon OS uses zero to represent an invalid handle, so skip to 1.
-    if (next_generation >= (1 << 15)) {
-        next_generation = 1;
-    }
-
-    generations[slot] = generation;
-    objects[slot] = obj;
-    obj->Open();
-
-    *out_handle = generation | (slot << 15);
-
-    return RESULT_SUCCESS;
-}
-
-ResultVal<Handle> HandleTable::Duplicate(Handle handle) {
-    auto object = GetObject(handle);
-    if (object.IsNull()) {
-        LOG_ERROR(Kernel, "Tried to duplicate invalid handle: {:08X}", handle);
-        return ResultInvalidHandle;
-    }
-
-    Handle out_handle{};
-    R_TRY(Add(&out_handle, object.GetPointerUnsafe()));
-
-    return MakeResult(out_handle);
-}
-
-bool HandleTable::Remove(Handle handle) {
-    if (!IsValid(handle)) {
-        LOG_ERROR(Kernel, "Handle is not valid! handle={:08X}", handle);
-        return {};
-    }
-
-    const u16 slot = GetSlot(handle);
-
-    if (objects[slot]) {
-        objects[slot]->Close();
-    }
-
-    objects[slot] = nullptr;
-
-    generations[slot] = next_free_slot;
-    next_free_slot = slot;
-
-    return true;
-}
-
-bool HandleTable::IsValid(Handle handle) const {
-    const std::size_t slot = GetSlot(handle);
-    const u16 generation = GetGeneration(handle);
-    const bool is_object_valid = (objects[slot] != nullptr);
-    return slot < table_size && is_object_valid && generations[slot] == generation;
-}
-
-void HandleTable::Clear() {
-    for (u16 i = 0; i < table_size; ++i) {
-        generations[i] = static_cast<u16>(i + 1);
-        objects[i] = nullptr;
-    }
-    next_free_slot = 0;
-}
-
-} // namespace Kernel
diff --git a/src/core/hle/kernel/handle_table.h b/src/core/hle/kernel/handle_table.h
deleted file mode 100644
index 791e303d10..0000000000
--- a/src/core/hle/kernel/handle_table.h
+++ /dev/null
@@ -1,212 +0,0 @@
-// Copyright 2014 Citra Emulator Project
-// Licensed under GPLv2 or any later version
-// Refer to the license.txt file included.
-
-#pragma once
-
-#include <array>
-#include <cstddef>
-#include <memory>
-
-#include "common/common_types.h"
-#include "core/hle/kernel/k_auto_object.h"
-#include "core/hle/kernel/k_spin_lock.h"
-#include "core/hle/kernel/kernel.h"
-#include "core/hle/result.h"
-
-namespace Kernel {
-
-class KernelCore;
-
-enum KernelHandle : Handle {
-    InvalidHandle = 0,
-    CurrentThread = 0xFFFF8000,
-    CurrentProcess = 0xFFFF8001,
-};
-
-/**
- * This class allows the creation of Handles, which are references to objects that can be tested
- * for validity and looked up. Here they are used to pass references to kernel objects to/from the
- * emulated process. it has been designed so that it follows the same handle format and has
- * approximately the same restrictions as the handle manager in the CTR-OS.
- *
- * Handles contain two sub-fields: a slot index (bits 31:15) and a generation value (bits 14:0).
- * The slot index is used to index into the arrays in this class to access the data corresponding
- * to the Handle.
- *
- * To prevent accidental use of a freed Handle whose slot has already been reused, a global counter
- * is kept and incremented every time a Handle is created. This is the Handle's "generation". The
- * value of the counter is stored into the Handle as well as in the handle table (in the
- * "generations" array). When looking up a handle, the Handle's generation must match with the
- * value stored on the class, otherwise the Handle is considered invalid.
- *
- * To find free slots when allocating a Handle without needing to scan the entire object array, the
- * generations field of unallocated slots is re-purposed as a linked list of indices to free slots.
- * When a Handle is created, an index is popped off the list and used for the new Handle. When it
- * is destroyed, it is again pushed onto the list to be re-used by the next allocation. It is
- * likely that this allocation strategy differs from the one used in CTR-OS, but this hasn't been
- * verified and isn't likely to cause any problems.
- */
-class HandleTable final : NonCopyable {
-public:
-    /// This is the maximum limit of handles allowed per process in Horizon
-    static constexpr std::size_t MAX_COUNT = 1024;
-
-    explicit HandleTable(KernelCore& kernel);
-    ~HandleTable();
-
-    /**
-     * Sets the number of handles that may be in use at one time
-     * for this handle table.
-     *
-     * @param handle_table_size The desired size to limit the handle table to.
-     *
-     * @returns an error code indicating if initialization was successful.
-     *          If initialization was not successful, then ERR_OUT_OF_MEMORY
-     *          will be returned.
-     *
-     * @pre handle_table_size must be within the range [0, 1024]
-     */
-    ResultCode SetSize(s32 handle_table_size);
-
-    /**
-     * Returns a new handle that points to the same object as the passed in handle.
-     * @return The duplicated Handle or one of the following errors:
-     *           - `ERR_INVALID_HANDLE`: an invalid handle was passed in.
-     *           - Any errors returned by `Create()`.
-     */
-    ResultVal<Handle> Duplicate(Handle handle);
-
-    /**
-     * Closes a handle, removing it from the table and decreasing the object's ref-count.
-     * @return `RESULT_SUCCESS` or one of the following errors:
-     *           - `ERR_INVALID_HANDLE`: an invalid handle was passed in.
-     */
-    bool Remove(Handle handle);
-
-    /// Checks if a handle is valid and points to an existing object.
-    bool IsValid(Handle handle) const;
-
-    template <typename T = KAutoObject>
-    KAutoObject* GetObjectImpl(Handle handle) const {
-        if (!IsValid(handle)) {
-            return nullptr;
-        }
-
-        auto* obj = objects[static_cast<u16>(handle >> 15)];
-        return obj->DynamicCast<T*>();
-    }
-
-    template <typename T = KAutoObject>
-    KScopedAutoObject<T> GetObject(Handle handle) const {
-        if (handle == CurrentThread) {
-            return kernel.CurrentScheduler()->GetCurrentThread()->DynamicCast<T*>();
-        } else if (handle == CurrentProcess) {
-            return kernel.CurrentProcess()->DynamicCast<T*>();
-        }
-
-        if (!IsValid(handle)) {
-            return nullptr;
-        }
-
-        auto* obj = objects[static_cast<u16>(handle >> 15)];
-        return obj->DynamicCast<T*>();
-    }
-
-    template <typename T = KAutoObject>
-    KScopedAutoObject<T> GetObjectWithoutPseudoHandle(Handle handle) const {
-        if (!IsValid(handle)) {
-            return nullptr;
-        }
-        auto* obj = objects[static_cast<u16>(handle >> 15)];
-        return obj->DynamicCast<T*>();
-    }
-
-    /// Closes all handles held in this table.
-    void Clear();
-
-    // NEW IMPL
-
-    template <typename T>
-    ResultCode Add(Handle* out_handle, T* obj) {
-        static_assert(std::is_base_of<KAutoObject, T>::value);
-        return this->Add(out_handle, obj, obj->GetTypeObj().GetClassToken());
-    }
-
-    ResultCode Add(Handle* out_handle, KAutoObject* obj, u16 type);
-
-    template <typename T>
-    bool GetMultipleObjects(T** out, const Handle* handles, size_t num_handles) const {
-        // Try to convert and open all the handles.
-        size_t num_opened;
-        {
-            // Lock the table.
-            KScopedSpinLock lk(lock);
-            for (num_opened = 0; num_opened < num_handles; num_opened++) {
-                // Get the current handle.
-                const auto cur_handle = handles[num_opened];
-
-                // Get the object for the current handle.
-                KAutoObject* cur_object = this->GetObjectImpl(cur_handle);
-                if (cur_object == nullptr) {
-                    break;
-                }
-
-                // Cast the current object to the desired type.
-                T* cur_t = cur_object->DynamicCast<T*>();
-                if (cur_t == nullptr) {
-                    break;
-                }
-
-                // Open a reference to the current object.
-                cur_t->Open();
-                out[num_opened] = cur_t;
-            }
-        }
-
-        // If we converted every object, succeed.
-        if (num_opened == num_handles) {
-            return true;
-        }
-
-        // If we didn't convert entry object, close the ones we opened.
-        for (size_t i = 0; i < num_opened; i++) {
-            out[i]->Close();
-        }
-
-        return false;
-    }
-
-private:
-    /// Stores the Object referenced by the handle or null if the slot is empty.
-    std::array<KAutoObject*, MAX_COUNT> objects{};
-
-    /**
-     * The value of `next_generation` when the handle was created, used to check for validity. For
-     * empty slots, contains the index of the next free slot in the list.
-     */
-    std::array<u16, MAX_COUNT> generations;
-
-    /**
-     * The limited size of the handle table. This can be specified by process
-     * capabilities in order to restrict the overall number of handles that
-     * can be created in a process instance
-     */
-    u16 table_size = static_cast<u16>(MAX_COUNT);
-
-    /**
-     * Global counter of the number of created handles. Stored in `generations` when a handle is
-     * created, and wraps around to 1 when it hits 0x8000.
-     */
-    u16 next_generation = 1;
-
-    /// Head of the free slots linked list.
-    u16 next_free_slot = 0;
-
-    mutable KSpinLock lock;
-
-    /// Underlying kernel instance that this handle table operates under.
-    KernelCore& kernel;
-};
-
-} // namespace Kernel
diff --git a/src/core/hle/kernel/hle_ipc.cpp b/src/core/hle/kernel/hle_ipc.cpp
index 69190286d2..b505d20a6e 100644
--- a/src/core/hle/kernel/hle_ipc.cpp
+++ b/src/core/hle/kernel/hle_ipc.cpp
@@ -14,8 +14,8 @@
 #include "common/common_types.h"
 #include "common/logging/log.h"
 #include "core/hle/ipc_helpers.h"
-#include "core/hle/kernel/handle_table.h"
 #include "core/hle/kernel/hle_ipc.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_process.h"
 #include "core/hle/kernel/k_readable_event.h"
 #include "core/hle/kernel/k_scheduler.h"
@@ -50,7 +50,7 @@ HLERequestContext::HLERequestContext(KernelCore& kernel_, Core::Memory::Memory&
 
 HLERequestContext::~HLERequestContext() = default;
 
-void HLERequestContext::ParseCommandBuffer(const HandleTable& handle_table, u32_le* src_cmdbuf,
+void HLERequestContext::ParseCommandBuffer(const KHandleTable& handle_table, u32_le* src_cmdbuf,
                                            bool incoming) {
     IPC::RequestParser rp(src_cmdbuf);
     command_header = rp.PopRaw<IPC::CommandHeader>();
@@ -163,7 +163,7 @@ void HLERequestContext::ParseCommandBuffer(const HandleTable& handle_table, u32_
     rp.Skip(1, false); // The command is actually an u64, but we don't use the high part.
 }
 
-ResultCode HLERequestContext::PopulateFromIncomingCommandBuffer(const HandleTable& handle_table,
+ResultCode HLERequestContext::PopulateFromIncomingCommandBuffer(const KHandleTable& handle_table,
                                                                 u32_le* src_cmdbuf) {
     ParseCommandBuffer(handle_table, src_cmdbuf, true);
     if (command_header->type == IPC::CommandType::Close) {
diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h
index 4b92ba6558..fa031c1212 100644
--- a/src/core/hle/kernel/hle_ipc.h
+++ b/src/core/hle/kernel/hle_ipc.h
@@ -17,6 +17,7 @@
 #include "common/swap.h"
 #include "core/hle/ipc.h"
 #include "core/hle/kernel/k_auto_object.h"
+#include "core/hle/kernel/svc_common.h"
 
 union ResultCode;
 
@@ -35,9 +36,9 @@ class ServiceFrameworkBase;
 namespace Kernel {
 
 class Domain;
-class HandleTable;
 class HLERequestContext;
 class KernelCore;
+class KHandleTable;
 class KProcess;
 class KServerSession;
 class KThread;
@@ -121,7 +122,7 @@ public:
     }
 
     /// Populates this context with data from the requesting process/thread.
-    ResultCode PopulateFromIncomingCommandBuffer(const HandleTable& handle_table,
+    ResultCode PopulateFromIncomingCommandBuffer(const KHandleTable& handle_table,
                                                  u32_le* src_cmdbuf);
 
     /// Writes data from this context back to the requesting process/thread.
@@ -267,7 +268,7 @@ public:
 private:
     friend class IPC::ResponseBuilder;
 
-    void ParseCommandBuffer(const HandleTable& handle_table, u32_le* src_cmdbuf, bool incoming);
+    void ParseCommandBuffer(const KHandleTable& handle_table, u32_le* src_cmdbuf, bool incoming);
 
     std::array<u32, IPC::COMMAND_BUFFER_LENGTH> cmd_buf;
     Kernel::KServerSession* server_session{};
diff --git a/src/core/hle/kernel/k_auto_object.h b/src/core/hle/kernel/k_auto_object.h
index 5a180b7dcb..32aaf9fc50 100644
--- a/src/core/hle/kernel/k_auto_object.h
+++ b/src/core/hle/kernel/k_auto_object.h
@@ -17,8 +17,6 @@ namespace Kernel {
 class KernelCore;
 class KProcess;
 
-using Handle = u32;
-
 #define KERNEL_AUTOOBJECT_TRAITS(CLASS, BASE_CLASS)                                                \
     NON_COPYABLE(CLASS);                                                                           \
     NON_MOVEABLE(CLASS);                                                                           \
diff --git a/src/core/hle/kernel/k_condition_variable.cpp b/src/core/hle/kernel/k_condition_variable.cpp
index a9738f7ced..f51cf3e7bf 100644
--- a/src/core/hle/kernel/k_condition_variable.cpp
+++ b/src/core/hle/kernel/k_condition_variable.cpp
@@ -179,7 +179,7 @@ KThread* KConditionVariable::SignalImpl(KThread* thread) {
 
     KThread* thread_to_close = nullptr;
     if (can_access) {
-        if (prev_tag == InvalidHandle) {
+        if (prev_tag == Svc::InvalidHandle) {
             // If nobody held the lock previously, we're all good.
             thread->SetSyncedObject(nullptr, RESULT_SUCCESS);
             thread->Wakeup();
diff --git a/src/core/hle/kernel/k_handle_table.cpp b/src/core/hle/kernel/k_handle_table.cpp
new file mode 100644
index 0000000000..0378447f68
--- /dev/null
+++ b/src/core/hle/kernel/k_handle_table.cpp
@@ -0,0 +1,135 @@
+// Copyright 2021 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "core/hle/kernel/k_handle_table.h"
+
+namespace Kernel {
+
+KHandleTable::KHandleTable(KernelCore& kernel_) : kernel{kernel_} {}
+KHandleTable ::~KHandleTable() = default;
+
+ResultCode KHandleTable::Finalize() {
+    // Get the table and clear our record of it.
+    u16 saved_table_size = 0;
+    {
+        KScopedSpinLock lk(m_lock);
+
+        std::swap(m_table_size, saved_table_size);
+    }
+
+    // Close and free all entries.
+    for (size_t i = 0; i < saved_table_size; i++) {
+        if (KAutoObject* obj = m_objects[i]; obj != nullptr) {
+            obj->Close();
+        }
+    }
+
+    return RESULT_SUCCESS;
+}
+
+bool KHandleTable::Remove(Handle handle) {
+    // Don't allow removal of a pseudo-handle.
+    if (Svc::IsPseudoHandle(handle)) {
+        return false;
+    }
+
+    // Handles must not have reserved bits set.
+    const auto handle_pack = HandlePack(handle);
+    if (handle_pack.reserved != 0) {
+        return false;
+    }
+
+    // Find the object and free the entry.
+    KAutoObject* obj = nullptr;
+    {
+        KScopedSpinLock lk(m_lock);
+
+        if (this->IsValidHandle(handle)) {
+            const auto index = handle_pack.index;
+
+            obj = m_objects[index];
+            this->FreeEntry(index);
+        } else {
+            return false;
+        }
+    }
+
+    // Close the object.
+    obj->Close();
+    return true;
+}
+
+ResultCode KHandleTable::Add(Handle* out_handle, KAutoObject* obj, u16 type) {
+    KScopedSpinLock lk(m_lock);
+
+    // Never exceed our capacity.
+    R_UNLESS(m_count < m_table_size, ResultOutOfHandles);
+
+    // Allocate entry, set output handle.
+    {
+        const auto linear_id = this->AllocateLinearId();
+        const auto index = this->AllocateEntry();
+
+        m_entry_infos[index].info = {.linear_id = linear_id, .type = type};
+        m_objects[index] = obj;
+
+        obj->Open();
+
+        *out_handle = EncodeHandle(static_cast<u16>(index), linear_id);
+    }
+
+    return RESULT_SUCCESS;
+}
+
+ResultCode KHandleTable::Reserve(Handle* out_handle) {
+    KScopedSpinLock lk(m_lock);
+
+    // Never exceed our capacity.
+    R_UNLESS(m_count < m_table_size, ResultOutOfHandles);
+
+    *out_handle = EncodeHandle(static_cast<u16>(this->AllocateEntry()), this->AllocateLinearId());
+    return RESULT_SUCCESS;
+}
+
+void KHandleTable::Unreserve(Handle handle) {
+    KScopedSpinLock lk(m_lock);
+
+    // Unpack the handle.
+    const auto handle_pack = HandlePack(handle);
+    const auto index = handle_pack.index;
+    const auto linear_id = handle_pack.linear_id;
+    const auto reserved = handle_pack.reserved;
+    ASSERT(reserved == 0);
+    ASSERT(linear_id != 0);
+
+    if (index < m_table_size) {
+        // NOTE: This code does not check the linear id.
+        ASSERT(m_objects[index] == nullptr);
+        this->FreeEntry(index);
+    }
+}
+
+void KHandleTable::Register(Handle handle, KAutoObject* obj, u16 type) {
+    KScopedSpinLock lk(m_lock);
+
+    // Unpack the handle.
+    const auto handle_pack = HandlePack(handle);
+    const auto index = handle_pack.index;
+    const auto linear_id = handle_pack.linear_id;
+    const auto reserved = handle_pack.reserved;
+    ASSERT(reserved == 0);
+    ASSERT(linear_id != 0);
+
+    if (index < m_table_size) {
+        // Set the entry.
+        ASSERT(m_objects[index] == nullptr);
+
+        m_entry_infos[index].info = {.linear_id = static_cast<u16>(linear_id), .type = type};
+        m_objects[index] = obj;
+
+        obj->Open();
+    }
+}
+
+} // namespace Kernel
diff --git a/src/core/hle/kernel/k_handle_table.h b/src/core/hle/kernel/k_handle_table.h
new file mode 100644
index 0000000000..e38ad0fd94
--- /dev/null
+++ b/src/core/hle/kernel/k_handle_table.h
@@ -0,0 +1,309 @@
+// Copyright 2021 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+
+#include "common/assert.h"
+#include "common/bit_field.h"
+#include "common/bit_util.h"
+#include "common/common_types.h"
+#include "core/hle/kernel/k_auto_object.h"
+#include "core/hle/kernel/k_spin_lock.h"
+#include "core/hle/kernel/k_thread.h"
+#include "core/hle/kernel/kernel.h"
+#include "core/hle/kernel/svc_common.h"
+#include "core/hle/kernel/svc_results.h"
+#include "core/hle/result.h"
+
+namespace Kernel {
+
+class KernelCore;
+
+class KHandleTable {
+    NON_COPYABLE(KHandleTable);
+    NON_MOVEABLE(KHandleTable);
+
+public:
+    static constexpr size_t MaxTableSize = 1024;
+
+private:
+    union HandlePack {
+        u32 raw;
+        BitField<0, 15, u32> index;
+        BitField<15, 15, u32> linear_id;
+        BitField<30, 2, u32> reserved;
+    };
+
+    static constexpr u16 MinLinearId = 1;
+    static constexpr u16 MaxLinearId = 0x7FFF;
+
+    static constexpr Handle EncodeHandle(u16 index, u16 linear_id) {
+        HandlePack handle{};
+        handle.index.Assign(index);
+        handle.linear_id.Assign(linear_id);
+        handle.reserved.Assign(0);
+        return handle.raw;
+    }
+
+    union EntryInfo {
+        struct {
+            u16 linear_id;
+            u16 type;
+        } info;
+        s32 next_free_index;
+
+        constexpr u16 GetLinearId() const {
+            return info.linear_id;
+        }
+        constexpr u16 GetType() const {
+            return info.type;
+        }
+        constexpr s32 GetNextFreeIndex() const {
+            return next_free_index;
+        }
+    };
+
+private:
+    std::array<EntryInfo, MaxTableSize> m_entry_infos{};
+    std::array<KAutoObject*, MaxTableSize> m_objects{};
+    s32 m_free_head_index{-1};
+    u16 m_table_size{};
+    u16 m_max_count{};
+    u16 m_next_linear_id{MinLinearId};
+    u16 m_count{};
+    mutable KSpinLock m_lock;
+
+public:
+    explicit KHandleTable(KernelCore& kernel_);
+    ~KHandleTable();
+
+    constexpr ResultCode Initialize(s32 size) {
+        R_UNLESS(size <= static_cast<s32>(MaxTableSize), ResultOutOfMemory);
+
+        // Initialize all fields.
+        m_max_count = 0;
+        m_table_size = static_cast<u16>((size <= 0) ? MaxTableSize : size);
+        m_next_linear_id = MinLinearId;
+        m_count = 0;
+        m_free_head_index = -1;
+
+        // Free all entries.
+        for (s32 i = 0; i < static_cast<s32>(m_table_size); ++i) {
+            m_objects[i] = nullptr;
+            m_entry_infos[i].next_free_index = i - 1;
+            m_free_head_index = i;
+        }
+
+        return RESULT_SUCCESS;
+    }
+
+    constexpr size_t GetTableSize() const {
+        return m_table_size;
+    }
+    constexpr size_t GetCount() const {
+        return m_count;
+    }
+    constexpr size_t GetMaxCount() const {
+        return m_max_count;
+    }
+
+    ResultCode Finalize();
+    bool Remove(Handle handle);
+
+    template <typename T = KAutoObject>
+    KScopedAutoObject<T> GetObjectWithoutPseudoHandle(Handle handle) const {
+        // Lock and look up in table.
+        KScopedSpinLock lk(m_lock);
+
+        if constexpr (std::is_same<T, KAutoObject>::value) {
+            return this->GetObjectImpl(handle);
+        } else {
+            if (auto* obj = this->GetObjectImpl(handle); obj != nullptr) {
+                return obj->DynamicCast<T*>();
+            } else {
+                return nullptr;
+            }
+        }
+    }
+
+    template <typename T = KAutoObject>
+    KScopedAutoObject<T> GetObject(Handle handle) const {
+        // Handle pseudo-handles.
+        if constexpr (std::derived_from<KProcess, T>) {
+            if (handle == Svc::PseudoHandle::CurrentProcess) {
+                auto* const cur_process = kernel.CurrentProcess();
+                ASSERT(cur_process != nullptr);
+                return cur_process;
+            }
+        } else if constexpr (std::derived_from<KThread, T>) {
+            if (handle == Svc::PseudoHandle::CurrentThread) {
+                auto* const cur_thread = GetCurrentThreadPointer(kernel);
+                ASSERT(cur_thread != nullptr);
+                return cur_thread;
+            }
+        }
+
+        return this->template GetObjectWithoutPseudoHandle<T>(handle);
+    }
+
+    ResultCode Reserve(Handle* out_handle);
+    void Unreserve(Handle handle);
+
+    template <typename T>
+    ResultCode Add(Handle* out_handle, T* obj) {
+        static_assert(std::is_base_of<KAutoObject, T>::value);
+        return this->Add(out_handle, obj, obj->GetTypeObj().GetClassToken());
+    }
+
+    template <typename T>
+    void Register(Handle handle, T* obj) {
+        static_assert(std::is_base_of<KAutoObject, T>::value);
+        return this->Register(handle, obj, obj->GetTypeObj().GetClassToken());
+    }
+
+    template <typename T>
+    bool GetMultipleObjects(T** out, const Handle* handles, size_t num_handles) const {
+        // Try to convert and open all the handles.
+        size_t num_opened;
+        {
+            // Lock the table.
+            KScopedSpinLock lk(m_lock);
+            for (num_opened = 0; num_opened < num_handles; num_opened++) {
+                // Get the current handle.
+                const auto cur_handle = handles[num_opened];
+
+                // Get the object for the current handle.
+                KAutoObject* cur_object = this->GetObjectImpl(cur_handle);
+                if (cur_object == nullptr) {
+                    break;
+                }
+
+                // Cast the current object to the desired type.
+                T* cur_t = cur_object->DynamicCast<T*>();
+                if (cur_t == nullptr) {
+                    break;
+                }
+
+                // Open a reference to the current object.
+                cur_t->Open();
+                out[num_opened] = cur_t;
+            }
+        }
+
+        // If we converted every object, succeed.
+        if (num_opened == num_handles) {
+            return true;
+        }
+
+        // If we didn't convert entry object, close the ones we opened.
+        for (size_t i = 0; i < num_opened; i++) {
+            out[i]->Close();
+        }
+
+        return false;
+    }
+
+private:
+    ResultCode Add(Handle* out_handle, KAutoObject* obj, u16 type);
+    void Register(Handle handle, KAutoObject* obj, u16 type);
+
+    constexpr s32 AllocateEntry() {
+        ASSERT(m_count < m_table_size);
+
+        const auto index = m_free_head_index;
+
+        m_free_head_index = m_entry_infos[index].GetNextFreeIndex();
+
+        m_max_count = std::max(m_max_count, ++m_count);
+
+        return index;
+    }
+
+    constexpr void FreeEntry(s32 index) {
+        ASSERT(m_count > 0);
+
+        m_objects[index] = nullptr;
+        m_entry_infos[index].next_free_index = m_free_head_index;
+
+        m_free_head_index = index;
+
+        --m_count;
+    }
+
+    constexpr u16 AllocateLinearId() {
+        const u16 id = m_next_linear_id++;
+        if (m_next_linear_id > MaxLinearId) {
+            m_next_linear_id = MinLinearId;
+        }
+        return id;
+    }
+
+    constexpr bool IsValidHandle(Handle handle) const {
+        // Unpack the handle.
+        const auto handle_pack = HandlePack(handle);
+        const auto raw_value = handle_pack.raw;
+        const auto index = handle_pack.index;
+        const auto linear_id = handle_pack.linear_id;
+        const auto reserved = handle_pack.reserved;
+        ASSERT(reserved == 0);
+
+        // Validate our indexing information.
+        if (raw_value == 0) {
+            return false;
+        }
+        if (linear_id == 0) {
+            return false;
+        }
+        if (index >= m_table_size) {
+            return false;
+        }
+
+        // Check that there's an object, and our serial id is correct.
+        if (m_objects[index] == nullptr) {
+            return false;
+        }
+        if (m_entry_infos[index].GetLinearId() != linear_id) {
+            return false;
+        }
+
+        return true;
+    }
+
+    constexpr KAutoObject* GetObjectImpl(Handle handle) const {
+        // Handles must not have reserved bits set.
+        const auto handle_pack = HandlePack(handle);
+        if (handle_pack.reserved != 0) {
+            return nullptr;
+        }
+
+        if (this->IsValidHandle(handle)) {
+            return m_objects[handle_pack.index];
+        } else {
+            return nullptr;
+        }
+    }
+
+    constexpr KAutoObject* GetObjectByIndexImpl(Handle* out_handle, size_t index) const {
+
+        // Index must be in bounds.
+        if (index >= m_table_size) {
+            return nullptr;
+        }
+
+        // Ensure entry has an object.
+        if (KAutoObject* obj = m_objects[index]; obj != nullptr) {
+            *out_handle = EncodeHandle(static_cast<u16>(index), m_entry_infos[index].GetLinearId());
+            return obj;
+        } else {
+            return nullptr;
+        }
+    }
+
+private:
+    KernelCore& kernel;
+};
+
+} // namespace Kernel
diff --git a/src/core/hle/kernel/k_process.cpp b/src/core/hle/kernel/k_process.cpp
index e542b1f078..1743181809 100644
--- a/src/core/hle/kernel/k_process.cpp
+++ b/src/core/hle/kernel/k_process.cpp
@@ -354,7 +354,7 @@ ResultCode KProcess::LoadFromMetadata(const FileSys::ProgramMetadata& metadata,
     tls_region_address = CreateTLSRegion();
     memory_reservation.Commit();
 
-    return handle_table.SetSize(capabilities.GetHandleTableSize());
+    return handle_table.Initialize(capabilities.GetHandleTableSize());
 }
 
 void KProcess::Run(s32 main_thread_priority, u64 stack_size) {
diff --git a/src/core/hle/kernel/k_process.h b/src/core/hle/kernel/k_process.h
index 5c54c63609..62ab26b052 100644
--- a/src/core/hle/kernel/k_process.h
+++ b/src/core/hle/kernel/k_process.h
@@ -11,10 +11,10 @@
 #include <unordered_map>
 #include <vector>
 #include "common/common_types.h"
-#include "core/hle/kernel/handle_table.h"
 #include "core/hle/kernel/k_address_arbiter.h"
 #include "core/hle/kernel/k_auto_object.h"
 #include "core/hle/kernel/k_condition_variable.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_synchronization_object.h"
 #include "core/hle/kernel/process_capability.h"
 #include "core/hle/kernel/slab_helpers.h"
@@ -104,12 +104,12 @@ public:
     }
 
     /// Gets a reference to the process' handle table.
-    HandleTable& GetHandleTable() {
+    KHandleTable& GetHandleTable() {
         return handle_table;
     }
 
     /// Gets a const reference to the process' handle table.
-    const HandleTable& GetHandleTable() const {
+    const KHandleTable& GetHandleTable() const {
         return handle_table;
     }
 
@@ -429,7 +429,7 @@ private:
     u64 total_process_running_time_ticks = 0;
 
     /// Per-process handle table for storing created object handles in.
-    HandleTable handle_table;
+    KHandleTable handle_table;
 
     /// Per-process address arbiter.
     KAddressArbiter address_arbiter;
diff --git a/src/core/hle/kernel/k_scoped_scheduler_lock_and_sleep.h b/src/core/hle/kernel/k_scoped_scheduler_lock_and_sleep.h
index ebecf0c778..1bfbbcfe26 100644
--- a/src/core/hle/kernel/k_scoped_scheduler_lock_and_sleep.h
+++ b/src/core/hle/kernel/k_scoped_scheduler_lock_and_sleep.h
@@ -8,7 +8,7 @@
 #pragma once
 
 #include "common/common_types.h"
-#include "core/hle/kernel/handle_table.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_thread.h"
 #include "core/hle/kernel/kernel.h"
 #include "core/hle/kernel/time_manager.h"
@@ -17,7 +17,7 @@ namespace Kernel {
 
 class [[nodiscard]] KScopedSchedulerLockAndSleep {
 public:
-    explicit KScopedSchedulerLockAndSleep(KernelCore & kernel, KThread * t, s64 timeout)
+    explicit KScopedSchedulerLockAndSleep(KernelCore& kernel, KThread* t, s64 timeout)
         : kernel(kernel), thread(t), timeout_tick(timeout) {
         // Lock the scheduler.
         kernel.GlobalSchedulerContext().scheduler_lock.Lock();
diff --git a/src/core/hle/kernel/k_server_session.cpp b/src/core/hle/kernel/k_server_session.cpp
index 3bc259693e..c8acaa4531 100644
--- a/src/core/hle/kernel/k_server_session.cpp
+++ b/src/core/hle/kernel/k_server_session.cpp
@@ -10,9 +10,9 @@
 #include "common/logging/log.h"
 #include "core/core_timing.h"
 #include "core/hle/ipc_helpers.h"
-#include "core/hle/kernel/handle_table.h"
 #include "core/hle/kernel/hle_ipc.h"
 #include "core/hle/kernel/k_client_port.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_process.h"
 #include "core/hle/kernel/k_scheduler.h"
 #include "core/hle/kernel/k_server_session.h"
diff --git a/src/core/hle/kernel/k_thread.cpp b/src/core/hle/kernel/k_thread.cpp
index 3de0157ace..ef6dfeeca1 100644
--- a/src/core/hle/kernel/k_thread.cpp
+++ b/src/core/hle/kernel/k_thread.cpp
@@ -18,8 +18,8 @@
 #include "core/core.h"
 #include "core/cpu_manager.h"
 #include "core/hardware_properties.h"
-#include "core/hle/kernel/handle_table.h"
 #include "core/hle/kernel/k_condition_variable.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_memory_layout.h"
 #include "core/hle/kernel/k_process.h"
 #include "core/hle/kernel/k_resource_limit.h"
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index f64e070818..825fab694e 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -26,9 +26,9 @@
 #include "core/cpu_manager.h"
 #include "core/device_memory.h"
 #include "core/hardware_properties.h"
-#include "core/hle/kernel/handle_table.h"
 #include "core/hle/kernel/init/init_slab_setup.h"
 #include "core/hle/kernel/k_client_port.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_memory_layout.h"
 #include "core/hle/kernel/k_memory_manager.h"
 #include "core/hle/kernel/k_process.h"
@@ -52,8 +52,7 @@ namespace Kernel {
 
 struct KernelCore::Impl {
     explicit Impl(Core::System& system, KernelCore& kernel)
-        : time_manager{system}, global_handle_table{kernel},
-          object_list_container{kernel}, system{system} {}
+        : time_manager{system}, object_list_container{kernel}, system{system} {}
 
     void SetMulticore(bool is_multicore) {
         this->is_multicore = is_multicore;
@@ -61,6 +60,7 @@ struct KernelCore::Impl {
 
     void Initialize(KernelCore& kernel) {
         global_scheduler_context = std::make_unique<Kernel::GlobalSchedulerContext>(kernel);
+        global_handle_table = std::make_unique<Kernel::KHandleTable>(kernel);
 
         service_thread_manager =
             std::make_unique<Common::ThreadWorker>(1, "yuzu:ServiceThreadManager");
@@ -118,7 +118,7 @@ struct KernelCore::Impl {
             current_process = nullptr;
         }
 
-        global_handle_table.Clear();
+        global_handle_table.reset();
 
         preemption_event = nullptr;
 
@@ -648,7 +648,7 @@ struct KernelCore::Impl {
 
     // This is the kernel's handle table or supervisor handle table which
     // stores all the objects in place.
-    HandleTable global_handle_table;
+    std::unique_ptr<KHandleTable> global_handle_table;
 
     KAutoObjectWithListContainer object_list_container;
 
@@ -722,7 +722,7 @@ KResourceLimit* KernelCore::GetSystemResourceLimit() {
 }
 
 KScopedAutoObject<KThread> KernelCore::RetrieveThreadFromGlobalHandleTable(Handle handle) const {
-    return impl->global_handle_table.GetObject<KThread>(handle);
+    return impl->global_handle_table->GetObject<KThread>(handle);
 }
 
 void KernelCore::AppendNewProcess(KProcess* process) {
@@ -876,12 +876,12 @@ u64 KernelCore::CreateNewUserProcessID() {
     return impl->next_user_process_id++;
 }
 
-Kernel::HandleTable& KernelCore::GlobalHandleTable() {
-    return impl->global_handle_table;
+KHandleTable& KernelCore::GlobalHandleTable() {
+    return *impl->global_handle_table;
 }
 
-const Kernel::HandleTable& KernelCore::GlobalHandleTable() const {
-    return impl->global_handle_table;
+const KHandleTable& KernelCore::GlobalHandleTable() const {
+    return *impl->global_handle_table;
 }
 
 void KernelCore::RegisterCoreThread(std::size_t core_id) {
diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h
index 0dd9deaeb7..7c46aa9975 100644
--- a/src/core/hle/kernel/kernel.h
+++ b/src/core/hle/kernel/kernel.h
@@ -14,6 +14,7 @@
 #include "core/hle/kernel/k_auto_object.h"
 #include "core/hle/kernel/k_slab_heap.h"
 #include "core/hle/kernel/memory_types.h"
+#include "core/hle/kernel/svc_common.h"
 
 namespace Core {
 class CPUInterruptHandler;
@@ -30,10 +31,10 @@ namespace Kernel {
 
 class KClientPort;
 class GlobalSchedulerContext;
-class HandleTable;
 class KAutoObjectWithListContainer;
 class KClientSession;
 class KEvent;
+class KHandleTable;
 class KLinkedListNode;
 class KMemoryManager;
 class KPort;
@@ -308,10 +309,10 @@ private:
     u64 CreateNewThreadID();
 
     /// Provides a reference to the global handle table.
-    Kernel::HandleTable& GlobalHandleTable();
+    KHandleTable& GlobalHandleTable();
 
     /// Provides a const reference to the global handle table.
-    const Kernel::HandleTable& GlobalHandleTable() const;
+    const KHandleTable& GlobalHandleTable() const;
 
     struct Impl;
     std::unique_ptr<Impl> impl;
diff --git a/src/core/hle/kernel/process_capability.cpp b/src/core/hle/kernel/process_capability.cpp
index 4ccac0b064..fcb8b1ea5c 100644
--- a/src/core/hle/kernel/process_capability.cpp
+++ b/src/core/hle/kernel/process_capability.cpp
@@ -6,7 +6,7 @@
 
 #include "common/bit_util.h"
 #include "common/logging/log.h"
-#include "core/hle/kernel/handle_table.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_page_table.h"
 #include "core/hle/kernel/process_capability.h"
 #include "core/hle/kernel/svc_results.h"
@@ -99,7 +99,7 @@ void ProcessCapabilities::InitializeForMetadatalessProcess() {
     interrupt_capabilities.set();
 
     // Allow using the maximum possible amount of handles
-    handle_table_size = static_cast<s32>(HandleTable::MAX_COUNT);
+    handle_table_size = static_cast<s32>(KHandleTable::MaxTableSize);
 
     // Allow all debugging capabilities.
     is_debuggable = true;
diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp
index 156c565b0e..d3293a1fef 100644
--- a/src/core/hle/kernel/svc.cpp
+++ b/src/core/hle/kernel/svc.cpp
@@ -21,12 +21,12 @@
 #include "core/core_timing.h"
 #include "core/core_timing_util.h"
 #include "core/cpu_manager.h"
-#include "core/hle/kernel/handle_table.h"
 #include "core/hle/kernel/k_address_arbiter.h"
 #include "core/hle/kernel/k_client_port.h"
 #include "core/hle/kernel/k_client_session.h"
 #include "core/hle/kernel/k_condition_variable.h"
 #include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_memory_block.h"
 #include "core/hle/kernel/k_memory_layout.h"
 #include "core/hle/kernel/k_page_table.h"
@@ -839,10 +839,10 @@ static ResultCode GetInfo(Core::System& system, u64* result, u64 info_id, Handle
         }
 
         KProcess* const current_process = system.Kernel().CurrentProcess();
-        HandleTable& handle_table = current_process->GetHandleTable();
+        KHandleTable& handle_table = current_process->GetHandleTable();
         const auto resource_limit = current_process->GetResourceLimit();
         if (!resource_limit) {
-            *result = KernelHandle::InvalidHandle;
+            *result = Svc::InvalidHandle;
             // Yes, the kernel considers this a successful operation.
             return RESULT_SUCCESS;
         }
@@ -1993,7 +1993,7 @@ static ResultCode SignalEvent(Core::System& system, Handle event_handle) {
     LOG_DEBUG(Kernel_SVC, "called, event_handle=0x{:08X}", event_handle);
 
     // Get the current handle table.
-    const HandleTable& handle_table = system.Kernel().CurrentProcess()->GetHandleTable();
+    const KHandleTable& handle_table = system.Kernel().CurrentProcess()->GetHandleTable();
 
     // Get the writable event.
     KScopedAutoObject writable_event = handle_table.GetObject<KWritableEvent>(event_handle);
diff --git a/src/core/hle/kernel/svc_common.h b/src/core/hle/kernel/svc_common.h
index 4af0495510..8a8f347b58 100644
--- a/src/core/hle/kernel/svc_common.h
+++ b/src/core/hle/kernel/svc_common.h
@@ -6,9 +6,24 @@
 
 #include "common/common_types.h"
 
+namespace Kernel {
+using Handle = u32;
+}
+
 namespace Kernel::Svc {
 
 constexpr s32 ArgumentHandleCountMax = 0x40;
 constexpr u32 HandleWaitMask{1u << 30};
 
+constexpr inline Handle InvalidHandle = Handle(0);
+
+enum PseudoHandle : Handle {
+    CurrentThread = 0xFFFF8000,
+    CurrentProcess = 0xFFFF8001,
+};
+
+constexpr bool IsPseudoHandle(const Handle& handle) {
+    return handle == PseudoHandle::CurrentProcess || handle == PseudoHandle::CurrentThread;
+}
+
 } // namespace Kernel::Svc
diff --git a/src/core/hle/kernel/time_manager.cpp b/src/core/hle/kernel/time_manager.cpp
index 59ebfc51f0..ae9b4be2fd 100644
--- a/src/core/hle/kernel/time_manager.cpp
+++ b/src/core/hle/kernel/time_manager.cpp
@@ -6,7 +6,6 @@
 #include "core/core.h"
 #include "core/core_timing.h"
 #include "core/core_timing_util.h"
-#include "core/hle/kernel/handle_table.h"
 #include "core/hle/kernel/k_scheduler.h"
 #include "core/hle/kernel/k_thread.h"
 #include "core/hle/kernel/kernel.h"
diff --git a/src/yuzu/debugger/wait_tree.cpp b/src/yuzu/debugger/wait_tree.cpp
index f040b4c08f..bdfda6c54f 100644
--- a/src/yuzu/debugger/wait_tree.cpp
+++ b/src/yuzu/debugger/wait_tree.cpp
@@ -12,8 +12,8 @@
 #include "common/assert.h"
 #include "core/arm/arm_interface.h"
 #include "core/core.h"
-#include "core/hle/kernel/handle_table.h"
 #include "core/hle/kernel/k_class_token.h"
+#include "core/hle/kernel/k_handle_table.h"
 #include "core/hle/kernel/k_process.h"
 #include "core/hle/kernel/k_readable_event.h"
 #include "core/hle/kernel/k_scheduler.h"
@@ -115,7 +115,7 @@ QString WaitTreeText::GetText() const {
     return text;
 }
 
-WaitTreeMutexInfo::WaitTreeMutexInfo(VAddr mutex_address, const Kernel::HandleTable& handle_table)
+WaitTreeMutexInfo::WaitTreeMutexInfo(VAddr mutex_address, const Kernel::KHandleTable& handle_table)
     : mutex_address(mutex_address) {
     mutex_value = Core::System::GetInstance().Memory().Read32(mutex_address);
     owner_handle = static_cast<Kernel::Handle>(mutex_value & Kernel::Svc::HandleWaitMask);
diff --git a/src/yuzu/debugger/wait_tree.h b/src/yuzu/debugger/wait_tree.h
index 3dd4acab01..d450345dfb 100644
--- a/src/yuzu/debugger/wait_tree.h
+++ b/src/yuzu/debugger/wait_tree.h
@@ -14,11 +14,12 @@
 
 #include "common/common_types.h"
 #include "core/hle/kernel/k_auto_object.h"
+#include "core/hle/kernel/svc_common.h"
 
 class EmuThread;
 
 namespace Kernel {
-class HandleTable;
+class KHandleTable;
 class KReadableEvent;
 class KSynchronizationObject;
 class KThread;
@@ -74,7 +75,7 @@ public:
 class WaitTreeMutexInfo : public WaitTreeExpandableItem {
     Q_OBJECT
 public:
-    explicit WaitTreeMutexInfo(VAddr mutex_address, const Kernel::HandleTable& handle_table);
+    explicit WaitTreeMutexInfo(VAddr mutex_address, const Kernel::KHandleTable& handle_table);
     ~WaitTreeMutexInfo() override;
 
     QString GetText() const override;