From d735f5c4580b2ce1b2cc3d6bbcd86901b02a83ef Mon Sep 17 00:00:00 2001
From: GPUCode <47210458+GPUCode@users.noreply.github.com>
Date: Tue, 20 Jun 2023 15:24:24 +0300
Subject: [PATCH] renderer_vulkan: Add vulkan initialization code (#6620)

* common: Move dynamic library to common

* This is so that video_core can use it

* logging: Add vulkan log target

* common: Allow defered library loading

* Also add some comments to the functions

* renderer_vulkan: Add vulkan initialization code

* renderer_vulkan: Address feedback
---
 src/citra_qt/main.cpp                         |  11 +-
 .../dynamic_library/dynamic_library.cpp       |  51 +-
 src/common/dynamic_library/dynamic_library.h  |  27 +-
 src/common/dynamic_library/fdk-aac.cpp        |   5 +-
 src/common/dynamic_library/fdk-aac.h          |   3 -
 src/common/dynamic_library/ffmpeg.cpp         |  22 +-
 src/common/dynamic_library/ffmpeg.h           |   3 -
 src/common/logging/backend.cpp                |   1 +
 src/common/logging/log.h                      |   2 +
 src/common/settings.h                         |   1 +
 src/video_core/CMakeLists.txt                 |  10 +-
 src/video_core/renderer_vulkan/pica_to_vk.h   | 198 ++++++
 src/video_core/renderer_vulkan/vk_common.cpp  |  12 +
 src/video_core/renderer_vulkan/vk_common.h    |  16 +
 .../renderer_vulkan/vk_instance.cpp           | 581 ++++++++++++++++++
 src/video_core/renderer_vulkan/vk_instance.h  | 287 +++++++++
 .../renderer_vulkan/vk_platform.cpp           | 366 +++++++++++
 src/video_core/renderer_vulkan/vk_platform.h  |  34 +
 18 files changed, 1576 insertions(+), 54 deletions(-)
 create mode 100644 src/video_core/renderer_vulkan/pica_to_vk.h
 create mode 100644 src/video_core/renderer_vulkan/vk_common.cpp
 create mode 100644 src/video_core/renderer_vulkan/vk_common.h
 create mode 100644 src/video_core/renderer_vulkan/vk_instance.cpp
 create mode 100644 src/video_core/renderer_vulkan/vk_instance.h
 create mode 100644 src/video_core/renderer_vulkan/vk_platform.cpp
 create mode 100644 src/video_core/renderer_vulkan/vk_platform.h

diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 7c857761f..ea019d825 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -64,6 +64,7 @@
 #include "common/arch.h"
 #include "common/common_paths.h"
 #include "common/detached_tasks.h"
+#include "common/dynamic_library/dynamic_library.h"
 #include "common/file_util.h"
 #include "common/literals.h"
 #include "common/logging/backend.h"
@@ -2242,11 +2243,11 @@ void GMainWindow::OnOpenFFmpeg() {
     }
 
     static const std::array library_names = {
-        DynamicLibrary::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR),
-        DynamicLibrary::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR),
-        DynamicLibrary::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR),
-        DynamicLibrary::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR),
-        DynamicLibrary::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR),
+        Common::DynamicLibrary::GetLibraryName("avcodec", LIBAVCODEC_VERSION_MAJOR),
+        Common::DynamicLibrary::GetLibraryName("avfilter", LIBAVFILTER_VERSION_MAJOR),
+        Common::DynamicLibrary::GetLibraryName("avformat", LIBAVFORMAT_VERSION_MAJOR),
+        Common::DynamicLibrary::GetLibraryName("avutil", LIBAVUTIL_VERSION_MAJOR),
+        Common::DynamicLibrary::GetLibraryName("swresample", LIBSWRESAMPLE_VERSION_MAJOR),
     };
 
     for (auto& library_name : library_names) {
diff --git a/src/common/dynamic_library/dynamic_library.cpp b/src/common/dynamic_library/dynamic_library.cpp
index b90cb1424..f5b348537 100644
--- a/src/common/dynamic_library/dynamic_library.cpp
+++ b/src/common/dynamic_library/dynamic_library.cpp
@@ -10,29 +10,11 @@
 #endif
 #include "dynamic_library.h"
 
-namespace DynamicLibrary {
+namespace Common {
 
 DynamicLibrary::DynamicLibrary(std::string_view name, int major, int minor) {
     auto full_name = GetLibraryName(name, major, minor);
-#if defined(_WIN32)
-    handle = reinterpret_cast<void*>(LoadLibraryA(full_name.c_str()));
-    if (!handle) {
-        DWORD error_message_id = GetLastError();
-        LPSTR message_buffer = nullptr;
-        size_t size =
-            FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
-                               FORMAT_MESSAGE_IGNORE_INSERTS,
-                           nullptr, error_message_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
-                           reinterpret_cast<LPSTR>(&message_buffer), 0, nullptr);
-        std::string message(message_buffer, size);
-        load_error = message;
-    }
-#else
-    handle = dlopen(full_name.c_str(), RTLD_LAZY);
-    if (!handle) {
-        load_error = dlerror();
-    }
-#endif // defined(_WIN32)
+    void(Load(full_name));
 }
 
 DynamicLibrary::~DynamicLibrary() {
@@ -46,7 +28,32 @@ DynamicLibrary::~DynamicLibrary() {
     }
 }
 
-void* DynamicLibrary::GetRawSymbol(std::string_view name) {
+bool DynamicLibrary::Load(std::string_view filename) {
+#if defined(_WIN32)
+    handle = reinterpret_cast<void*>(LoadLibraryA(filename.data()));
+    if (!handle) {
+        DWORD error_message_id = GetLastError();
+        LPSTR message_buffer = nullptr;
+        size_t size =
+            FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
+                               FORMAT_MESSAGE_IGNORE_INSERTS,
+                           nullptr, error_message_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+                           reinterpret_cast<LPSTR>(&message_buffer), 0, nullptr);
+        std::string message(message_buffer, size);
+        load_error = message;
+        return false;
+    }
+#else
+    handle = dlopen(filename.data(), RTLD_LAZY);
+    if (!handle) {
+        load_error = dlerror();
+        return false;
+    }
+#endif // defined(_WIN32)
+    return true;
+}
+
+void* DynamicLibrary::GetRawSymbol(std::string_view name) const {
 #if defined(_WIN32)
     return reinterpret_cast<void*>(GetProcAddress(reinterpret_cast<HMODULE>(handle), name.data()));
 #else
@@ -84,4 +91,4 @@ std::string DynamicLibrary::GetLibraryName(std::string_view name, int major, int
 #endif
 }
 
-} // namespace DynamicLibrary
+} // namespace Common
diff --git a/src/common/dynamic_library/dynamic_library.h b/src/common/dynamic_library/dynamic_library.h
index 9846c8326..2fbb7a1b8 100644
--- a/src/common/dynamic_library/dynamic_library.h
+++ b/src/common/dynamic_library/dynamic_library.h
@@ -5,35 +5,46 @@
 #pragma once
 
 #include <string>
-#include "common/common_types.h"
 
-namespace DynamicLibrary {
+namespace Common {
 
 class DynamicLibrary {
 public:
+    explicit DynamicLibrary();
     explicit DynamicLibrary(std::string_view name, int major = -1, int minor = -1);
     ~DynamicLibrary();
 
-    bool IsLoaded() {
+    /// Returns true if the library is loaded, otherwise false.
+    [[nodiscard]] bool IsLoaded() {
         return handle != nullptr;
     }
 
-    std::string_view GetLoadError() {
+    /// Loads (or replaces) the handle with the specified library file name.
+    /// Returns true if the library was loaded and can be used.
+    [[nodiscard]] bool Load(std::string_view filename);
+
+    /// Returns a string containing the last generated load error, if it occured.
+    [[nodiscard]] std::string_view GetLoadError() const {
         return load_error;
     }
 
+    /// Obtains the address of the specified symbol, automatically casting to the correct type.
     template <typename T>
-    T GetSymbol(std::string_view name) {
+    [[nodiscard]] T GetSymbol(std::string_view name) const {
         return reinterpret_cast<T>(GetRawSymbol(name));
     }
 
-    static std::string GetLibraryName(std::string_view name, int major = -1, int minor = -1);
+    /// Returns the specified library name in platform-specific format.
+    /// Major/minor versions will not be included if set to -1.
+    /// If libname already contains the "lib" prefix, it will not be added again.
+    [[nodiscard]] static std::string GetLibraryName(std::string_view name, int major = -1,
+                                                    int minor = -1);
 
 private:
-    void* GetRawSymbol(std::string_view name);
+    void* GetRawSymbol(std::string_view name) const;
 
     void* handle;
     std::string load_error;
 };
 
-} // namespace DynamicLibrary
+} // namespace Common
diff --git a/src/common/dynamic_library/fdk-aac.cpp b/src/common/dynamic_library/fdk-aac.cpp
index d4b0bcd62..20dedfd07 100644
--- a/src/common/dynamic_library/fdk-aac.cpp
+++ b/src/common/dynamic_library/fdk-aac.cpp
@@ -2,6 +2,7 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include "common/dynamic_library/dynamic_library.h"
 #include "common/dynamic_library/fdk-aac.h"
 #include "common/logging/log.h"
 
@@ -15,7 +16,7 @@ aacDecoder_GetStreamInfo_func aacDecoder_GetStreamInfo;
 aacDecoder_DecodeFrame_func aacDecoder_DecodeFrame;
 aacDecoder_Fill_func aacDecoder_Fill;
 
-static std::unique_ptr<DynamicLibrary> fdk_aac;
+static std::unique_ptr<Common::DynamicLibrary> fdk_aac;
 
 #define LOAD_SYMBOL(library, name)                                                                 \
     any_failed = any_failed || (name = library->GetSymbol<name##_func>(#name)) == nullptr
@@ -25,7 +26,7 @@ bool LoadFdkAac() {
         return true;
     }
 
-    fdk_aac = std::make_unique<DynamicLibrary>("fdk-aac", 2);
+    fdk_aac = std::make_unique<Common::DynamicLibrary>("fdk-aac", 2);
     if (!fdk_aac->IsLoaded()) {
         LOG_WARNING(Common, "Could not dynamically load libfdk-aac: {}", fdk_aac->GetLoadError());
         fdk_aac.reset();
diff --git a/src/common/dynamic_library/fdk-aac.h b/src/common/dynamic_library/fdk-aac.h
index 4c1dca4b8..a80e237eb 100644
--- a/src/common/dynamic_library/fdk-aac.h
+++ b/src/common/dynamic_library/fdk-aac.h
@@ -8,9 +8,6 @@ extern "C" {
 #include <fdk-aac/aacdecoder_lib.h>
 }
 
-#include "common/common_types.h"
-#include "common/dynamic_library/dynamic_library.h"
-
 namespace DynamicLibrary::FdkAac {
 
 typedef INT (*aacDecoder_GetLibInfo_func)(LIB_INFO* info);
diff --git a/src/common/dynamic_library/ffmpeg.cpp b/src/common/dynamic_library/ffmpeg.cpp
index 1da17faf4..c9c1af33a 100644
--- a/src/common/dynamic_library/ffmpeg.cpp
+++ b/src/common/dynamic_library/ffmpeg.cpp
@@ -2,6 +2,7 @@
 // Licensed under GPLv2 or any later version
 // Refer to the license.txt file included.
 
+#include "common/dynamic_library/dynamic_library.h"
 #include "common/dynamic_library/ffmpeg.h"
 #include "common/logging/log.h"
 
@@ -110,11 +111,11 @@ swr_free_func swr_free;
 swr_init_func swr_init;
 swresample_version_func swresample_version;
 
-static std::unique_ptr<DynamicLibrary> avutil;
-static std::unique_ptr<DynamicLibrary> avcodec;
-static std::unique_ptr<DynamicLibrary> avfilter;
-static std::unique_ptr<DynamicLibrary> avformat;
-static std::unique_ptr<DynamicLibrary> swresample;
+static std::unique_ptr<Common::DynamicLibrary> avutil;
+static std::unique_ptr<Common::DynamicLibrary> avcodec;
+static std::unique_ptr<Common::DynamicLibrary> avfilter;
+static std::unique_ptr<Common::DynamicLibrary> avformat;
+static std::unique_ptr<Common::DynamicLibrary> swresample;
 
 #define LOAD_SYMBOL(library, name)                                                                 \
     any_failed = any_failed || (name = library->GetSymbol<name##_func>(#name)) == nullptr
@@ -124,7 +125,7 @@ static bool LoadAVUtil() {
         return true;
     }
 
-    avutil = std::make_unique<DynamicLibrary>("avutil", LIBAVUTIL_VERSION_MAJOR);
+    avutil = std::make_unique<Common::DynamicLibrary>("avutil", LIBAVUTIL_VERSION_MAJOR);
     if (!avutil->IsLoaded()) {
         LOG_WARNING(Common, "Could not dynamically load libavutil: {}", avutil->GetLoadError());
         avutil.reset();
@@ -194,7 +195,7 @@ static bool LoadAVCodec() {
         return true;
     }
 
-    avcodec = std::make_unique<DynamicLibrary>("avcodec", LIBAVCODEC_VERSION_MAJOR);
+    avcodec = std::make_unique<Common::DynamicLibrary>("avcodec", LIBAVCODEC_VERSION_MAJOR);
     if (!avcodec->IsLoaded()) {
         LOG_WARNING(Common, "Could not dynamically load libavcodec: {}", avcodec->GetLoadError());
         avcodec.reset();
@@ -251,7 +252,7 @@ static bool LoadAVFilter() {
         return true;
     }
 
-    avfilter = std::make_unique<DynamicLibrary>("avfilter", LIBAVFILTER_VERSION_MAJOR);
+    avfilter = std::make_unique<Common::DynamicLibrary>("avfilter", LIBAVFILTER_VERSION_MAJOR);
     if (!avfilter->IsLoaded()) {
         LOG_WARNING(Common, "Could not dynamically load libavfilter: {}", avfilter->GetLoadError());
         avfilter.reset();
@@ -296,7 +297,7 @@ static bool LoadAVFormat() {
         return true;
     }
 
-    avformat = std::make_unique<DynamicLibrary>("avformat", LIBAVFORMAT_VERSION_MAJOR);
+    avformat = std::make_unique<Common::DynamicLibrary>("avformat", LIBAVFORMAT_VERSION_MAJOR);
     if (!avformat->IsLoaded()) {
         LOG_WARNING(Common, "Could not dynamically load libavformat: {}", avformat->GetLoadError());
         avformat.reset();
@@ -344,7 +345,8 @@ static bool LoadSWResample() {
         return true;
     }
 
-    swresample = std::make_unique<DynamicLibrary>("swresample", LIBSWRESAMPLE_VERSION_MAJOR);
+    swresample =
+        std::make_unique<Common::DynamicLibrary>("swresample", LIBSWRESAMPLE_VERSION_MAJOR);
     if (!swresample->IsLoaded()) {
         LOG_WARNING(Common, "Could not dynamically load libswresample: {}",
                     swresample->GetLoadError());
diff --git a/src/common/dynamic_library/ffmpeg.h b/src/common/dynamic_library/ffmpeg.h
index 67eac1787..f79fef8da 100644
--- a/src/common/dynamic_library/ffmpeg.h
+++ b/src/common/dynamic_library/ffmpeg.h
@@ -15,9 +15,6 @@ extern "C" {
 #include <libswresample/swresample.h>
 }
 
-#include "common/common_types.h"
-#include "common/dynamic_library/dynamic_library.h"
-
 namespace DynamicLibrary::FFmpeg {
 
 // avutil
diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp
index bbf0b48f9..de5ce7b41 100644
--- a/src/common/logging/backend.cpp
+++ b/src/common/logging/backend.cpp
@@ -234,6 +234,7 @@ void DebuggerBackend::Write(const Entry& entry) {
     CLS(Render)                                                                                    \
     SUB(Render, Software)                                                                          \
     SUB(Render, OpenGL)                                                                            \
+    SUB(Render, Vulkan)                                                                            \
     CLS(Audio)                                                                                     \
     SUB(Audio, DSP)                                                                                \
     SUB(Audio, Sink)                                                                               \
diff --git a/src/common/logging/log.h b/src/common/logging/log.h
index cf3cf0dea..8cd98db14 100644
--- a/src/common/logging/log.h
+++ b/src/common/logging/log.h
@@ -8,6 +8,7 @@
 #include <array>
 #include "common/common_types.h"
 #include "common/logging/formatter.h"
+
 namespace Log {
 
 // trims up to and including the last of ../, ..\, src/, src\ in a string
@@ -103,6 +104,7 @@ enum class Class : ClassType {
     Render,            ///< Emulator video output and hardware acceleration
     Render_Software,   ///< Software renderer backend
     Render_OpenGL,     ///< OpenGL backend
+    Render_Vulkan,     ///< Vulkan backend
     Audio,             ///< Audio emulation
     Audio_DSP,         ///< The HLE and LLE implementations of the DSP
     Audio_Sink,        ///< Emulator audio output backend
diff --git a/src/common/settings.h b/src/common/settings.h
index f80205ba9..37754b495 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -432,6 +432,7 @@ struct Values {
         "graphics_api"};
     Setting<bool> use_gles{false, "use_gles"};
     Setting<bool> renderer_debug{false, "renderer_debug"};
+    Setting<bool> dump_command_buffers{false, "dump_command_buffers"};
     SwitchableSetting<bool> use_hw_shader{true, "use_hw_shader"};
     SwitchableSetting<bool> use_disk_shader_cache{true, "use_disk_shader_cache"};
     SwitchableSetting<bool> shaders_accurate_mul{true, "shaders_accurate_mul"};
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 547ed2d22..025274e03 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -99,6 +99,13 @@ add_library(video_core STATIC
     renderer_software/sw_rasterizer.h
     renderer_software/sw_texturing.cpp
     renderer_software/sw_texturing.h
+    renderer_vulkan/pica_to_vk.h
+    renderer_vulkan/vk_common.cpp
+    renderer_vulkan/vk_common.h
+    renderer_vulkan/vk_instance.cpp
+    renderer_vulkan/vk_instance.h
+    renderer_vulkan/vk_platform.cpp
+    renderer_vulkan/vk_platform.h
     shader/debug_data.h
     shader/shader.cpp
     shader/shader.h
@@ -127,7 +134,8 @@ target_include_directories(video_core PRIVATE ${HOST_SHADERS_INCLUDE})
 create_target_directory_groups(video_core)
 
 target_link_libraries(video_core PUBLIC citra_common citra_core)
-target_link_libraries(video_core PRIVATE Boost::serialization dds-ktx glad json-headers nihstro-headers tsl::robin_map)
+target_link_libraries(video_core PRIVATE Boost::serialization dds-ktx json-headers nihstro-headers tsl::robin_map)
+target_link_libraries(video_core PRIVATE vulkan-headers vma glad)
 set_target_properties(video_core PROPERTIES INTERPROCEDURAL_OPTIMIZATION ${ENABLE_LTO})
 
 if ("x86_64" IN_LIST ARCHITECTURE)
diff --git a/src/video_core/renderer_vulkan/pica_to_vk.h b/src/video_core/renderer_vulkan/pica_to_vk.h
new file mode 100644
index 000000000..a5546f1dd
--- /dev/null
+++ b/src/video_core/renderer_vulkan/pica_to_vk.h
@@ -0,0 +1,198 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "common/logging/log.h"
+#include "core/core.h"
+#include "video_core/regs.h"
+#include "video_core/renderer_vulkan/vk_common.h"
+
+namespace PicaToVK {
+
+using TextureFilter = Pica::TexturingRegs::TextureConfig::TextureFilter;
+
+inline vk::Filter TextureFilterMode(TextureFilter mode) {
+    switch (mode) {
+    case TextureFilter::Linear:
+        return vk::Filter::eLinear;
+    case TextureFilter::Nearest:
+        return vk::Filter::eNearest;
+    default:
+        UNIMPLEMENTED_MSG("Unknown texture filtering mode {}", mode);
+    }
+
+    return vk::Filter::eLinear;
+}
+
+inline vk::SamplerMipmapMode TextureMipFilterMode(TextureFilter mip) {
+    switch (mip) {
+    case TextureFilter::Linear:
+        return vk::SamplerMipmapMode::eLinear;
+    case TextureFilter::Nearest:
+        return vk::SamplerMipmapMode::eNearest;
+    default:
+        UNIMPLEMENTED_MSG("Unknown texture mipmap filtering mode {}", mip);
+    }
+
+    return vk::SamplerMipmapMode::eLinear;
+}
+
+inline vk::SamplerAddressMode WrapMode(Pica::TexturingRegs::TextureConfig::WrapMode mode) {
+    static constexpr std::array<vk::SamplerAddressMode, 8> wrap_mode_table{{
+        vk::SamplerAddressMode::eClampToEdge,
+        vk::SamplerAddressMode::eClampToBorder,
+        vk::SamplerAddressMode::eRepeat,
+        vk::SamplerAddressMode::eMirroredRepeat,
+        // TODO(wwylele): ClampToEdge2 and ClampToBorder2 are not properly implemented here. See the
+        // comments in enum WrapMode.
+        vk::SamplerAddressMode::eClampToEdge,
+        vk::SamplerAddressMode::eClampToBorder,
+        vk::SamplerAddressMode::eRepeat,
+        vk::SamplerAddressMode::eRepeat,
+    }};
+
+    const auto index = static_cast<std::size_t>(mode);
+    ASSERT_MSG(index < wrap_mode_table.size(), "Unknown texture wrap mode {}", index);
+
+    if (index > 3) {
+        Core::System::GetInstance().TelemetrySession().AddField(
+            Common::Telemetry::FieldType::Session, "VideoCore_Pica_UnsupportedTextureWrapMode",
+            static_cast<u32>(index));
+        LOG_WARNING(Render_Vulkan, "Using texture wrap mode {}", index);
+    }
+
+    return wrap_mode_table[index];
+}
+
+inline vk::BlendOp BlendEquation(Pica::FramebufferRegs::BlendEquation equation) {
+    static constexpr std::array<vk::BlendOp, 5> blend_equation_table{{
+        vk::BlendOp::eAdd,
+        vk::BlendOp::eSubtract,
+        vk::BlendOp::eReverseSubtract,
+        vk::BlendOp::eMin,
+        vk::BlendOp::eMax,
+    }};
+
+    const auto index = static_cast<std::size_t>(equation);
+    ASSERT_MSG(index < blend_equation_table.size(), "Unknown blend equation {}", index);
+    return blend_equation_table[index];
+}
+
+inline vk::BlendFactor BlendFunc(Pica::FramebufferRegs::BlendFactor factor) {
+    static constexpr std::array<vk::BlendFactor, 15> blend_func_table{{
+        vk::BlendFactor::eZero,                  // BlendFactor::Zero
+        vk::BlendFactor::eOne,                   // BlendFactor::One
+        vk::BlendFactor::eSrcColor,              // BlendFactor::SourceColor
+        vk::BlendFactor::eOneMinusSrcColor,      // BlendFactor::OneMinusSourceColor
+        vk::BlendFactor::eDstColor,              // BlendFactor::DestColor
+        vk::BlendFactor::eOneMinusDstColor,      // BlendFactor::OneMinusDestColor
+        vk::BlendFactor::eSrcAlpha,              // BlendFactor::SourceAlpha
+        vk::BlendFactor::eOneMinusSrcAlpha,      // BlendFactor::OneMinusSourceAlpha
+        vk::BlendFactor::eDstAlpha,              // BlendFactor::DestAlpha
+        vk::BlendFactor::eOneMinusDstAlpha,      // BlendFactor::OneMinusDestAlpha
+        vk::BlendFactor::eConstantColor,         // BlendFactor::ConstantColor
+        vk::BlendFactor::eOneMinusConstantColor, // BlendFactor::OneMinusConstantColor
+        vk::BlendFactor::eConstantAlpha,         // BlendFactor::ConstantAlpha
+        vk::BlendFactor::eOneMinusConstantAlpha, // BlendFactor::OneMinusConstantAlpha
+        vk::BlendFactor::eSrcAlphaSaturate,      // BlendFactor::SourceAlphaSaturate
+    }};
+
+    const auto index = static_cast<std::size_t>(factor);
+    ASSERT_MSG(index < blend_func_table.size(), "Unknown blend factor {}", index);
+    return blend_func_table[index];
+}
+
+inline vk::LogicOp LogicOp(Pica::FramebufferRegs::LogicOp op) {
+    static constexpr std::array<vk::LogicOp, 16> logic_op_table{{
+        vk::LogicOp::eClear,        // Clear
+        vk::LogicOp::eAnd,          // And
+        vk::LogicOp::eAndReverse,   // AndReverse
+        vk::LogicOp::eCopy,         // Copy
+        vk::LogicOp::eSet,          // Set
+        vk::LogicOp::eCopyInverted, // CopyInverted
+        vk::LogicOp::eNoOp,         // NoOp
+        vk::LogicOp::eInvert,       // Invert
+        vk::LogicOp::eNand,         // Nand
+        vk::LogicOp::eOr,           // Or
+        vk::LogicOp::eNor,          // Nor
+        vk::LogicOp::eXor,          // Xor
+        vk::LogicOp::eEquivalent,   // Equiv
+        vk::LogicOp::eAndInverted,  // AndInverted
+        vk::LogicOp::eOrReverse,    // OrReverse
+        vk::LogicOp::eOrInverted,   // OrInverted
+    }};
+
+    const auto index = static_cast<std::size_t>(op);
+    ASSERT_MSG(index < logic_op_table.size(), "Unknown logic op {}", index);
+    return logic_op_table[index];
+}
+
+inline vk::CompareOp CompareFunc(Pica::FramebufferRegs::CompareFunc func) {
+    static constexpr std::array<vk::CompareOp, 8> compare_func_table{{
+        vk::CompareOp::eNever,          // CompareFunc::Never
+        vk::CompareOp::eAlways,         // CompareFunc::Always
+        vk::CompareOp::eEqual,          // CompareFunc::Equal
+        vk::CompareOp::eNotEqual,       // CompareFunc::NotEqual
+        vk::CompareOp::eLess,           // CompareFunc::LessThan
+        vk::CompareOp::eLessOrEqual,    // CompareFunc::LessThanOrEqual
+        vk::CompareOp::eGreater,        // CompareFunc::GreaterThan
+        vk::CompareOp::eGreaterOrEqual, // CompareFunc::GreaterThanOrEqual
+    }};
+
+    const auto index = static_cast<std::size_t>(func);
+    ASSERT_MSG(index < compare_func_table.size(), "Unknown compare function {}", index);
+    return compare_func_table[index];
+}
+
+inline vk::StencilOp StencilOp(Pica::FramebufferRegs::StencilAction action) {
+    static constexpr std::array<vk::StencilOp, 8> stencil_op_table{{
+        vk::StencilOp::eKeep,              // StencilAction::Keep
+        vk::StencilOp::eZero,              // StencilAction::Zero
+        vk::StencilOp::eReplace,           // StencilAction::Replace
+        vk::StencilOp::eIncrementAndClamp, // StencilAction::Increment
+        vk::StencilOp::eDecrementAndClamp, // StencilAction::Decrement
+        vk::StencilOp::eInvert,            // StencilAction::Invert
+        vk::StencilOp::eIncrementAndWrap,  // StencilAction::IncrementWrap
+        vk::StencilOp::eDecrementAndWrap,  // StencilAction::DecrementWrap
+    }};
+
+    const auto index = static_cast<std::size_t>(action);
+    ASSERT_MSG(index < stencil_op_table.size(), "Unknown stencil op {}", index);
+    return stencil_op_table[index];
+}
+
+inline vk::PrimitiveTopology PrimitiveTopology(Pica::PipelineRegs::TriangleTopology topology) {
+    switch (topology) {
+    case Pica::PipelineRegs::TriangleTopology::Fan:
+        return vk::PrimitiveTopology::eTriangleFan;
+    case Pica::PipelineRegs::TriangleTopology::List:
+    case Pica::PipelineRegs::TriangleTopology::Shader:
+        return vk::PrimitiveTopology::eTriangleList;
+    case Pica::PipelineRegs::TriangleTopology::Strip:
+        return vk::PrimitiveTopology::eTriangleStrip;
+    }
+}
+
+inline vk::CullModeFlags CullMode(Pica::RasterizerRegs::CullMode mode) {
+    switch (mode) {
+    case Pica::RasterizerRegs::CullMode::KeepAll:
+        return vk::CullModeFlagBits::eNone;
+    case Pica::RasterizerRegs::CullMode::KeepClockWise:
+    case Pica::RasterizerRegs::CullMode::KeepCounterClockWise:
+        return vk::CullModeFlagBits::eBack;
+    }
+}
+
+inline vk::FrontFace FrontFace(Pica::RasterizerRegs::CullMode mode) {
+    switch (mode) {
+    case Pica::RasterizerRegs::CullMode::KeepAll:
+    case Pica::RasterizerRegs::CullMode::KeepClockWise:
+        return vk::FrontFace::eCounterClockwise;
+    case Pica::RasterizerRegs::CullMode::KeepCounterClockWise:
+        return vk::FrontFace::eClockwise;
+    }
+}
+
+} // namespace PicaToVK
diff --git a/src/video_core/renderer_vulkan/vk_common.cpp b/src/video_core/renderer_vulkan/vk_common.cpp
new file mode 100644
index 000000000..ebec21566
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_common.cpp
@@ -0,0 +1,12 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "video_core/renderer_vulkan/vk_common.h"
+
+// Implement vma functions
+#define VMA_IMPLEMENTATION
+#include <vk_mem_alloc.h>
+
+// Store the dispatch loader here
+VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE
diff --git a/src/video_core/renderer_vulkan/vk_common.h b/src/video_core/renderer_vulkan/vk_common.h
new file mode 100644
index 000000000..a8147acbe
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_common.h
@@ -0,0 +1,16 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+// Include vulkan-hpp header
+#define VK_ENABLE_BETA_EXTENSIONS
+#define VK_NO_PROTOTYPES
+#define VULKAN_HPP_DISPATCH_LOADER_DYNAMIC 1
+#define VULKAN_HPP_NO_CONSTRUCTORS
+#define VULKAN_HPP_NO_STRUCT_SETTERS
+#include <vulkan/vulkan.hpp>
+
+#define VMA_STATIC_VULKAN_FUNCTIONS 0
+#define VMA_DYNAMIC_VULKAN_FUNCTIONS 1
diff --git a/src/video_core/renderer_vulkan/vk_instance.cpp b/src/video_core/renderer_vulkan/vk_instance.cpp
new file mode 100644
index 000000000..2388032a8
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_instance.cpp
@@ -0,0 +1,581 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <span>
+#include <boost/container/static_vector.hpp>
+
+#include "common/assert.h"
+#include "common/settings.h"
+#include "core/frontend/emu_window.h"
+#include "video_core/custom_textures/custom_format.h"
+#include "video_core/renderer_vulkan/vk_instance.h"
+#include "video_core/renderer_vulkan/vk_platform.h"
+
+#include <vk_mem_alloc.h>
+
+namespace Vulkan {
+
+namespace {
+vk::Format MakeFormat(VideoCore::PixelFormat format) {
+    switch (format) {
+    case VideoCore::PixelFormat::RGBA8:
+        return vk::Format::eR8G8B8A8Unorm;
+    case VideoCore::PixelFormat::RGB8:
+        return vk::Format::eB8G8R8Unorm;
+    case VideoCore::PixelFormat::RGB5A1:
+        return vk::Format::eR5G5B5A1UnormPack16;
+    case VideoCore::PixelFormat::RGB565:
+        return vk::Format::eR5G6B5UnormPack16;
+    case VideoCore::PixelFormat::RGBA4:
+        return vk::Format::eR4G4B4A4UnormPack16;
+    case VideoCore::PixelFormat::D16:
+        return vk::Format::eD16Unorm;
+    case VideoCore::PixelFormat::D24:
+        return vk::Format::eX8D24UnormPack32;
+    case VideoCore::PixelFormat::D24S8:
+        return vk::Format::eD24UnormS8Uint;
+    case VideoCore::PixelFormat::Invalid:
+        LOG_ERROR(Render_Vulkan, "Unknown texture format {}!", format);
+        return vk::Format::eUndefined;
+    default:
+        return vk::Format::eR8G8B8A8Unorm; ///< Use default case for the texture formats
+    }
+}
+
+vk::Format MakeCustomFormat(VideoCore::CustomPixelFormat format) {
+    switch (format) {
+    case VideoCore::CustomPixelFormat::RGBA8:
+        return vk::Format::eR8G8B8A8Unorm;
+    case VideoCore::CustomPixelFormat::BC1:
+        return vk::Format::eBc1RgbaUnormBlock;
+    case VideoCore::CustomPixelFormat::BC3:
+        return vk::Format::eBc3UnormBlock;
+    case VideoCore::CustomPixelFormat::BC5:
+        return vk::Format::eBc5UnormBlock;
+    case VideoCore::CustomPixelFormat::BC7:
+        return vk::Format::eBc7UnormBlock;
+    case VideoCore::CustomPixelFormat::ASTC4:
+        return vk::Format::eAstc4x4UnormBlock;
+    case VideoCore::CustomPixelFormat::ASTC6:
+        return vk::Format::eAstc6x6UnormBlock;
+    case VideoCore::CustomPixelFormat::ASTC8:
+        return vk::Format::eAstc8x6UnormBlock;
+    default:
+        LOG_ERROR(Render_Vulkan, "Unknown custom format {}", format);
+    }
+    return vk::Format::eR8G8B8A8Unorm;
+}
+
+vk::Format MakeAttributeFormat(Pica::PipelineRegs::VertexAttributeFormat format, u32 count,
+                               bool scaled = true) {
+    static constexpr std::array attrib_formats_scaled = {
+        vk::Format::eR8Sscaled,        vk::Format::eR8G8Sscaled,
+        vk::Format::eR8G8B8Sscaled,    vk::Format::eR8G8B8A8Sscaled,
+        vk::Format::eR8Uscaled,        vk::Format::eR8G8Uscaled,
+        vk::Format::eR8G8B8Uscaled,    vk::Format::eR8G8B8A8Uscaled,
+        vk::Format::eR16Sscaled,       vk::Format::eR16G16Sscaled,
+        vk::Format::eR16G16B16Sscaled, vk::Format::eR16G16B16A16Sscaled,
+        vk::Format::eR32Sfloat,        vk::Format::eR32G32Sfloat,
+        vk::Format::eR32G32B32Sfloat,  vk::Format::eR32G32B32A32Sfloat,
+    };
+    static constexpr std::array attrib_formats_int = {
+        vk::Format::eR8Sint,          vk::Format::eR8G8Sint,
+        vk::Format::eR8G8B8Sint,      vk::Format::eR8G8B8A8Sint,
+        vk::Format::eR8Uint,          vk::Format::eR8G8Uint,
+        vk::Format::eR8G8B8Uint,      vk::Format::eR8G8B8A8Uint,
+        vk::Format::eR16Sint,         vk::Format::eR16G16Sint,
+        vk::Format::eR16G16B16Sint,   vk::Format::eR16G16B16A16Sint,
+        vk::Format::eR32Sfloat,       vk::Format::eR32G32Sfloat,
+        vk::Format::eR32G32B32Sfloat, vk::Format::eR32G32B32A32Sfloat,
+    };
+
+    const u32 index = static_cast<u32>(format);
+    return (scaled ? attrib_formats_scaled : attrib_formats_int)[index * 4 + count - 1];
+}
+
+vk::ImageAspectFlags MakeAspect(VideoCore::SurfaceType type) {
+    switch (type) {
+    case VideoCore::SurfaceType::Color:
+    case VideoCore::SurfaceType::Texture:
+    case VideoCore::SurfaceType::Fill:
+        return vk::ImageAspectFlagBits::eColor;
+    case VideoCore::SurfaceType::Depth:
+        return vk::ImageAspectFlagBits::eDepth;
+    case VideoCore::SurfaceType::DepthStencil:
+        return vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil;
+    default:
+        LOG_CRITICAL(Render_Vulkan, "Invalid surface type {}", type);
+        UNREACHABLE();
+    }
+
+    return vk::ImageAspectFlagBits::eColor;
+}
+
+std::vector<std::string> GetSupportedExtensions(vk::PhysicalDevice physical) {
+    const std::vector extensions = physical.enumerateDeviceExtensionProperties();
+    std::vector<std::string> supported_extensions;
+    supported_extensions.reserve(extensions.size());
+    for (const auto& extension : extensions) {
+        supported_extensions.emplace_back(extension.extensionName.data());
+    }
+    return supported_extensions;
+}
+} // Anonymous namespace
+
+Instance::Instance(bool enable_validation, bool dump_command_buffers)
+    : library{OpenLibrary()}, instance{CreateInstance(*library,
+                                                      Frontend::WindowSystemType::Headless,
+                                                      enable_validation, dump_command_buffers)},
+      physical_devices{instance->enumeratePhysicalDevices()} {}
+
+Instance::Instance(Frontend::EmuWindow& window, u32 physical_device_index)
+    : library{OpenLibrary()}, instance{CreateInstance(
+                                  *library, window.GetWindowInfo().type,
+                                  Settings::values.renderer_debug.GetValue(),
+                                  Settings::values.dump_command_buffers.GetValue())},
+      debug_callback{CreateDebugCallback(*instance)}, physical_devices{
+                                                          instance->enumeratePhysicalDevices()} {
+    const std::size_t num_physical_devices = static_cast<u16>(physical_devices.size());
+    ASSERT_MSG(physical_device_index < num_physical_devices,
+               "Invalid physical device index {} provided when only {} devices exist",
+               physical_device_index, num_physical_devices);
+
+    physical_device = physical_devices[physical_device_index];
+    properties = physical_device.getProperties();
+
+    CollectTelemetryParameters();
+    CreateDevice();
+    CreateFormatTable();
+    CreateCustomFormatTable();
+    CreateAttribTable();
+}
+
+Instance::~Instance() {
+    vmaDestroyAllocator(allocator);
+}
+
+const FormatTraits& Instance::GetTraits(VideoCore::PixelFormat pixel_format) const {
+    if (pixel_format == VideoCore::PixelFormat::Invalid) [[unlikely]] {
+        return null_traits;
+    }
+    return format_table[static_cast<u32>(pixel_format)];
+}
+
+const FormatTraits& Instance::GetTraits(VideoCore::CustomPixelFormat pixel_format) const {
+    return custom_format_table[static_cast<u32>(pixel_format)];
+}
+
+const FormatTraits& Instance::GetTraits(Pica::PipelineRegs::VertexAttributeFormat format,
+                                        u32 count) const {
+    if (count == 0) [[unlikely]] {
+        ASSERT_MSG(false, "Unable to retrieve traits for invalid attribute component count");
+    }
+    const u32 index = static_cast<u32>(format);
+    return attrib_table[index * 4 + count - 1];
+}
+
+FormatTraits Instance::DetermineTraits(VideoCore::PixelFormat pixel_format, vk::Format format) {
+    const vk::ImageAspectFlags format_aspect = MakeAspect(VideoCore::GetFormatType(pixel_format));
+    const vk::FormatProperties format_properties = physical_device.getFormatProperties(format);
+
+    const vk::FormatFeatureFlagBits attachment_usage =
+        (format_aspect & vk::ImageAspectFlagBits::eDepth)
+            ? vk::FormatFeatureFlagBits::eDepthStencilAttachment
+            : vk::FormatFeatureFlagBits::eColorAttachmentBlend;
+
+    const vk::FormatFeatureFlags storage_usage = vk::FormatFeatureFlagBits::eStorageImage;
+    const vk::FormatFeatureFlags transfer_usage = vk::FormatFeatureFlagBits::eSampledImage;
+    const vk::FormatFeatureFlags blit_usage =
+        vk::FormatFeatureFlagBits::eBlitSrc | vk::FormatFeatureFlagBits::eBlitDst;
+
+    const bool supports_transfer =
+        (format_properties.optimalTilingFeatures & transfer_usage) == transfer_usage;
+    const bool supports_blit = (format_properties.optimalTilingFeatures & blit_usage) == blit_usage;
+    const bool supports_attachment =
+        (format_properties.optimalTilingFeatures & attachment_usage) == attachment_usage &&
+        pixel_format != VideoCore::PixelFormat::RGB8;
+    const bool supports_storage =
+        (format_properties.optimalTilingFeatures & storage_usage) == storage_usage;
+    const bool needs_conversion =
+        // Requires component flip.
+        pixel_format == VideoCore::PixelFormat::RGBA8 ||
+        // Requires (de)interleaving.
+        pixel_format == VideoCore::PixelFormat::D24S8;
+
+    // Find the most inclusive usage flags for this format
+    vk::ImageUsageFlags best_usage{};
+    if (supports_blit || supports_transfer) {
+        best_usage |= vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst |
+                      vk::ImageUsageFlagBits::eTransferSrc;
+    }
+    if (supports_attachment) {
+        best_usage |= (format_aspect & vk::ImageAspectFlagBits::eDepth)
+                          ? vk::ImageUsageFlagBits::eDepthStencilAttachment
+                          : vk::ImageUsageFlagBits::eColorAttachment;
+    }
+    if (supports_storage) {
+        best_usage |= vk::ImageUsageFlagBits::eStorage;
+    }
+
+    return FormatTraits{
+        .transfer_support = supports_transfer,
+        .blit_support = supports_blit,
+        .attachment_support = supports_attachment,
+        .storage_support = supports_storage,
+        .needs_conversion = needs_conversion,
+        .usage = best_usage,
+        .aspect = format_aspect,
+        .native = format,
+    };
+}
+
+void Instance::CreateFormatTable() {
+    constexpr std::array pixel_formats = {
+        VideoCore::PixelFormat::RGBA8,  VideoCore::PixelFormat::RGB8,
+        VideoCore::PixelFormat::RGB5A1, VideoCore::PixelFormat::RGB565,
+        VideoCore::PixelFormat::RGBA4,  VideoCore::PixelFormat::IA8,
+        VideoCore::PixelFormat::RG8,    VideoCore::PixelFormat::I8,
+        VideoCore::PixelFormat::A8,     VideoCore::PixelFormat::IA4,
+        VideoCore::PixelFormat::I4,     VideoCore::PixelFormat::A4,
+        VideoCore::PixelFormat::ETC1,   VideoCore::PixelFormat::ETC1A4,
+        VideoCore::PixelFormat::D16,    VideoCore::PixelFormat::D24,
+        VideoCore::PixelFormat::D24S8,
+    };
+
+    for (const auto& pixel_format : pixel_formats) {
+        const vk::Format format = MakeFormat(pixel_format);
+        FormatTraits traits = DetermineTraits(pixel_format, format);
+
+        const bool is_suitable =
+            traits.transfer_support && traits.attachment_support &&
+            (traits.blit_support || traits.aspect & vk::ImageAspectFlagBits::eDepth);
+
+        // Fall back if the native format is not suitable.
+        if (!is_suitable) {
+            // Always fallback to RGBA8 or D32(S8) for convenience
+            auto fallback = vk::Format::eR8G8B8A8Unorm;
+            if (traits.aspect & vk::ImageAspectFlagBits::eDepth) {
+                fallback = vk::Format::eD32Sfloat;
+                if (traits.aspect & vk::ImageAspectFlagBits::eStencil) {
+                    fallback = vk::Format::eD32SfloatS8Uint;
+                }
+            }
+            LOG_WARNING(Render_Vulkan, "Format {} unsupported, falling back unconditionally to {}",
+                        vk::to_string(format), vk::to_string(fallback));
+            traits = DetermineTraits(pixel_format, fallback);
+            // Always requires conversion if backing format does not match.
+            traits.needs_conversion = true;
+        }
+
+        const u32 index = static_cast<u32>(pixel_format);
+        format_table[index] = traits;
+    }
+}
+
+void Instance::CreateCustomFormatTable() {
+    // The traits are the same for RGBA8
+    custom_format_table[0] = format_table[static_cast<u32>(VideoCore::PixelFormat::RGBA8)];
+
+    constexpr std::array custom_formats = {
+        VideoCore::CustomPixelFormat::BC1,   VideoCore::CustomPixelFormat::BC3,
+        VideoCore::CustomPixelFormat::BC5,   VideoCore::CustomPixelFormat::BC7,
+        VideoCore::CustomPixelFormat::ASTC4, VideoCore::CustomPixelFormat::ASTC6,
+        VideoCore::CustomPixelFormat::ASTC8,
+    };
+
+    for (const auto& custom_format : custom_formats) {
+        const vk::Format format = MakeCustomFormat(custom_format);
+        const vk::FormatProperties format_properties = physical_device.getFormatProperties(format);
+
+        // Compressed formats don't support blit_dst in general so just check for transfer
+        const vk::FormatFeatureFlags transfer_usage = vk::FormatFeatureFlagBits::eSampledImage;
+        const bool supports_transfer =
+            (format_properties.optimalTilingFeatures & transfer_usage) == transfer_usage;
+
+        vk::ImageUsageFlags best_usage{};
+        if (supports_transfer) {
+            best_usage |= vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst |
+                          vk::ImageUsageFlagBits::eTransferSrc;
+        }
+
+        const u32 index = static_cast<u32>(custom_format);
+        custom_format_table[index] = FormatTraits{
+            .transfer_support = supports_transfer,
+            .usage = best_usage,
+            .aspect = vk::ImageAspectFlagBits::eColor,
+            .native = format,
+        };
+    }
+}
+
+void Instance::DetermineEmulation(Pica::PipelineRegs::VertexAttributeFormat format,
+                                  bool& needs_cast) {
+    // Check if (u)scaled formats can be used to emulate the 3 component format
+    vk::Format four_comp_format = MakeAttributeFormat(format, 4);
+    vk::FormatProperties format_properties = physical_device.getFormatProperties(four_comp_format);
+    needs_cast = !(format_properties.bufferFeatures & vk::FormatFeatureFlagBits::eVertexBuffer);
+}
+
+void Instance::CreateAttribTable() {
+    constexpr std::array attrib_formats = {
+        Pica::PipelineRegs::VertexAttributeFormat::BYTE,
+        Pica::PipelineRegs::VertexAttributeFormat::UBYTE,
+        Pica::PipelineRegs::VertexAttributeFormat::SHORT,
+        Pica::PipelineRegs::VertexAttributeFormat::FLOAT,
+    };
+
+    for (const auto& format : attrib_formats) {
+        for (u32 count = 1; count <= 4; count++) {
+            bool needs_cast{false};
+            bool needs_emulation{false};
+            vk::Format attrib_format = MakeAttributeFormat(format, count);
+            vk::FormatProperties format_properties =
+                physical_device.getFormatProperties(attrib_format);
+            if (!(format_properties.bufferFeatures & vk::FormatFeatureFlagBits::eVertexBuffer)) {
+                needs_cast = true;
+                attrib_format = MakeAttributeFormat(format, count, false);
+                format_properties = physical_device.getFormatProperties(attrib_format);
+                if (!(format_properties.bufferFeatures &
+                      vk::FormatFeatureFlagBits::eVertexBuffer)) {
+                    ASSERT_MSG(
+                        count == 3,
+                        "Vertex attribute emulation is only supported for 3 component formats");
+                    DetermineEmulation(format, needs_cast);
+                    needs_emulation = true;
+                }
+            }
+
+            const u32 index = static_cast<u32>(format) * 4 + count - 1;
+            attrib_table[index] = FormatTraits{
+                .needs_conversion = needs_cast,
+                .needs_emulation = needs_emulation,
+                .native = attrib_format,
+            };
+        }
+    }
+}
+
+bool Instance::CreateDevice() {
+    const vk::StructureChain feature_chain = physical_device.getFeatures2<
+        vk::PhysicalDeviceFeatures2, vk::PhysicalDevicePortabilitySubsetFeaturesKHR,
+        vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT,
+        vk::PhysicalDeviceExtendedDynamicState2FeaturesEXT,
+        vk::PhysicalDeviceExtendedDynamicState3FeaturesEXT,
+        vk::PhysicalDeviceTimelineSemaphoreFeaturesKHR,
+        vk::PhysicalDeviceCustomBorderColorFeaturesEXT, vk::PhysicalDeviceIndexTypeUint8FeaturesEXT,
+        vk::PhysicalDevicePipelineCreationCacheControlFeaturesEXT>();
+    const vk::StructureChain properties_chain =
+        physical_device.getProperties2<vk::PhysicalDeviceProperties2,
+                                       vk::PhysicalDevicePortabilitySubsetPropertiesKHR>();
+
+    features = feature_chain.get().features;
+    available_extensions = GetSupportedExtensions(physical_device);
+    if (available_extensions.empty()) {
+        LOG_CRITICAL(Render_Vulkan, "No extensions supported by device.");
+        return false;
+    }
+
+    boost::container::static_vector<const char*, 12> enabled_extensions;
+    const auto add_extension = [&](std::string_view extension, bool blacklist = false,
+                                   std::string_view reason = "") -> bool {
+        const auto result =
+            std::find_if(available_extensions.begin(), available_extensions.end(),
+                         [&](const std::string& name) { return name == extension; });
+
+        if (result != available_extensions.end() && !blacklist) {
+            LOG_INFO(Render_Vulkan, "Enabling extension: {}", extension);
+            enabled_extensions.push_back(extension.data());
+            return true;
+        } else if (blacklist) {
+            LOG_WARNING(Render_Vulkan, "Extension {} has been blacklisted because {}", extension,
+                        reason);
+            return false;
+        }
+
+        LOG_WARNING(Render_Vulkan, "Extension {} unavailable.", extension);
+        return false;
+    };
+
+    const bool is_arm = driver_id == vk::DriverIdKHR::eArmProprietary;
+    const bool is_qualcomm = driver_id == vk::DriverIdKHR::eQualcommProprietary;
+
+    add_extension(VK_KHR_SWAPCHAIN_EXTENSION_NAME);
+    image_format_list = add_extension(VK_KHR_IMAGE_FORMAT_LIST_EXTENSION_NAME);
+    shader_stencil_export = add_extension(VK_EXT_SHADER_STENCIL_EXPORT_EXTENSION_NAME);
+    const bool has_timeline_semaphores = add_extension(
+        VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME, is_qualcomm, "it is broken on Qualcomm drivers");
+    const bool has_portability_subset = add_extension(VK_KHR_PORTABILITY_SUBSET_EXTENSION_NAME);
+    const bool has_extended_dynamic_state =
+        add_extension(VK_EXT_EXTENDED_DYNAMIC_STATE_EXTENSION_NAME, is_arm || is_qualcomm,
+                      "it is broken on Qualcomm and ARM drivers");
+    const bool has_custom_border_color = add_extension(VK_EXT_CUSTOM_BORDER_COLOR_EXTENSION_NAME);
+    const bool has_index_type_uint8 = add_extension(VK_EXT_INDEX_TYPE_UINT8_EXTENSION_NAME);
+    const bool has_pipeline_creation_cache_control =
+        add_extension(VK_EXT_PIPELINE_CREATION_CACHE_CONTROL_EXTENSION_NAME);
+
+    const auto family_properties = physical_device.getQueueFamilyProperties();
+    if (family_properties.empty()) {
+        LOG_CRITICAL(Render_Vulkan, "Physical device reported no queues.");
+        return false;
+    }
+
+    bool graphics_queue_found = false;
+    for (std::size_t i = 0; i < family_properties.size(); i++) {
+        const u32 index = static_cast<u32>(i);
+        if (family_properties[i].queueFlags & vk::QueueFlagBits::eGraphics) {
+            queue_family_index = index;
+            graphics_queue_found = true;
+        }
+    }
+
+    if (!graphics_queue_found) {
+        LOG_CRITICAL(Render_Vulkan, "Unable to find graphics and/or present queues.");
+        return false;
+    }
+
+    static constexpr std::array<f32, 1> queue_priorities = {1.0f};
+
+    const vk::DeviceQueueCreateInfo queue_info = {
+        .queueFamilyIndex = queue_family_index,
+        .queueCount = static_cast<u32>(queue_priorities.size()),
+        .pQueuePriorities = queue_priorities.data(),
+    };
+
+    vk::StructureChain device_chain = {
+        vk::DeviceCreateInfo{
+            .queueCreateInfoCount = 1u,
+            .pQueueCreateInfos = &queue_info,
+            .enabledExtensionCount = static_cast<u32>(enabled_extensions.size()),
+            .ppEnabledExtensionNames = enabled_extensions.data(),
+        },
+        vk::PhysicalDeviceFeatures2{
+            .features{
+                .geometryShader = features.geometryShader,
+                .logicOp = features.logicOp,
+                .depthClamp = features.depthClamp,
+                .largePoints = features.largePoints,
+                .samplerAnisotropy = features.samplerAnisotropy,
+                .fragmentStoresAndAtomics = features.fragmentStoresAndAtomics,
+                .shaderClipDistance = features.shaderClipDistance,
+            },
+        },
+        vk::PhysicalDevicePortabilitySubsetFeaturesKHR{},
+        vk::PhysicalDeviceTimelineSemaphoreFeaturesKHR{},
+        vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT{},
+        vk::PhysicalDeviceExtendedDynamicState2FeaturesEXT{},
+        vk::PhysicalDeviceExtendedDynamicState3FeaturesEXT{},
+        vk::PhysicalDeviceCustomBorderColorFeaturesEXT{},
+        vk::PhysicalDeviceIndexTypeUint8FeaturesEXT{},
+        vk::PhysicalDevicePipelineCreationCacheControlFeaturesEXT{},
+    };
+
+#define PROP_GET(structName, prop, property) property = properties_chain.get<structName>().prop;
+
+#define FEAT_SET(structName, feature, property)                                                    \
+    if (feature_chain.get<structName>().feature) {                                                 \
+        property = true;                                                                           \
+        device_chain.get<structName>().feature = true;                                             \
+    } else {                                                                                       \
+        property = false;                                                                          \
+        device_chain.get<structName>().feature = false;                                            \
+    }
+
+    if (has_portability_subset) {
+        FEAT_SET(vk::PhysicalDevicePortabilitySubsetFeaturesKHR, triangleFans,
+                 triangle_fan_supported)
+        FEAT_SET(vk::PhysicalDevicePortabilitySubsetFeaturesKHR, imageViewFormatReinterpretation,
+                 image_view_reinterpretation)
+        PROP_GET(vk::PhysicalDevicePortabilitySubsetPropertiesKHR,
+                 minVertexInputBindingStrideAlignment, min_vertex_stride_alignment)
+    } else {
+        device_chain.unlink<vk::PhysicalDevicePortabilitySubsetFeaturesKHR>();
+    }
+
+    if (has_timeline_semaphores) {
+        FEAT_SET(vk::PhysicalDeviceTimelineSemaphoreFeaturesKHR, timelineSemaphore,
+                 timeline_semaphores)
+    } else {
+        device_chain.unlink<vk::PhysicalDeviceTimelineSemaphoreFeaturesKHR>();
+    }
+
+    if (has_index_type_uint8) {
+        FEAT_SET(vk::PhysicalDeviceIndexTypeUint8FeaturesEXT, indexTypeUint8, index_type_uint8)
+    } else {
+        device_chain.unlink<vk::PhysicalDeviceIndexTypeUint8FeaturesEXT>();
+    }
+
+    if (has_extended_dynamic_state) {
+        FEAT_SET(vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT, extendedDynamicState,
+                 extended_dynamic_state)
+    } else {
+        device_chain.unlink<vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>();
+    }
+
+    if (has_custom_border_color) {
+        FEAT_SET(vk::PhysicalDeviceCustomBorderColorFeaturesEXT, customBorderColors,
+                 custom_border_color)
+        FEAT_SET(vk::PhysicalDeviceCustomBorderColorFeaturesEXT, customBorderColorWithoutFormat,
+                 custom_border_color)
+    } else {
+        device_chain.unlink<vk::PhysicalDeviceCustomBorderColorFeaturesEXT>();
+    }
+
+    if (has_pipeline_creation_cache_control) {
+        FEAT_SET(vk::PhysicalDevicePipelineCreationCacheControlFeaturesEXT,
+                 pipelineCreationCacheControl, pipeline_creation_cache_control)
+    } else {
+        device_chain.unlink<vk::PhysicalDevicePipelineCreationCacheControlFeaturesEXT>();
+    }
+
+#undef PROP_GET
+#undef FEAT_SET
+
+    try {
+        device = physical_device.createDeviceUnique(device_chain.get());
+    } catch (vk::ExtensionNotPresentError& err) {
+        LOG_CRITICAL(Render_Vulkan, "Some required extensions are not available {}", err.what());
+        return false;
+    }
+
+    VULKAN_HPP_DEFAULT_DISPATCHER.init(*device);
+
+    graphics_queue = device->getQueue(queue_family_index, 0);
+    present_queue = device->getQueue(queue_family_index, 0);
+
+    CreateAllocator();
+    return true;
+}
+
+void Instance::CreateAllocator() {
+    const VmaVulkanFunctions functions = {
+        .vkGetInstanceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr,
+        .vkGetDeviceProcAddr = VULKAN_HPP_DEFAULT_DISPATCHER.vkGetDeviceProcAddr,
+    };
+
+    const VmaAllocatorCreateInfo allocator_info = {
+        .physicalDevice = physical_device,
+        .device = *device,
+        .pVulkanFunctions = &functions,
+        .instance = *instance,
+        .vulkanApiVersion = vk::enumerateInstanceVersion(),
+    };
+
+    const VkResult result = vmaCreateAllocator(&allocator_info, &allocator);
+    if (result != VK_SUCCESS) {
+        UNREACHABLE_MSG("Failed to initialize VMA with error {}", result);
+    }
+}
+
+void Instance::CollectTelemetryParameters() {
+    const vk::StructureChain property_chain =
+        physical_device
+            .getProperties2<vk::PhysicalDeviceProperties2, vk::PhysicalDeviceDriverProperties>();
+    const vk::PhysicalDeviceDriverProperties driver =
+        property_chain.get<vk::PhysicalDeviceDriverProperties>();
+
+    driver_id = driver.driverID;
+    vendor_name = driver.driverName.data();
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_instance.h b/src/video_core/renderer_vulkan/vk_instance.h
new file mode 100644
index 000000000..dc5c07d3d
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_instance.h
@@ -0,0 +1,287 @@
+// Copyright 2022 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <span>
+
+#include "video_core/rasterizer_cache/pixel_format.h"
+#include "video_core/regs_pipeline.h"
+#include "video_core/renderer_vulkan/vk_platform.h"
+
+namespace Frontend {
+class EmuWindow;
+}
+
+namespace VideoCore {
+enum class CustomPixelFormat : u32;
+}
+
+VK_DEFINE_HANDLE(VmaAllocator)
+
+namespace Vulkan {
+
+struct FormatTraits {
+    bool transfer_support = false;
+    bool blit_support = false;
+    bool attachment_support = false;
+    bool storage_support = false;
+    bool needs_conversion = false;
+    bool needs_emulation = false;
+    vk::ImageUsageFlags usage{};
+    vk::ImageAspectFlags aspect;
+    vk::Format native = vk::Format::eUndefined;
+};
+
+class Instance {
+public:
+    explicit Instance(bool validation = false, bool dump_command_buffers = false);
+    explicit Instance(Frontend::EmuWindow& window, u32 physical_device_index);
+    ~Instance();
+
+    /// Returns the FormatTraits struct for the provided pixel format
+    const FormatTraits& GetTraits(VideoCore::PixelFormat pixel_format) const;
+    const FormatTraits& GetTraits(VideoCore::CustomPixelFormat pixel_format) const;
+
+    /// Returns the FormatTraits struct for the provided attribute format and count
+    const FormatTraits& GetTraits(Pica::PipelineRegs::VertexAttributeFormat format,
+                                  u32 count) const;
+
+    /// Returns the Vulkan instance
+    vk::Instance GetInstance() const {
+        return *instance;
+    }
+
+    /// Returns the current physical device
+    vk::PhysicalDevice GetPhysicalDevice() const {
+        return physical_device;
+    }
+
+    /// Returns the Vulkan device
+    vk::Device GetDevice() const {
+        return *device;
+    }
+
+    /// Returns the VMA allocator handle
+    VmaAllocator GetAllocator() const {
+        return allocator;
+    }
+
+    /// Returns a list of the available physical devices
+    std::span<const vk::PhysicalDevice> GetPhysicalDevices() const {
+        return physical_devices;
+    }
+
+    /// Retrieve queue information
+    u32 GetGraphicsQueueFamilyIndex() const {
+        return queue_family_index;
+    }
+
+    u32 GetPresentQueueFamilyIndex() const {
+        return queue_family_index;
+    }
+
+    vk::Queue GetGraphicsQueue() const {
+        return graphics_queue;
+    }
+
+    vk::Queue GetPresentQueue() const {
+        return present_queue;
+    }
+
+    /// Returns true if logic operations need shader emulation
+    bool NeedsLogicOpEmulation() const {
+        return !features.logicOp;
+    }
+
+    bool UseGeometryShaders() const {
+#ifdef __ANDROID__
+        // Geometry shaders are extremely expensive on tilers to avoid them at all
+        // cost even if it hurts accuracy somewhat. TODO: Make this an option
+        return false;
+#else
+        return features.geometryShader;
+#endif
+    }
+
+    /// Returns true if anisotropic filtering is supported
+    bool IsAnisotropicFilteringSupported() const {
+        return features.samplerAnisotropy;
+    }
+
+    /// Returns true when VK_KHR_timeline_semaphore is supported
+    bool IsTimelineSemaphoreSupported() const {
+        return timeline_semaphores;
+    }
+
+    /// Returns true when VK_EXT_extended_dynamic_state is supported
+    bool IsExtendedDynamicStateSupported() const {
+        return extended_dynamic_state;
+    }
+
+    /// Returns true when VK_EXT_custom_border_color is supported
+    bool IsCustomBorderColorSupported() const {
+        return custom_border_color;
+    }
+
+    /// Returns true when VK_EXT_index_type_uint8 is supported
+    bool IsIndexTypeUint8Supported() const {
+        return index_type_uint8;
+    }
+
+    /// Returns true when VK_KHR_image_format_list is supported
+    bool IsImageFormatListSupported() const {
+        return image_format_list;
+    }
+
+    /// Returns true when VK_EXT_pipeline_creation_cache_control is supported
+    bool IsPipelineCreationCacheControlSupported() const {
+        return pipeline_creation_cache_control;
+    }
+
+    /// Returns true when VK_EXT_shader_stencil_export is supported
+    bool IsShaderStencilExportSupported() const {
+        return shader_stencil_export;
+    }
+
+    /// Returns true if VK_EXT_debug_utils is supported
+    bool IsExtDebugUtilsSupported() const {
+        return debug_messenger_supported;
+    }
+
+    /// Returns the vendor ID of the physical device
+    u32 GetVendorID() const {
+        return properties.vendorID;
+    }
+
+    /// Returns the device ID of the physical device
+    u32 GetDeviceID() const {
+        return properties.deviceID;
+    }
+
+    /// Returns the driver ID.
+    vk::DriverId GetDriverID() const {
+        return driver_id;
+    }
+
+    /// Returns the current driver version provided in Vulkan-formatted version numbers.
+    u32 GetDriverVersion() const {
+        return properties.driverVersion;
+    }
+
+    /// Returns the current Vulkan API version provided in Vulkan-formatted version numbers.
+    u32 ApiVersion() const {
+        return properties.apiVersion;
+    }
+
+    /// Returns the vendor name reported from Vulkan.
+    std::string_view GetVendorName() const {
+        return vendor_name;
+    }
+
+    /// Returns the list of available extensions.
+    const std::vector<std::string>& GetAvailableExtensions() const {
+        return available_extensions;
+    }
+
+    /// Returns the device name.
+    std::string_view GetModelName() const {
+        return properties.deviceName;
+    }
+
+    /// Returns the pipeline cache unique identifier
+    const auto GetPipelineCacheUUID() const {
+        return properties.pipelineCacheUUID;
+    }
+
+    /// Returns the minimum required alignment for uniforms
+    vk::DeviceSize UniformMinAlignment() const {
+        return properties.limits.minUniformBufferOffsetAlignment;
+    }
+
+    /// Returns the maximum supported elements in a texel buffer
+    u32 MaxTexelBufferElements() const {
+        return properties.limits.maxTexelBufferElements;
+    }
+
+    /// Returns true if shaders can declare the ClipDistance attribute
+    bool IsShaderClipDistanceSupported() const {
+        return features.shaderClipDistance;
+    }
+
+    /// Returns true if triangle fan is an accepted primitive topology
+    bool IsTriangleFanSupported() const {
+        return triangle_fan_supported;
+    }
+
+    /// Returns the minimum vertex stride alignment
+    u32 GetMinVertexStrideAlignment() const {
+        return min_vertex_stride_alignment;
+    }
+
+    /// Returns true if commands should be flushed at the end of each major renderpass
+    bool ShouldFlush() const {
+        return driver_id == vk::DriverIdKHR::eArmProprietary ||
+               driver_id == vk::DriverIdKHR::eQualcommProprietary;
+    }
+
+private:
+    /// Returns the optimal supported usage for the requested format
+    [[nodiscard]] FormatTraits DetermineTraits(VideoCore::PixelFormat pixel_format,
+                                               vk::Format format);
+
+    /// Determines the best available vertex attribute format emulation
+    void DetermineEmulation(Pica::PipelineRegs::VertexAttributeFormat format, bool& needs_cast);
+
+    /// Creates the format compatibility table for the current device
+    void CreateFormatTable();
+    void CreateCustomFormatTable();
+
+    /// Creates the attribute format table for the current device
+    void CreateAttribTable();
+
+    /// Creates the logical device opportunistically enabling extensions
+    bool CreateDevice();
+
+    /// Creates the VMA allocator handle
+    void CreateAllocator();
+
+    /// Collects telemetry information from the device.
+    void CollectTelemetryParameters();
+
+private:
+    std::shared_ptr<Common::DynamicLibrary> library;
+    vk::UniqueInstance instance;
+    vk::PhysicalDevice physical_device;
+    vk::UniqueDevice device;
+    vk::PhysicalDeviceProperties properties;
+    vk::PhysicalDeviceFeatures features;
+    vk::DriverIdKHR driver_id;
+    DebugCallback debug_callback;
+    std::string vendor_name;
+    VmaAllocator allocator{};
+    vk::Queue present_queue;
+    vk::Queue graphics_queue;
+    std::vector<vk::PhysicalDevice> physical_devices;
+    FormatTraits null_traits;
+    std::array<FormatTraits, VideoCore::PIXEL_FORMAT_COUNT> format_table;
+    std::array<FormatTraits, 10> custom_format_table;
+    std::array<FormatTraits, 16> attrib_table;
+    std::vector<std::string> available_extensions;
+    u32 queue_family_index{0};
+    bool triangle_fan_supported{true};
+    bool image_view_reinterpretation{true};
+    u32 min_vertex_stride_alignment{1};
+    bool timeline_semaphores{};
+    bool extended_dynamic_state{};
+    bool custom_border_color{};
+    bool index_type_uint8{};
+    bool image_format_list{};
+    bool pipeline_creation_cache_control{};
+    bool shader_stencil_export{};
+    bool debug_messenger_supported{};
+    bool debug_report_supported{};
+};
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_platform.cpp b/src/video_core/renderer_vulkan/vk_platform.cpp
new file mode 100644
index 000000000..dafb86a8d
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_platform.cpp
@@ -0,0 +1,366 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+// Include the vulkan platform specific header
+#if defined(ANDROID)
+#define VK_USE_PLATFORM_ANDROID_KHR
+#elif defined(WIN32)
+#define VK_USE_PLATFORM_WIN32_KHR
+#elif defined(__APPLE__)
+#define VK_USE_PLATFORM_METAL_EXT
+#else
+#define VK_USE_PLATFORM_WAYLAND_KHR
+#define VK_USE_PLATFORM_XLIB_KHR
+#endif
+
+#include <memory>
+#include <vector>
+#include <boost/container/static_vector.hpp>
+
+#include "common/assert.h"
+#include "common/logging/log.h"
+#include "common/settings.h"
+#include "core/frontend/emu_window.h"
+#include "video_core/renderer_vulkan/vk_platform.h"
+
+namespace Vulkan {
+
+namespace {
+static VKAPI_ATTR VkBool32 VKAPI_CALL DebugUtilsCallback(
+    VkDebugUtilsMessageSeverityFlagBitsEXT severity, VkDebugUtilsMessageTypeFlagsEXT type,
+    const VkDebugUtilsMessengerCallbackDataEXT* callback_data, void* user_data) {
+
+    switch (callback_data->messageIdNumber) {
+    case 0x609a13b: // Vertex attribute at location not consumed by shader
+        return VK_FALSE;
+    default:
+        break;
+    }
+
+    Log::Level level{};
+    switch (severity) {
+    case VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT:
+        level = Log::Level::Error;
+        break;
+    case VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT:
+        level = Log::Level::Info;
+        break;
+    case VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT:
+    case VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT:
+        level = Log::Level::Debug;
+        break;
+    default:
+        level = Log::Level::Info;
+    }
+
+    LOG_GENERIC(Log::Class::Render_Vulkan, level, "{}: {}",
+                callback_data->pMessageIdName ? callback_data->pMessageIdName : "<null>",
+                callback_data->pMessage ? callback_data->pMessage : "<null>");
+
+    return VK_FALSE;
+}
+
+static VKAPI_ATTR VkBool32 VKAPI_CALL DebugReportCallback(VkDebugReportFlagsEXT flags,
+                                                          VkDebugReportObjectTypeEXT objectType,
+                                                          uint64_t object, size_t location,
+                                                          int32_t messageCode,
+                                                          const char* pLayerPrefix,
+                                                          const char* pMessage, void* pUserData) {
+
+    const VkDebugReportFlagBitsEXT severity = static_cast<VkDebugReportFlagBitsEXT>(flags);
+    Log::Level level{};
+    switch (severity) {
+    case VK_DEBUG_REPORT_ERROR_BIT_EXT:
+        level = Log::Level::Error;
+        break;
+    case VK_DEBUG_REPORT_INFORMATION_BIT_EXT:
+        level = Log::Level::Warning;
+        break;
+    case VK_DEBUG_REPORT_DEBUG_BIT_EXT:
+    case VK_DEBUG_REPORT_WARNING_BIT_EXT:
+    case VK_DEBUG_REPORT_PERFORMANCE_WARNING_BIT_EXT:
+        level = Log::Level::Debug;
+        break;
+    default:
+        level = Log::Level::Info;
+    }
+
+    const vk::DebugReportObjectTypeEXT type = static_cast<vk::DebugReportObjectTypeEXT>(objectType);
+    LOG_GENERIC(Log::Class::Render_Vulkan, level,
+                "type = {}, object = {} | MessageCode = {:#x}, LayerPrefix = {} | {}",
+                vk::to_string(type), object, messageCode, pLayerPrefix, pMessage);
+
+    return VK_FALSE;
+}
+} // Anonymous namespace
+
+std::shared_ptr<Common::DynamicLibrary> OpenLibrary() {
+    auto library = std::make_shared<Common::DynamicLibrary>();
+#ifdef __APPLE__
+    const std::string filename = Common::DynamicLibrary::GetLibraryName("vulkan");
+    library->Load(filename);
+    if (!library->IsLoaded()) {
+        // Fall back to directly loading bundled MoltenVK library.
+        library->Load("libMoltenVK.dylib");
+    }
+#else
+    std::string filename = Common::DynamicLibrary::GetLibraryName("vulkan", 1);
+    LOG_DEBUG(Render_Vulkan, "Trying Vulkan library: {}", filename);
+    if (!library->Load(filename)) {
+        // Android devices may not have libvulkan.so.1, only libvulkan.so.
+        filename = Common::DynamicLibrary::GetLibraryName("vulkan");
+        LOG_DEBUG(Render_Vulkan, "Trying Vulkan library (second attempt): {}", filename);
+        void(library->Load(filename));
+    }
+#endif
+    return library;
+}
+
+vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::EmuWindow& emu_window) {
+    const auto& window_info = emu_window.GetWindowInfo();
+    vk::SurfaceKHR surface{};
+
+#if defined(VK_USE_PLATFORM_WIN32_KHR)
+    if (window_info.type == Frontend::WindowSystemType::Windows) {
+        const vk::Win32SurfaceCreateInfoKHR win32_ci = {
+            .hinstance = nullptr,
+            .hwnd = static_cast<HWND>(window_info.render_surface),
+        };
+
+        if (instance.createWin32SurfaceKHR(&win32_ci, nullptr, &surface) != vk::Result::eSuccess) {
+            LOG_CRITICAL(Render_Vulkan, "Failed to initialize Win32 surface");
+            UNREACHABLE();
+        }
+    }
+#elif defined(VK_USE_PLATFORM_XLIB_KHR) || defined(VK_USE_PLATFORM_WAYLAND_KHR)
+    if (window_info.type == Frontend::WindowSystemType::X11) {
+        const vk::XlibSurfaceCreateInfoKHR xlib_ci = {
+            .dpy = static_cast<Display*>(window_info.display_connection),
+            .window = reinterpret_cast<Window>(window_info.render_surface),
+        };
+
+        if (instance.createXlibSurfaceKHR(&xlib_ci, nullptr, &surface) != vk::Result::eSuccess) {
+            LOG_ERROR(Render_Vulkan, "Failed to initialize Xlib surface");
+            UNREACHABLE();
+        }
+    } else if (window_info.type == Frontend::WindowSystemType::Wayland) {
+        const vk::WaylandSurfaceCreateInfoKHR wayland_ci = {
+            .display = static_cast<wl_display*>(window_info.display_connection),
+            .surface = static_cast<wl_surface*>(window_info.render_surface),
+        };
+
+        if (instance.createWaylandSurfaceKHR(&wayland_ci, nullptr, &surface) !=
+            vk::Result::eSuccess) {
+            LOG_ERROR(Render_Vulkan, "Failed to initialize Wayland surface");
+            UNREACHABLE();
+        }
+    }
+#elif defined(VK_USE_PLATFORM_METAL_EXT)
+    if (window_info.type == Frontend::WindowSystemType::MacOS) {
+        const vk::MetalSurfaceCreateInfoEXT macos_ci = {
+            .pLayer = static_cast<const CAMetalLayer*>(window_info.render_surface),
+        };
+
+        if (instance.createMetalSurfaceEXT(&macos_ci, nullptr, &surface) != vk::Result::eSuccess) {
+            LOG_CRITICAL(Render_Vulkan, "Failed to initialize MacOS surface");
+            UNREACHABLE();
+        }
+    }
+#elif defined(VK_USE_PLATFORM_ANDROID_KHR)
+    if (window_info.type == Frontend::WindowSystemType::Android) {
+        vk::AndroidSurfaceCreateInfoKHR android_ci = {
+            .window = reinterpret_cast<ANativeWindow*>(window_info.render_surface),
+        };
+
+        if (instance.createAndroidSurfaceKHR(&android_ci, nullptr, &surface) !=
+            vk::Result::eSuccess) {
+            LOG_CRITICAL(Render_Vulkan, "Failed to initialize Android surface");
+            UNREACHABLE();
+        }
+    }
+#endif
+
+    if (!surface) {
+        LOG_CRITICAL(Render_Vulkan, "Presentation not supported on this platform");
+        UNREACHABLE();
+    }
+
+    return surface;
+}
+
+std::vector<const char*> GetInstanceExtensions(Frontend::WindowSystemType window_type,
+                                               bool enable_debug_utils) {
+    const auto properties = vk::enumerateInstanceExtensionProperties();
+    if (properties.empty()) {
+        LOG_ERROR(Render_Vulkan, "Failed to query extension properties");
+        return {};
+    }
+
+    // Add the windowing system specific extension
+    std::vector<const char*> extensions;
+    extensions.reserve(6);
+
+#if defined(__APPLE__)
+    extensions.push_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME);
+#endif
+
+    switch (window_type) {
+    case Frontend::WindowSystemType::Headless:
+        break;
+#if defined(VK_USE_PLATFORM_WIN32_KHR)
+    case Frontend::WindowSystemType::Windows:
+        extensions.push_back(VK_KHR_WIN32_SURFACE_EXTENSION_NAME);
+        break;
+#elif defined(VK_USE_PLATFORM_XLIB_KHR) || defined(VK_USE_PLATFORM_WAYLAND_KHR)
+    case Frontend::WindowSystemType::X11:
+        extensions.push_back(VK_KHR_XLIB_SURFACE_EXTENSION_NAME);
+        break;
+    case Frontend::WindowSystemType::Wayland:
+        extensions.push_back(VK_KHR_WAYLAND_SURFACE_EXTENSION_NAME);
+        break;
+#elif defined(VK_USE_PLATFORM_METAL_EXT)
+    case Frontend::WindowSystemType::MacOS:
+        extensions.push_back(VK_EXT_METAL_SURFACE_EXTENSION_NAME);
+        break;
+#elif defined(VK_USE_PLATFORM_ANDROID_KHR)
+    case Frontend::WindowSystemType::Android:
+        extensions.push_back(VK_KHR_ANDROID_SURFACE_EXTENSION_NAME);
+        break;
+#endif
+    default:
+        LOG_ERROR(Render_Vulkan, "Presentation not supported on this platform");
+        break;
+    }
+
+    if (window_type != Frontend::WindowSystemType::Headless) {
+        extensions.push_back(VK_KHR_SURFACE_EXTENSION_NAME);
+    }
+
+    if (enable_debug_utils) {
+        extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
+        extensions.push_back(VK_EXT_DEBUG_REPORT_EXTENSION_NAME);
+    }
+
+    // Sanitize extension list
+    std::erase_if(extensions, [&](const char* extension) -> bool {
+        const auto it =
+            std::find_if(properties.begin(), properties.end(), [extension](const auto& prop) {
+                return std::strcmp(extension, prop.extensionName) == 0;
+            });
+
+        if (it == properties.end()) {
+            LOG_INFO(Render_Vulkan, "Candidate instance extension {} is not available", extension);
+            return true;
+        }
+        return false;
+    });
+
+    return extensions;
+}
+
+vk::InstanceCreateFlags GetInstanceFlags() {
+#if defined(__APPLE__)
+    return vk::InstanceCreateFlagBits::eEnumeratePortabilityKHR;
+#else
+    return static_cast<vk::InstanceCreateFlags>(0);
+#endif
+}
+
+vk::UniqueInstance CreateInstance(const Common::DynamicLibrary& library,
+                                  Frontend::WindowSystemType window_type, bool enable_validation,
+                                  bool dump_command_buffers) {
+    const auto vkGetInstanceProcAddr =
+        library.GetSymbol<PFN_vkGetInstanceProcAddr>("vkGetInstanceProcAddr");
+    if (!vkGetInstanceProcAddr) {
+        LOG_CRITICAL(Render_Vulkan, "Failed GetSymbol vkGetInstanceProcAddr");
+        return {};
+    }
+    VULKAN_HPP_DEFAULT_DISPATCHER.init(vkGetInstanceProcAddr);
+
+    const auto extensions = GetInstanceExtensions(window_type, enable_validation);
+    const u32 available_version = vk::enumerateInstanceVersion();
+    if (available_version < VK_API_VERSION_1_1) {
+        LOG_CRITICAL(Render_Vulkan, "Vulkan 1.0 is not supported, 1.1 is required!");
+        return {};
+    }
+
+    const vk::ApplicationInfo application_info = {
+        .pApplicationName = "Citra",
+        .applicationVersion = VK_MAKE_VERSION(1, 0, 0),
+        .pEngineName = "Citra Vulkan",
+        .engineVersion = VK_MAKE_VERSION(1, 0, 0),
+        .apiVersion = available_version,
+    };
+
+    boost::container::static_vector<const char*, 2> layers;
+    if (enable_validation) {
+        layers.push_back("VK_LAYER_KHRONOS_validation");
+    }
+    if (dump_command_buffers) {
+        layers.push_back("VK_LAYER_LUNARG_api_dump");
+    }
+
+    const vk::InstanceCreateInfo instance_ci = {
+        .flags = GetInstanceFlags(),
+        .pApplicationInfo = &application_info,
+        .enabledLayerCount = static_cast<u32>(layers.size()),
+        .ppEnabledLayerNames = layers.data(),
+        .enabledExtensionCount = static_cast<u32>(extensions.size()),
+        .ppEnabledExtensionNames = extensions.data(),
+    };
+
+    auto instance = vk::createInstanceUnique(instance_ci);
+
+    VULKAN_HPP_DEFAULT_DISPATCHER.init(*instance);
+
+    return instance;
+}
+
+vk::UniqueDebugUtilsMessengerEXT CreateDebugMessenger(vk::Instance instance) {
+    const vk::DebugUtilsMessengerCreateInfoEXT msg_ci = {
+        .messageSeverity = vk::DebugUtilsMessageSeverityFlagBitsEXT::eInfo |
+                           vk::DebugUtilsMessageSeverityFlagBitsEXT::eError |
+                           vk::DebugUtilsMessageSeverityFlagBitsEXT::eWarning |
+                           vk::DebugUtilsMessageSeverityFlagBitsEXT::eVerbose,
+        .messageType = vk::DebugUtilsMessageTypeFlagBitsEXT::eGeneral |
+                       vk::DebugUtilsMessageTypeFlagBitsEXT::eValidation |
+                       vk::DebugUtilsMessageTypeFlagBitsEXT::eDeviceAddressBinding |
+                       vk::DebugUtilsMessageTypeFlagBitsEXT::ePerformance,
+        .pfnUserCallback = DebugUtilsCallback,
+    };
+    return instance.createDebugUtilsMessengerEXTUnique(msg_ci);
+}
+
+vk::UniqueDebugReportCallbackEXT CreateDebugReportCallback(vk::Instance instance) {
+    const vk::DebugReportCallbackCreateInfoEXT callback_ci = {
+        .flags = vk::DebugReportFlagBitsEXT::eDebug | vk::DebugReportFlagBitsEXT::eInformation |
+                 vk::DebugReportFlagBitsEXT::eError |
+                 vk::DebugReportFlagBitsEXT::ePerformanceWarning |
+                 vk::DebugReportFlagBitsEXT::eWarning,
+        .pfnCallback = DebugReportCallback,
+    };
+    return instance.createDebugReportCallbackEXTUnique(callback_ci);
+}
+
+DebugCallback CreateDebugCallback(vk::Instance instance) {
+    if (!Settings::values.renderer_debug) {
+        return {};
+    }
+    const auto properties = vk::enumerateInstanceExtensionProperties();
+    if (properties.empty()) {
+        LOG_ERROR(Render_Vulkan, "Failed to query extension properties");
+        return {};
+    }
+    const auto it = std::find_if(properties.begin(), properties.end(), [](const auto& prop) {
+        return std::strcmp(VK_EXT_DEBUG_UTILS_EXTENSION_NAME, prop.extensionName) == 0;
+    });
+    // Prefer debug util messenger if available.
+    if (it != properties.end()) {
+        return CreateDebugMessenger(instance);
+    }
+    // Otherwise fallback to debug report callback.
+    return CreateDebugReportCallback(instance);
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_platform.h b/src/video_core/renderer_vulkan/vk_platform.h
new file mode 100644
index 000000000..f4b69e6c2
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_platform.h
@@ -0,0 +1,34 @@
+// Copyright 2023 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <variant>
+
+#include "common/common_types.h"
+#include "common/dynamic_library/dynamic_library.h"
+#include "video_core/renderer_vulkan/vk_common.h"
+
+namespace Frontend {
+class EmuWindow;
+enum class WindowSystemType : u8;
+} // namespace Frontend
+
+namespace Vulkan {
+
+using DebugCallback =
+    std::variant<vk::UniqueDebugUtilsMessengerEXT, vk::UniqueDebugReportCallbackEXT>;
+
+std::shared_ptr<Common::DynamicLibrary> OpenLibrary();
+
+vk::SurfaceKHR CreateSurface(vk::Instance instance, const Frontend::EmuWindow& emu_window);
+
+vk::UniqueInstance CreateInstance(const Common::DynamicLibrary& library,
+                                  Frontend::WindowSystemType window_type, bool enable_validation,
+                                  bool dump_command_buffers);
+
+DebugCallback CreateDebugCallback(vk::Instance instance);
+
+} // namespace Vulkan