diff --git a/.gitmodules b/.gitmodules
index a33a041672..2558a5ebc1 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -37,3 +37,6 @@
 [submodule "discord-rpc"]
     path = externals/discord-rpc
     url = https://github.com/discordapp/discord-rpc.git
+[submodule "Vulkan-Headers"]
+    path = externals/Vulkan-Headers
+    url = https://github.com/KhronosGroup/Vulkan-Headers.git
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 97d888762d..32cfa85802 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -23,6 +23,8 @@ option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OF
 
 option(ENABLE_CUBEB "Enables the cubeb audio backend" ON)
 
+option(ENABLE_VULKAN "Enables Vulkan backend" ON)
+
 option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
 
 if(NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit)
diff --git a/externals/Vulkan-Headers b/externals/Vulkan-Headers
new file mode 160000
index 0000000000..7f02d9bb81
--- /dev/null
+++ b/externals/Vulkan-Headers
@@ -0,0 +1 @@
+Subproject commit 7f02d9bb810f371de0fe833c80004c34f7ff8c57
diff --git a/src/common/logging/backend.cpp b/src/common/logging/backend.cpp
index 12f6d01144..a5e031189b 100644
--- a/src/common/logging/backend.cpp
+++ b/src/common/logging/backend.cpp
@@ -232,6 +232,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 d4ec31ec35..8ed6d50502 100644
--- a/src/common/logging/log.h
+++ b/src/common/logging/log.h
@@ -112,6 +112,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 implementation of the DSP
     Audio_Sink,        ///< Emulator audio output backend
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 1db0d031d9..d35a738d59 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -101,6 +101,16 @@ add_library(video_core STATIC
     video_core.h
 )
 
+if (ENABLE_VULKAN)
+    target_sources(video_core PRIVATE
+        renderer_vulkan/declarations.h
+        renderer_vulkan/vk_device.cpp
+        renderer_vulkan/vk_device.h)
+
+    target_include_directories(video_core PRIVATE ../../externals/Vulkan-Headers/include)
+    target_compile_definitions(video_core PRIVATE HAS_VULKAN)
+endif()
+
 create_target_directory_groups(video_core)
 
 target_link_libraries(video_core PUBLIC common core)
diff --git a/src/video_core/renderer_vulkan/declarations.h b/src/video_core/renderer_vulkan/declarations.h
new file mode 100644
index 0000000000..ba25b5bc70
--- /dev/null
+++ b/src/video_core/renderer_vulkan/declarations.h
@@ -0,0 +1,45 @@
+// Copyright 2019 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <vulkan/vulkan.hpp>
+
+namespace Vulkan {
+
+// vulkan.hpp unique handlers use DispatchLoaderStatic
+template <typename T>
+using UniqueHandle = vk::UniqueHandle<T, vk::DispatchLoaderDynamic>;
+
+using UniqueAccelerationStructureNV = UniqueHandle<vk::AccelerationStructureNV>;
+using UniqueBuffer = UniqueHandle<vk::Buffer>;
+using UniqueBufferView = UniqueHandle<vk::BufferView>;
+using UniqueCommandBuffer = UniqueHandle<vk::CommandBuffer>;
+using UniqueCommandPool = UniqueHandle<vk::CommandPool>;
+using UniqueDescriptorPool = UniqueHandle<vk::DescriptorPool>;
+using UniqueDescriptorSet = UniqueHandle<vk::DescriptorSet>;
+using UniqueDescriptorSetLayout = UniqueHandle<vk::DescriptorSetLayout>;
+using UniqueDescriptorUpdateTemplate = UniqueHandle<vk::DescriptorUpdateTemplate>;
+using UniqueDevice = UniqueHandle<vk::Device>;
+using UniqueDeviceMemory = UniqueHandle<vk::DeviceMemory>;
+using UniqueEvent = UniqueHandle<vk::Event>;
+using UniqueFence = UniqueHandle<vk::Fence>;
+using UniqueFramebuffer = UniqueHandle<vk::Framebuffer>;
+using UniqueImage = UniqueHandle<vk::Image>;
+using UniqueImageView = UniqueHandle<vk::ImageView>;
+using UniqueIndirectCommandsLayoutNVX = UniqueHandle<vk::IndirectCommandsLayoutNVX>;
+using UniqueObjectTableNVX = UniqueHandle<vk::ObjectTableNVX>;
+using UniquePipeline = UniqueHandle<vk::Pipeline>;
+using UniquePipelineCache = UniqueHandle<vk::PipelineCache>;
+using UniquePipelineLayout = UniqueHandle<vk::PipelineLayout>;
+using UniqueQueryPool = UniqueHandle<vk::QueryPool>;
+using UniqueRenderPass = UniqueHandle<vk::RenderPass>;
+using UniqueSampler = UniqueHandle<vk::Sampler>;
+using UniqueSamplerYcbcrConversion = UniqueHandle<vk::SamplerYcbcrConversion>;
+using UniqueSemaphore = UniqueHandle<vk::Semaphore>;
+using UniqueShaderModule = UniqueHandle<vk::ShaderModule>;
+using UniqueSwapchainKHR = UniqueHandle<vk::SwapchainKHR>;
+using UniqueValidationCacheEXT = UniqueHandle<vk::ValidationCacheEXT>;
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_device.cpp b/src/video_core/renderer_vulkan/vk_device.cpp
new file mode 100644
index 0000000000..78a4e5f0e0
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_device.cpp
@@ -0,0 +1,231 @@
+// Copyright 2018 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <map>
+#include <optional>
+#include <set>
+#include <vector>
+#include "common/assert.h"
+#include "video_core/renderer_vulkan/declarations.h"
+#include "video_core/renderer_vulkan/vk_device.h"
+
+namespace Vulkan {
+
+namespace Alternatives {
+
+constexpr std::array<vk::Format, 3> Depth24UnormS8Uint = {
+    vk::Format::eD32SfloatS8Uint, vk::Format::eD16UnormS8Uint, {}};
+constexpr std::array<vk::Format, 3> Depth16UnormS8Uint = {
+    vk::Format::eD24UnormS8Uint, vk::Format::eD32SfloatS8Uint, {}};
+
+} // namespace Alternatives
+
+constexpr const vk::Format* GetFormatAlternatives(vk::Format format) {
+    switch (format) {
+    case vk::Format::eD24UnormS8Uint:
+        return Alternatives::Depth24UnormS8Uint.data();
+    case vk::Format::eD16UnormS8Uint:
+        return Alternatives::Depth16UnormS8Uint.data();
+    default:
+        return nullptr;
+    }
+}
+
+constexpr vk::FormatFeatureFlags GetFormatFeatures(vk::FormatProperties properties,
+                                                   FormatType format_type) {
+    switch (format_type) {
+    case FormatType::Linear:
+        return properties.linearTilingFeatures;
+    case FormatType::Optimal:
+        return properties.optimalTilingFeatures;
+    case FormatType::Buffer:
+        return properties.bufferFeatures;
+    default:
+        return {};
+    }
+}
+
+VKDevice::VKDevice(const vk::DispatchLoaderDynamic& dldi, vk::PhysicalDevice physical,
+                   vk::SurfaceKHR surface)
+    : physical{physical}, format_properties{GetFormatProperties(dldi, physical)} {
+    SetupFamilies(dldi, surface);
+    SetupProperties(dldi);
+}
+
+VKDevice::~VKDevice() = default;
+
+bool VKDevice::Create(const vk::DispatchLoaderDynamic& dldi, vk::Instance instance) {
+    const auto queue_cis = GetDeviceQueueCreateInfos();
+    vk::PhysicalDeviceFeatures device_features{};
+
+    const std::vector<const char*> extensions = {VK_KHR_SWAPCHAIN_EXTENSION_NAME};
+    const vk::DeviceCreateInfo device_ci({}, static_cast<u32>(queue_cis.size()), queue_cis.data(),
+                                         0, nullptr, static_cast<u32>(extensions.size()),
+                                         extensions.data(), &device_features);
+    vk::Device dummy_logical;
+    if (physical.createDevice(&device_ci, nullptr, &dummy_logical, dldi) != vk::Result::eSuccess) {
+        LOG_CRITICAL(Render_Vulkan, "Logical device failed to be created!");
+        return false;
+    }
+
+    dld.init(instance, dldi.vkGetInstanceProcAddr, dummy_logical, dldi.vkGetDeviceProcAddr);
+    logical = UniqueDevice(
+        dummy_logical, vk::ObjectDestroy<vk::NoParent, vk::DispatchLoaderDynamic>(nullptr, dld));
+
+    graphics_queue = logical->getQueue(graphics_family, 0, dld);
+    present_queue = logical->getQueue(present_family, 0, dld);
+    return true;
+}
+
+vk::Format VKDevice::GetSupportedFormat(vk::Format wanted_format,
+                                        vk::FormatFeatureFlags wanted_usage,
+                                        FormatType format_type) const {
+    if (IsFormatSupported(wanted_format, wanted_usage, format_type)) {
+        return wanted_format;
+    }
+    // The wanted format is not supported by hardware, search for alternatives
+    const vk::Format* alternatives = GetFormatAlternatives(wanted_format);
+    if (alternatives == nullptr) {
+        LOG_CRITICAL(Render_Vulkan,
+                     "Format={} with usage={} and type={} has no defined alternatives and host "
+                     "hardware does not support it",
+                     static_cast<u32>(wanted_format), static_cast<u32>(wanted_usage),
+                     static_cast<u32>(format_type));
+        UNREACHABLE();
+        return wanted_format;
+    }
+
+    std::size_t i = 0;
+    for (vk::Format alternative = alternatives[0]; alternative != vk::Format{};
+         alternative = alternatives[++i]) {
+        if (!IsFormatSupported(alternative, wanted_usage, format_type))
+            continue;
+        LOG_WARNING(Render_Vulkan,
+                    "Emulating format={} with alternative format={} with usage={} and type={}",
+                    static_cast<u32>(wanted_format), static_cast<u32>(alternative),
+                    static_cast<u32>(wanted_usage), static_cast<u32>(format_type));
+        return alternative;
+    }
+
+    // No alternatives found, panic
+    LOG_CRITICAL(Render_Vulkan,
+                 "Format={} with usage={} and type={} is not supported by the host hardware and "
+                 "doesn't support any of the alternatives",
+                 static_cast<u32>(wanted_format), static_cast<u32>(wanted_usage),
+                 static_cast<u32>(format_type));
+    UNREACHABLE();
+    return wanted_format;
+}
+
+bool VKDevice::IsFormatSupported(vk::Format wanted_format, vk::FormatFeatureFlags wanted_usage,
+                                 FormatType format_type) const {
+    const auto it = format_properties.find(wanted_format);
+    if (it == format_properties.end()) {
+        LOG_CRITICAL(Render_Vulkan, "Unimplemented format query={}",
+                     static_cast<u32>(wanted_format));
+        UNREACHABLE();
+        return true;
+    }
+    const vk::FormatFeatureFlags supported_usage = GetFormatFeatures(it->second, format_type);
+    return (supported_usage & wanted_usage) == wanted_usage;
+}
+
+bool VKDevice::IsSuitable(const vk::DispatchLoaderDynamic& dldi, vk::PhysicalDevice physical,
+                          vk::SurfaceKHR surface) {
+    const std::string swapchain_extension = VK_KHR_SWAPCHAIN_EXTENSION_NAME;
+
+    bool has_swapchain{};
+    for (const auto& prop : physical.enumerateDeviceExtensionProperties(nullptr, dldi)) {
+        has_swapchain |= prop.extensionName == swapchain_extension;
+    }
+    if (!has_swapchain) {
+        // The device doesn't support creating swapchains.
+        return false;
+    }
+
+    bool has_graphics{}, has_present{};
+    const auto queue_family_properties = physical.getQueueFamilyProperties(dldi);
+    for (u32 i = 0; i < static_cast<u32>(queue_family_properties.size()); ++i) {
+        const auto& family = queue_family_properties[i];
+        if (family.queueCount == 0)
+            continue;
+
+        has_graphics |=
+            (family.queueFlags & vk::QueueFlagBits::eGraphics) != static_cast<vk::QueueFlagBits>(0);
+        has_present |= physical.getSurfaceSupportKHR(i, surface, dldi) != 0;
+    }
+    if (!has_graphics || !has_present) {
+        // The device doesn't have a graphics and present queue.
+        return false;
+    }
+
+    // TODO(Rodrigo): Check if the device matches all requeriments.
+    const vk::PhysicalDeviceProperties props = physical.getProperties(dldi);
+    if (props.limits.maxUniformBufferRange < 65536) {
+        return false;
+    }
+
+    // Device is suitable.
+    return true;
+}
+
+void VKDevice::SetupFamilies(const vk::DispatchLoaderDynamic& dldi, vk::SurfaceKHR surface) {
+    std::optional<u32> graphics_family_, present_family_;
+
+    const auto queue_family_properties = physical.getQueueFamilyProperties(dldi);
+    for (u32 i = 0; i < static_cast<u32>(queue_family_properties.size()); ++i) {
+        if (graphics_family_ && present_family_)
+            break;
+
+        const auto& queue_family = queue_family_properties[i];
+        if (queue_family.queueCount == 0)
+            continue;
+
+        if (queue_family.queueFlags & vk::QueueFlagBits::eGraphics)
+            graphics_family_ = i;
+        if (physical.getSurfaceSupportKHR(i, surface, dldi))
+            present_family_ = i;
+    }
+    ASSERT(graphics_family_ && present_family_);
+
+    graphics_family = *graphics_family_;
+    present_family = *present_family_;
+}
+
+void VKDevice::SetupProperties(const vk::DispatchLoaderDynamic& dldi) {
+    const vk::PhysicalDeviceProperties props = physical.getProperties(dldi);
+    device_type = props.deviceType;
+    uniform_buffer_alignment = static_cast<u64>(props.limits.minUniformBufferOffsetAlignment);
+}
+
+std::vector<vk::DeviceQueueCreateInfo> VKDevice::GetDeviceQueueCreateInfos() const {
+    static const float QUEUE_PRIORITY = 1.f;
+
+    std::set<u32> unique_queue_families = {graphics_family, present_family};
+    std::vector<vk::DeviceQueueCreateInfo> queue_cis;
+
+    for (u32 queue_family : unique_queue_families)
+        queue_cis.push_back({{}, queue_family, 1, &QUEUE_PRIORITY});
+
+    return queue_cis;
+}
+
+std::map<vk::Format, vk::FormatProperties> VKDevice::GetFormatProperties(
+    const vk::DispatchLoaderDynamic& dldi, vk::PhysicalDevice physical) {
+    std::map<vk::Format, vk::FormatProperties> format_properties;
+
+    const auto AddFormatQuery = [&format_properties, &dldi, physical](vk::Format format) {
+        format_properties.emplace(format, physical.getFormatProperties(format, dldi));
+    };
+    AddFormatQuery(vk::Format::eA8B8G8R8UnormPack32);
+    AddFormatQuery(vk::Format::eR5G6B5UnormPack16);
+    AddFormatQuery(vk::Format::eD32Sfloat);
+    AddFormatQuery(vk::Format::eD16UnormS8Uint);
+    AddFormatQuery(vk::Format::eD24UnormS8Uint);
+    AddFormatQuery(vk::Format::eD32SfloatS8Uint);
+
+    return format_properties;
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_device.h b/src/video_core/renderer_vulkan/vk_device.h
new file mode 100644
index 0000000000..e87c7a5085
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_device.h
@@ -0,0 +1,116 @@
+// Copyright 2018 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <map>
+#include <vector>
+#include "common/common_types.h"
+#include "video_core/renderer_vulkan/declarations.h"
+
+namespace Vulkan {
+
+/// Format usage descriptor
+enum class FormatType { Linear, Optimal, Buffer };
+
+/// Handles data specific to a physical device.
+class VKDevice final {
+public:
+    explicit VKDevice(const vk::DispatchLoaderDynamic& dldi, vk::PhysicalDevice physical,
+                      vk::SurfaceKHR surface);
+    ~VKDevice();
+
+    /// Initializes the device. Returns true on success.
+    bool Create(const vk::DispatchLoaderDynamic& dldi, vk::Instance instance);
+
+    /**
+     * Returns a format supported by the device for the passed requeriments.
+     * @param wanted_format The ideal format to be returned. It may not be the returned format.
+     * @param wanted_usage The usage that must be fulfilled even if the format is not supported.
+     * @param format_type Format type usage.
+     * @returns A format supported by the device.
+     */
+    vk::Format GetSupportedFormat(vk::Format wanted_format, vk::FormatFeatureFlags wanted_usage,
+                                  FormatType format_type) const;
+
+    /// Returns the dispatch loader with direct function pointers of the device
+    const vk::DispatchLoaderDynamic& GetDispatchLoader() const {
+        return dld;
+    }
+
+    /// Returns the logical device
+    vk::Device GetLogical() const {
+        return logical.get();
+    }
+
+    /// Returns the physical device.
+    vk::PhysicalDevice GetPhysical() const {
+        return physical;
+    }
+
+    /// Returns the main graphics queue.
+    vk::Queue GetGraphicsQueue() const {
+        return graphics_queue;
+    }
+
+    /// Returns the main present queue.
+    vk::Queue GetPresentQueue() const {
+        return present_queue;
+    }
+
+    /// Returns main graphics queue family index.
+    u32 GetGraphicsFamily() const {
+        return graphics_family;
+    }
+
+    /// Returns main present queue family index.
+    u32 GetPresentFamily() const {
+        return present_family;
+    }
+
+    /// Returns if the device is integrated with the host CPU
+    bool IsIntegrated() const {
+        return device_type == vk::PhysicalDeviceType::eIntegratedGpu;
+    }
+
+    /// Returns uniform buffer alignment requeriment
+    u64 GetUniformBufferAlignment() const {
+        return uniform_buffer_alignment;
+    }
+
+    /// Checks if the physical device is suitable.
+    static bool IsSuitable(const vk::DispatchLoaderDynamic& dldi, vk::PhysicalDevice physical,
+                           vk::SurfaceKHR surface);
+
+private:
+    /// Sets up queue families.
+    void SetupFamilies(const vk::DispatchLoaderDynamic& dldi, vk::SurfaceKHR surface);
+
+    /// Sets up device properties.
+    void SetupProperties(const vk::DispatchLoaderDynamic& dldi);
+
+    /// Returns a list of queue initialization descriptors.
+    std::vector<vk::DeviceQueueCreateInfo> GetDeviceQueueCreateInfos() const;
+
+    /// Returns true if a format is supported.
+    bool IsFormatSupported(vk::Format wanted_format, vk::FormatFeatureFlags wanted_usage,
+                           FormatType format_type) const;
+
+    /// Returns the device properties for Vulkan formats.
+    static std::map<vk::Format, vk::FormatProperties> GetFormatProperties(
+        const vk::DispatchLoaderDynamic& dldi, vk::PhysicalDevice physical);
+
+    const vk::PhysicalDevice physical;  ///< Physical device
+    vk::DispatchLoaderDynamic dld;      ///< Device function pointers
+    UniqueDevice logical;               ///< Logical device
+    vk::Queue graphics_queue;           ///< Main graphics queue
+    vk::Queue present_queue;            ///< Main present queue
+    u32 graphics_family{};              ///< Main graphics queue family index
+    u32 present_family{};               ///< Main present queue family index
+    vk::PhysicalDeviceType device_type; ///< Physical device type
+    u64 uniform_buffer_alignment{};     ///< Uniform buffer alignment requeriment
+    std::map<vk::Format, vk::FormatProperties> format_properties; ///< Format properties dictionary
+};
+
+} // namespace Vulkan